mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AuthN: Rebuild Authenticate so we only have to call it once in context handler (#61705)
* API: Add reqSignedIn to router groups * AuthN: Add fall through in context handler * AuthN: Add IsAnonymous field * AuthN: add priority to context aware clients * ContextHandler: Add comment * AuthN: Add a simple priority queue * AuthN: Add Name to client interface * AuthN: register clients with function * AuthN: update mock and fake to implement interface * AuthN: rewrite test without reflection * AuthN: add comment * AuthN: fix queue insert * AuthN: rewrite tests * AuthN: make the queue generic so we can reuse it for hooks * ContextHandler: Add fixme for auth headers * AuthN: remove unused variable * AuthN: use multierror * AuthN: write proper tests for queue * AuthN: Add queue item that can store the value and priority Co-authored-by: Jo <joao.guerreiro@grafana.com>
This commit is contained in:
@@ -670,7 +670,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersSync)), routing.Wrap(hs.PostSyncUserWithLDAP))
|
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersSync)), routing.Wrap(hs.PostSyncUserWithLDAP))
|
||||||
adminRoute.Get("/ldap/:username", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersRead)), routing.Wrap(hs.GetUserFromLDAP))
|
adminRoute.Get("/ldap/:username", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersRead)), routing.Wrap(hs.GetUserFromLDAP))
|
||||||
adminRoute.Get("/ldap/status", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), routing.Wrap(hs.GetLDAPStatus))
|
adminRoute.Get("/ldap/status", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), routing.Wrap(hs.GetLDAPStatus))
|
||||||
})
|
}, reqSignedIn)
|
||||||
|
|
||||||
// Administering users
|
// Administering users
|
||||||
r.Group("/api/admin/users", func(adminUserRoute routing.RouteRegister) {
|
r.Group("/api/admin/users", func(adminUserRoute routing.RouteRegister) {
|
||||||
@@ -688,7 +688,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
adminUserRoute.Post("/:id/logout", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
|
adminUserRoute.Post("/:id/logout", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
|
||||||
adminUserRoute.Get("/:id/auth-tokens", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
|
adminUserRoute.Get("/:id/auth-tokens", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
|
||||||
adminUserRoute.Post("/:id/revoke-auth-token", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersAuthTokenUpdate, userIDScope)), routing.Wrap(hs.AdminRevokeUserAuthToken))
|
adminUserRoute.Post("/:id/revoke-auth-token", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersAuthTokenUpdate, userIDScope)), routing.Wrap(hs.AdminRevokeUserAuthToken))
|
||||||
})
|
}, reqSignedIn)
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
r.Get("/render/*", reqSignedIn, hs.RenderToPng)
|
r.Get("/render/*", reqSignedIn, hs.RenderToPng)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -37,7 +38,7 @@ func accessForbidden(c *models.ReqContext) {
|
|||||||
|
|
||||||
func notAuthorized(c *models.ReqContext) {
|
func notAuthorized(c *models.ReqContext) {
|
||||||
if c.IsApiRequest() {
|
if c.IsApiRequest() {
|
||||||
c.JsonApiErr(401, "Unauthorized", nil)
|
c.WriteErrOrFallback(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), c.LookupTokenErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) er
|
|||||||
type PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, err error)
|
type PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, err error)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// Authenticate authenticates a request using the specified client.
|
// Authenticate authenticates a request
|
||||||
Authenticate(ctx context.Context, client string, r *Request) (*Identity, bool, error)
|
Authenticate(ctx context.Context, r *Request) (*Identity, error)
|
||||||
// RegisterPostAuthHook registers a hook that is called after a successful authentication.
|
// RegisterPostAuthHook registers a hook that is called after a successful authentication.
|
||||||
RegisterPostAuthHook(hook PostAuthHookFn)
|
RegisterPostAuthHook(hook PostAuthHookFn)
|
||||||
// Login authenticates a request and creates a session on successful authentication.
|
// Login authenticates a request and creates a session on successful authentication.
|
||||||
@@ -65,10 +65,18 @@ type Service interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
|
// Name returns the name of a client
|
||||||
|
Name() string
|
||||||
// Authenticate performs the authentication for the request
|
// Authenticate performs the authentication for the request
|
||||||
Authenticate(ctx context.Context, r *Request) (*Identity, error)
|
Authenticate(ctx context.Context, r *Request) (*Identity, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextAwareClient interface {
|
||||||
|
Client
|
||||||
// Test should return true if client can be used to authenticate request
|
// Test should return true if client can be used to authenticate request
|
||||||
Test(ctx context.Context, r *Request) bool
|
Test(ctx context.Context, r *Request) bool
|
||||||
|
// Priority for the client, a lower number means higher priority
|
||||||
|
Priority() uint
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedirectClient interface {
|
type RedirectClient interface {
|
||||||
@@ -132,6 +140,8 @@ type Identity struct {
|
|||||||
// Namespace* constants. For example, "user:1" or "api-key:1".
|
// Namespace* constants. For example, "user:1" or "api-key:1".
|
||||||
// If the entity is not found in the DB or this entity is non-persistent, this field will be empty.
|
// If the entity is not found in the DB or this entity is non-persistent, this field will be empty.
|
||||||
ID string
|
ID string
|
||||||
|
// IsAnonymous
|
||||||
|
IsAnonymous bool
|
||||||
// Login is the short hand identifier of the entity. Should be unique.
|
// Login is the short hand identifier of the entity. Should be unique.
|
||||||
Login string
|
Login string
|
||||||
// Name is the display name of the entity. It is not guaranteed to be unique.
|
// Name is the display name of the entity. It is not guaranteed to be unique.
|
||||||
@@ -171,11 +181,6 @@ func (i *Identity) Role() org.RoleType {
|
|||||||
return i.OrgRoles[i.OrgID]
|
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 == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: improve error handling
|
// TODO: improve error handling
|
||||||
func (i *Identity) NamespacedID() (string, int64) {
|
func (i *Identity) NamespacedID() (string, int64) {
|
||||||
var (
|
var (
|
||||||
@@ -222,7 +227,7 @@ func (i *Identity) SignedInUser() *user.SignedInUser {
|
|||||||
Email: i.Email,
|
Email: i.Email,
|
||||||
OrgCount: i.OrgCount,
|
OrgCount: i.OrgCount,
|
||||||
IsGrafanaAdmin: isGrafanaAdmin,
|
IsGrafanaAdmin: isGrafanaAdmin,
|
||||||
IsAnonymous: i.IsAnonymous(),
|
IsAnonymous: i.IsAnonymous,
|
||||||
IsDisabled: i.IsDisabled,
|
IsDisabled: i.IsDisabled,
|
||||||
HelpFlags1: i.HelpFlags1,
|
HelpFlags1: i.HelpFlags1,
|
||||||
LastSeenAt: i.LastSeenAt,
|
LastSeenAt: i.LastSeenAt,
|
||||||
|
|||||||
34
pkg/services/authn/authnimpl/priority_queue.go
Normal file
34
pkg/services/authn/authnimpl/priority_queue.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package authnimpl
|
||||||
|
|
||||||
|
func newQueue[T any]() *queue[T] {
|
||||||
|
return &queue[T]{items: []queueItem[T]{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
type queue[T any] struct {
|
||||||
|
items []queueItem[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
type queueItem[T any] struct {
|
||||||
|
v T
|
||||||
|
p uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *queue[T]) insert(v T, p uint) {
|
||||||
|
// no items in the queue so we just add it
|
||||||
|
if len(q.items) == 0 {
|
||||||
|
q.items = append(q.items, queueItem[T]{v, p})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the position in the queue the item should be placed based on priority
|
||||||
|
for i, item := range q.items {
|
||||||
|
if p < item.p {
|
||||||
|
q.items = append(q.items[:i+1], q.items[i:]...)
|
||||||
|
q.items[i] = queueItem[T]{v, p}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// item did not have higher priority then what is in the queue currently, so we need to add it to the end
|
||||||
|
q.items = append(q.items, queueItem[T]{v, p})
|
||||||
|
}
|
||||||
48
pkg/services/authn/authnimpl/priority_queue_test.go
Normal file
48
pkg/services/authn/authnimpl/priority_queue_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package authnimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQueue(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
desc string
|
||||||
|
clients []authn.ContextAwareClient
|
||||||
|
expectedOrder []string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
desc: "expect correct order",
|
||||||
|
clients: []authn.ContextAwareClient{
|
||||||
|
&authntest.FakeClient{ExpectedName: "1", ExpectedPriority: 1},
|
||||||
|
&authntest.FakeClient{ExpectedName: "5", ExpectedPriority: 5},
|
||||||
|
&authntest.FakeClient{ExpectedName: "3", ExpectedPriority: 3},
|
||||||
|
&authntest.FakeClient{ExpectedName: "2", ExpectedPriority: 2},
|
||||||
|
&authntest.FakeClient{ExpectedName: "4", ExpectedPriority: 4},
|
||||||
|
},
|
||||||
|
expectedOrder: []string{"1", "2", "3", "4", "5"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
q := newQueue[authn.ContextAwareClient]()
|
||||||
|
for _, c := range tt.clients {
|
||||||
|
q.insert(c, c.Priority())
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, q.items, len(tt.expectedOrder))
|
||||||
|
|
||||||
|
for i := range q.items {
|
||||||
|
assert.Equal(t, q.items[i].v.Name(), tt.expectedOrder[i])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
@@ -34,7 +35,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errDisabledIdentity = errutil.NewBase(errutil.StatusUnauthorized, "identity.disabled")
|
errCantAuthenticateReq = errutil.NewBase(errutil.StatusUnauthorized, "auth.unauthorized")
|
||||||
|
errDisabledIdentity = errutil.NewBase(errutil.StatusUnauthorized, "identity.disabled")
|
||||||
)
|
)
|
||||||
|
|
||||||
// make sure service implements authn.Service interface
|
// make sure service implements authn.Service interface
|
||||||
@@ -55,20 +57,23 @@ func ProvideService(
|
|||||||
log: log.New("authn.service"),
|
log: log.New("authn.service"),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
clients: make(map[string]authn.Client),
|
clients: make(map[string]authn.Client),
|
||||||
|
clientQueue: newQueue[authn.ContextAwareClient](),
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
sessionService: sessionService,
|
sessionService: sessionService,
|
||||||
postAuthHooks: []authn.PostAuthHookFn{},
|
postAuthHooks: []authn.PostAuthHookFn{},
|
||||||
}
|
}
|
||||||
|
|
||||||
s.clients[authn.ClientRender] = clients.ProvideRender(userService, renderService)
|
s.RegisterClient(clients.ProvideRender(userService, renderService))
|
||||||
s.clients[authn.ClientAPIKey] = clients.ProvideAPIKey(apikeyService, userService)
|
s.RegisterClient(clients.ProvideAPIKey(apikeyService, userService))
|
||||||
|
|
||||||
sessionClient := clients.ProvideSession(sessionService, userService, cfg.LoginCookieName, cfg.LoginMaxLifetime)
|
if cfg.LoginCookieName != "" {
|
||||||
s.clients[authn.ClientSession] = sessionClient
|
sessionClient := clients.ProvideSession(sessionService, userService, cfg.LoginCookieName, cfg.LoginMaxLifetime)
|
||||||
s.RegisterPostAuthHook(sessionClient.RefreshTokenHook)
|
s.RegisterClient(sessionClient)
|
||||||
|
s.RegisterPostAuthHook(sessionClient.RefreshTokenHook)
|
||||||
|
}
|
||||||
|
|
||||||
if s.cfg.AnonymousEnabled {
|
if s.cfg.AnonymousEnabled {
|
||||||
s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService)
|
s.RegisterClient(clients.ProvideAnonymous(cfg, orgService))
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxyClients []authn.ProxyClient
|
var proxyClients []authn.ProxyClient
|
||||||
@@ -89,11 +94,11 @@ func ProvideService(
|
|||||||
if len(passwordClients) > 0 {
|
if len(passwordClients) > 0 {
|
||||||
passwordClient := clients.ProvidePassword(loginAttempts, passwordClients...)
|
passwordClient := clients.ProvidePassword(loginAttempts, passwordClients...)
|
||||||
if s.cfg.BasicAuthEnabled {
|
if s.cfg.BasicAuthEnabled {
|
||||||
s.clients[authn.ClientBasic] = clients.ProvideBasic(passwordClient)
|
s.RegisterClient(clients.ProvideBasic(passwordClient))
|
||||||
}
|
}
|
||||||
// FIXME (kalleep): Remove the global variable and stick it into cfg
|
// FIXME (kalleep): Remove the global variable and stick it into cfg
|
||||||
if !setting.DisableLoginForm {
|
if !setting.DisableLoginForm {
|
||||||
s.clients[authn.ClientForm] = clients.ProvideForm(passwordClient)
|
s.RegisterClient(clients.ProvideForm(passwordClient))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,12 +107,12 @@ func ProvideService(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Error("failed to configure auth proxy", "err", err)
|
s.log.Error("failed to configure auth proxy", "err", err)
|
||||||
} else {
|
} else {
|
||||||
s.clients[authn.ClientProxy] = proxy
|
s.RegisterClient(proxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.cfg.JWTAuthEnabled {
|
if s.cfg.JWTAuthEnabled {
|
||||||
s.clients[authn.ClientJWT] = clients.ProvideJWT(jwtService, cfg)
|
s.RegisterClient(clients.ProvideJWT(jwtService, cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME (jguer): move to User package
|
// FIXME (jguer): move to User package
|
||||||
@@ -126,9 +131,11 @@ func ProvideService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
clients map[string]authn.Client
|
|
||||||
|
clients map[string]authn.Client
|
||||||
|
clientQueue *queue[authn.ContextAwareClient]
|
||||||
|
|
||||||
tracer tracing.Tracer
|
tracer tracing.Tracer
|
||||||
sessionService auth.UserTokenService
|
sessionService auth.UserTokenService
|
||||||
@@ -139,42 +146,54 @@ type Service struct {
|
|||||||
postLoginHooks []authn.PostLoginHookFn
|
postLoginHooks []authn.PostLoginHookFn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Request) (*authn.Identity, bool, error) {
|
func (s *Service) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
c, ok := s.clients[client]
|
|
||||||
if !ok {
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.Test(ctx, r) {
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, span := s.tracer.Start(ctx, "authn.Authenticate")
|
ctx, span := s.tracer.Start(ctx, "authn.Authenticate")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
span.SetAttributes("authn.client", client, attribute.Key("authn.client").String(client))
|
|
||||||
|
|
||||||
|
var authErr error
|
||||||
|
for _, item := range s.clientQueue.items {
|
||||||
|
if item.v.Test(ctx, r) {
|
||||||
|
identity, err := s.authenticate(ctx, item.v, r)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("failed to authenticate", "client", item.v.Name(), "err", err)
|
||||||
|
authErr = multierror.Append(authErr, err)
|
||||||
|
// try next
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if identity != nil {
|
||||||
|
return identity, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if authErr != nil {
|
||||||
|
return nil, authErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errCantAuthenticateReq.Errorf("cannot authenticate request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) authenticate(ctx context.Context, c authn.Client, r *authn.Request) (*authn.Identity, error) {
|
||||||
r.OrgID = orgIDFromRequest(r)
|
r.OrgID = orgIDFromRequest(r)
|
||||||
identity, err := c.Authenticate(ctx, r)
|
identity, err := c.Authenticate(ctx, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.FromContext(ctx).Warn("auth client could not authenticate request", "client", client, "error", err)
|
s.log.FromContext(ctx).Warn("auth client could not authenticate request", "client", c.Name(), "error", err)
|
||||||
span.AddEvents([]string{"message"}, []tracing.EventValue{{Str: "auth client could not authenticate request"}})
|
return nil, err
|
||||||
return nil, true, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME (kalleep): Handle disabled identities
|
|
||||||
|
|
||||||
for _, hook := range s.postAuthHooks {
|
for _, hook := range s.postAuthHooks {
|
||||||
if err := hook(ctx, identity, r); err != nil {
|
if err := hook(ctx, identity, r); err != nil {
|
||||||
s.log.FromContext(ctx).Warn("post auth hook failed", "error", err, "id", identity)
|
s.log.FromContext(ctx).Warn("post auth hook failed", "error", err, "id", identity)
|
||||||
return nil, false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if identity.IsDisabled {
|
if identity.IsDisabled {
|
||||||
return nil, true, errDisabledIdentity.Errorf("identity is disabled")
|
return nil, errDisabledIdentity.Errorf("identity is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
return identity, true, nil
|
return identity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
|
func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
|
||||||
@@ -182,18 +201,18 @@ func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (identity *authn.Identity, err error) {
|
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() {
|
defer func() {
|
||||||
for _, hook := range s.postLoginHooks {
|
for _, hook := range s.postLoginHooks {
|
||||||
hook(ctx, identity, r, err)
|
hook(ctx, identity, r, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
c, ok := s.clients[client]
|
||||||
|
if !ok {
|
||||||
|
return nil, authn.ErrClientNotConfigured.Errorf("client not configured: %s", client)
|
||||||
|
}
|
||||||
|
|
||||||
|
identity, err = s.authenticate(ctx, c, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -242,6 +261,13 @@ func (s *Service) RedirectURL(ctx context.Context, client string, r *authn.Reque
|
|||||||
return redirectClient.RedirectURL(ctx, r)
|
return redirectClient.RedirectURL(ctx, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) RegisterClient(c authn.Client) {
|
||||||
|
s.clients[c.Name()] = c
|
||||||
|
if cac, ok := c.(authn.ContextAwareClient); ok {
|
||||||
|
s.clientQueue.insert(cac, cac.Priority())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func orgIDFromRequest(r *authn.Request) int64 {
|
func orgIDFromRequest(r *authn.Request) int64 {
|
||||||
if r.HTTPRequest == nil {
|
if r.HTTPRequest == nil {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -8,76 +8,106 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"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/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestService_Authenticate(t *testing.T) {
|
func TestService_Authenticate(t *testing.T) {
|
||||||
type TestCase struct {
|
type TestCase struct {
|
||||||
desc string
|
desc string
|
||||||
clientName string
|
clients []authn.Client
|
||||||
clientErr error
|
expectedIdentity *authn.Identity
|
||||||
clientIdentity *authn.Identity
|
expectedErrors []error
|
||||||
expectedOK bool
|
|
||||||
expectedErr error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientErr = errors.New("some err")
|
var (
|
||||||
|
firstErr = errors.New("first")
|
||||||
|
lastErr = errors.New("last")
|
||||||
|
)
|
||||||
|
|
||||||
tests := []TestCase{
|
tests := []TestCase{
|
||||||
{
|
{
|
||||||
desc: "should succeed with authentication for configured client",
|
desc: "should succeed with authentication for configured client",
|
||||||
clientIdentity: &authn.Identity{},
|
clients: []authn.Client{
|
||||||
clientName: "fake",
|
&authntest.FakeClient{ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: "user:1"}},
|
||||||
expectedOK: true,
|
},
|
||||||
|
expectedIdentity: &authn.Identity{ID: "user:1"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should return false when client is not configured",
|
desc: "should succeed with authentication for second client when first test fail",
|
||||||
clientName: "gitlab",
|
clients: []authn.Client{
|
||||||
expectedOK: false,
|
&authntest.FakeClient{ExpectedName: "1", ExpectedPriority: 1, ExpectedTest: false},
|
||||||
|
&authntest.FakeClient{ExpectedName: "2", ExpectedPriority: 2, ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: "user:2"}},
|
||||||
|
},
|
||||||
|
expectedIdentity: &authn.Identity{ID: "user:2"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should return true and error when client could be used but failed to authenticate",
|
desc: "should succeed with authentication for third client when error happened in first",
|
||||||
clientName: "fake",
|
clients: []authn.Client{
|
||||||
expectedOK: true,
|
&authntest.FakeClient{ExpectedName: "1", ExpectedPriority: 2, ExpectedTest: false},
|
||||||
clientErr: clientErr,
|
&authntest.FakeClient{ExpectedName: "2", ExpectedPriority: 1, ExpectedTest: true, ExpectedErr: errors.New("some error")},
|
||||||
expectedErr: clientErr,
|
&authntest.FakeClient{ExpectedName: "3", ExpectedPriority: 3, ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: "user:3"}},
|
||||||
|
},
|
||||||
|
expectedIdentity: &authn.Identity{ID: "user:3"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should return error if identity is disabled",
|
desc: "should return error when no client could authenticate the request",
|
||||||
clientName: "fake",
|
clients: []authn.Client{
|
||||||
clientIdentity: &authn.Identity{IsDisabled: true},
|
&authntest.FakeClient{ExpectedName: "1", ExpectedPriority: 2, ExpectedTest: false},
|
||||||
expectedOK: true,
|
&authntest.FakeClient{ExpectedName: "2", ExpectedPriority: 1, ExpectedTest: false},
|
||||||
expectedErr: errDisabledIdentity,
|
&authntest.FakeClient{ExpectedName: "3", ExpectedPriority: 3, ExpectedTest: false},
|
||||||
|
},
|
||||||
|
expectedErrors: []error{errCantAuthenticateReq},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return all errors in chain",
|
||||||
|
clients: []authn.Client{
|
||||||
|
&authntest.FakeClient{ExpectedName: "1", ExpectedPriority: 2, ExpectedTest: false},
|
||||||
|
&authntest.FakeClient{ExpectedName: "2", ExpectedPriority: 1, ExpectedTest: true, ExpectedErr: firstErr},
|
||||||
|
&authntest.FakeClient{ExpectedName: "3", ExpectedPriority: 3, ExpectedTest: true, ExpectedErr: lastErr},
|
||||||
|
},
|
||||||
|
expectedErrors: []error{firstErr, lastErr},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return error on disabled identity",
|
||||||
|
clients: []authn.Client{
|
||||||
|
&authntest.FakeClient{ExpectedName: "1", ExpectedTest: true, ExpectedIdentity: &authn.Identity{IsDisabled: true}},
|
||||||
|
},
|
||||||
|
expectedErrors: []error{errDisabledIdentity},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
svc := setupTests(t, func(svc *Service) {
|
svc := setupTests(t, func(svc *Service) {
|
||||||
svc.clients["fake"] = &authntest.FakeClient{
|
for _, c := range tt.clients {
|
||||||
ExpectedIdentity: tt.clientIdentity,
|
svc.RegisterClient(c)
|
||||||
ExpectedErr: tt.clientErr,
|
|
||||||
ExpectedTest: tt.expectedOK,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
_, ok, err := svc.Authenticate(context.Background(), tt.clientName, &authn.Request{})
|
identity, err := svc.Authenticate(context.Background(), &authn.Request{})
|
||||||
assert.Equal(t, tt.expectedOK, ok)
|
if len(tt.expectedErrors) == 0 {
|
||||||
assert.ErrorIs(t, err, tt.expectedErr)
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, tt.expectedIdentity, identity)
|
||||||
|
} else {
|
||||||
|
for _, e := range tt.expectedErrors {
|
||||||
|
assert.ErrorIs(t, err, e)
|
||||||
|
}
|
||||||
|
assert.Nil(t, identity)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_AuthenticateOrgID(t *testing.T) {
|
func TestService_Authenticate_OrgID(t *testing.T) {
|
||||||
type TestCase struct {
|
type TestCase struct {
|
||||||
desc string
|
desc string
|
||||||
req *authn.Request
|
req *authn.Request
|
||||||
@@ -123,18 +153,16 @@ func TestService_AuthenticateOrgID(t *testing.T) {
|
|||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
var calledWith int64
|
var calledWith int64
|
||||||
s := setupTests(t, func(svc *Service) {
|
s := setupTests(t, func(svc *Service) {
|
||||||
svc.clients["fake"] = authntest.MockClient{
|
svc.RegisterClient(authntest.MockClient{
|
||||||
AuthenticateFunc: func(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
AuthenticateFunc: func(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
calledWith = r.OrgID
|
calledWith = r.OrgID
|
||||||
return &authn.Identity{}, nil
|
return &authn.Identity{}, nil
|
||||||
},
|
},
|
||||||
TestFunc: func(ctx context.Context, r *authn.Request) bool {
|
TestFunc: func(ctx context.Context, r *authn.Request) bool { return true },
|
||||||
return true
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
_, _, _ = s.Authenticate(context.Background(), "fake", tt.req)
|
_, _ = s.Authenticate(context.Background(), tt.req)
|
||||||
assert.Equal(t, tt.expectedOrgID, calledWith)
|
assert.Equal(t, tt.expectedOrgID, calledWith)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -157,7 +185,7 @@ func TestService_Login(t *testing.T) {
|
|||||||
|
|
||||||
tests := []TestCase{
|
tests := []TestCase{
|
||||||
{
|
{
|
||||||
desc: "should authenticate and create session for valid request",
|
desc: "should login for valid request",
|
||||||
client: "fake",
|
client: "fake",
|
||||||
expectedClientOK: true,
|
expectedClientOK: true,
|
||||||
expectedClientIdentity: &authn.Identity{
|
expectedClientIdentity: &authn.Identity{
|
||||||
@@ -169,12 +197,12 @@ func TestService_Login(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should not authenticate with invalid client",
|
desc: "should not login with invalid client",
|
||||||
client: "invalid",
|
client: "invalid",
|
||||||
expectedErr: authn.ErrClientNotConfigured,
|
expectedErr: authn.ErrClientNotConfigured,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "should not authenticate non user identity",
|
desc: "should not login non user identity",
|
||||||
client: "fake",
|
client: "fake",
|
||||||
expectedClientOK: true,
|
expectedClientOK: true,
|
||||||
expectedClientIdentity: &authn.Identity{ID: "apikey:1"},
|
expectedClientIdentity: &authn.Identity{ID: "apikey:1"},
|
||||||
@@ -185,11 +213,12 @@ func TestService_Login(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
s := setupTests(t, func(svc *Service) {
|
s := setupTests(t, func(svc *Service) {
|
||||||
svc.clients["fake"] = &authntest.FakeClient{
|
svc.RegisterClient(&authntest.FakeClient{
|
||||||
|
ExpectedName: "fake",
|
||||||
ExpectedErr: tt.expectedClientErr,
|
ExpectedErr: tt.expectedClientErr,
|
||||||
ExpectedTest: tt.expectedClientOK,
|
ExpectedTest: tt.expectedClientOK,
|
||||||
ExpectedIdentity: tt.expectedClientIdentity,
|
ExpectedIdentity: tt.expectedClientIdentity,
|
||||||
}
|
})
|
||||||
svc.sessionService = &authtest.FakeUserAuthTokenService{
|
svc.sessionService = &authtest.FakeUserAuthTokenService{
|
||||||
CreateTokenProvider: func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
|
CreateTokenProvider: func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
|
||||||
if tt.expectedSessionErr != nil {
|
if tt.expectedSessionErr != nil {
|
||||||
@@ -240,10 +269,8 @@ func TestService_RedirectURL(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
service := setupTests(t, func(svc *Service) {
|
service := setupTests(t, func(svc *Service) {
|
||||||
svc.clients["redirect"] = authntest.FakeRedirectClient{
|
svc.RegisterClient(authntest.FakeRedirectClient{ExpectedName: "redirect", ExpectedURL: tt.expectedURL})
|
||||||
ExpectedURL: tt.expectedURL,
|
svc.RegisterClient(&authntest.FakeClient{ExpectedName: "non-redirect"})
|
||||||
}
|
|
||||||
svc.clients["non-redirect"] = &authntest.FakeClient{}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
u, err := service.RedirectURL(context.Background(), tt.client, nil)
|
u, err := service.RedirectURL(context.Background(), tt.client, nil)
|
||||||
@@ -265,10 +292,11 @@ func setupTests(t *testing.T, opts ...func(svc *Service)) *Service {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
s := &Service{
|
s := &Service{
|
||||||
log: log.NewNopLogger(),
|
log: log.NewNopLogger(),
|
||||||
cfg: setting.NewCfg(),
|
cfg: setting.NewCfg(),
|
||||||
clients: map[string]authn.Client{},
|
clientQueue: newQueue[authn.ContextAwareClient](),
|
||||||
tracer: tracing.InitializeTracerForTest(),
|
clients: map[string]authn.Client{},
|
||||||
|
tracer: tracing.InitializeTracerForTest(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
|
|||||||
@@ -10,14 +10,20 @@ type FakeService struct {
|
|||||||
authn.Service
|
authn.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ authn.Client = new(FakeClient)
|
var _ authn.ContextAwareClient = new(FakeClient)
|
||||||
|
|
||||||
type FakeClient struct {
|
type FakeClient struct {
|
||||||
|
ExpectedName string
|
||||||
ExpectedErr error
|
ExpectedErr error
|
||||||
ExpectedTest bool
|
ExpectedTest bool
|
||||||
|
ExpectedPriority uint
|
||||||
ExpectedIdentity *authn.Identity
|
ExpectedIdentity *authn.Identity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) Name() string {
|
||||||
|
return f.ExpectedName
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FakeClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (f *FakeClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
return f.ExpectedIdentity, f.ExpectedErr
|
return f.ExpectedIdentity, f.ExpectedErr
|
||||||
}
|
}
|
||||||
@@ -26,6 +32,10 @@ func (f *FakeClient) Test(ctx context.Context, r *authn.Request) bool {
|
|||||||
return f.ExpectedTest
|
return f.ExpectedTest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeClient) Priority() uint {
|
||||||
|
return f.ExpectedPriority
|
||||||
|
}
|
||||||
|
|
||||||
var _ authn.PasswordClient = new(FakePasswordClient)
|
var _ authn.PasswordClient = new(FakePasswordClient)
|
||||||
|
|
||||||
type FakePasswordClient struct {
|
type FakePasswordClient struct {
|
||||||
@@ -42,18 +52,18 @@ var _ authn.RedirectClient = new(FakeRedirectClient)
|
|||||||
type FakeRedirectClient struct {
|
type FakeRedirectClient struct {
|
||||||
ExpectedErr error
|
ExpectedErr error
|
||||||
ExpectedURL string
|
ExpectedURL string
|
||||||
ExpectedOK bool
|
ExpectedName string
|
||||||
ExpectedIdentity *authn.Identity
|
ExpectedIdentity *authn.Identity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f FakeRedirectClient) Name() string {
|
||||||
|
return f.ExpectedName
|
||||||
|
}
|
||||||
|
|
||||||
func (f FakeRedirectClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (f FakeRedirectClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
return f.ExpectedIdentity, f.ExpectedErr
|
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) {
|
func (f FakeRedirectClient) RedirectURL(ctx context.Context, r *authn.Request) (string, error) {
|
||||||
return f.ExpectedURL, f.ExpectedErr
|
return f.ExpectedURL, f.ExpectedErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,20 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(MockClient)
|
var _ authn.ContextAwareClient = new(MockClient)
|
||||||
|
|
||||||
type MockClient struct {
|
type MockClient struct {
|
||||||
|
NameFunc func() string
|
||||||
AuthenticateFunc func(ctx context.Context, r *authn.Request) (*authn.Identity, error)
|
AuthenticateFunc func(ctx context.Context, r *authn.Request) (*authn.Identity, error)
|
||||||
TestFunc func(ctx context.Context, r *authn.Request) bool
|
TestFunc func(ctx context.Context, r *authn.Request) bool
|
||||||
|
PriorityFunc func() uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MockClient) Name() string {
|
||||||
|
if m.NameFunc != nil {
|
||||||
|
return m.NameFunc()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MockClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (m MockClient) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
@@ -27,6 +36,13 @@ func (m MockClient) Test(ctx context.Context, r *authn.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m MockClient) Priority() uint {
|
||||||
|
if m.PriorityFunc != nil {
|
||||||
|
return m.PriorityFunc()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
var _ authn.ProxyClient = new(MockProxyClient)
|
var _ authn.ProxyClient = new(MockProxyClient)
|
||||||
|
|
||||||
type MockProxyClient struct {
|
type MockProxyClient struct {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(Anonymous)
|
var _ authn.ContextAwareClient = new(Anonymous)
|
||||||
|
|
||||||
func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service) *Anonymous {
|
func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service) *Anonymous {
|
||||||
return &Anonymous{
|
return &Anonymous{
|
||||||
@@ -25,6 +25,10 @@ type Anonymous struct {
|
|||||||
orgService org.Service
|
orgService org.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Anonymous) Name() string {
|
||||||
|
return authn.ClientAnonymous
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
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})
|
o, err := a.orgService.GetByName(ctx, &org.GetOrgByNameQuery{Name: a.cfg.AnonymousOrgName})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,6 +37,7 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn.
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &authn.Identity{
|
return &authn.Identity{
|
||||||
|
IsAnonymous: true,
|
||||||
OrgID: o.ID,
|
OrgID: o.ID,
|
||||||
OrgName: o.Name,
|
OrgName: o.Name,
|
||||||
OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)},
|
OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)},
|
||||||
@@ -44,3 +49,7 @@ func (a *Anonymous) Test(ctx context.Context, r *authn.Request) bool {
|
|||||||
// If anonymous client is register it can always be used for authentication
|
// If anonymous client is register it can always be used for authentication
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Anonymous) Priority() uint {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ var (
|
|||||||
errAPIKeyRevoked = errutil.NewBase(errutil.StatusUnauthorized, "api-key.revoked", errutil.WithPublicMessage("Revoked API key"))
|
errAPIKeyRevoked = errutil.NewBase(errutil.StatusUnauthorized, "api-key.revoked", errutil.WithPublicMessage("Revoked API key"))
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(APIKey)
|
var _ authn.ContextAwareClient = new(APIKey)
|
||||||
|
|
||||||
func ProvideAPIKey(apiKeyService apikey.Service, userService user.Service) *APIKey {
|
func ProvideAPIKey(apiKeyService apikey.Service, userService user.Service) *APIKey {
|
||||||
return &APIKey{
|
return &APIKey{
|
||||||
@@ -39,6 +39,10 @@ type APIKey struct {
|
|||||||
apiKeyService apikey.Service
|
apiKeyService apikey.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *APIKey) Name() string {
|
||||||
|
return authn.ClientAPIKey
|
||||||
|
}
|
||||||
|
|
||||||
func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
apiKey, err := s.getAPIKey(ctx, getTokenFromRequest(r))
|
apiKey, err := s.getAPIKey(ctx, getTokenFromRequest(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -133,6 +137,10 @@ func (s *APIKey) Test(ctx context.Context, r *authn.Request) bool {
|
|||||||
return looksLikeApiKey(getTokenFromRequest(r))
|
return looksLikeApiKey(getTokenFromRequest(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *APIKey) Priority() uint {
|
||||||
|
return 30
|
||||||
|
}
|
||||||
|
|
||||||
func looksLikeApiKey(token string) bool {
|
func looksLikeApiKey(token string) bool {
|
||||||
return token != ""
|
return token != ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ var (
|
|||||||
errDecodingBasicAuthHeader = errutil.NewBase(errutil.StatusBadRequest, "basic-auth.invalid-header", errutil.WithPublicMessage("Invalid Basic Auth Header"))
|
errDecodingBasicAuthHeader = errutil.NewBase(errutil.StatusBadRequest, "basic-auth.invalid-header", errutil.WithPublicMessage("Invalid Basic Auth Header"))
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(Basic)
|
var _ authn.ContextAwareClient = new(Basic)
|
||||||
|
|
||||||
func ProvideBasic(client authn.PasswordClient) *Basic {
|
func ProvideBasic(client authn.PasswordClient) *Basic {
|
||||||
return &Basic{client}
|
return &Basic{client}
|
||||||
@@ -23,6 +23,10 @@ type Basic struct {
|
|||||||
client authn.PasswordClient
|
client authn.PasswordClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Basic) Name() string {
|
||||||
|
return authn.ClientBasic
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
username, password, err := util.DecodeBasicAuthHeader(getBasicAuthHeaderFromRequest(r))
|
username, password, err := util.DecodeBasicAuthHeader(getBasicAuthHeaderFromRequest(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,6 +40,10 @@ func (c *Basic) Test(ctx context.Context, r *authn.Request) bool {
|
|||||||
return looksLikeBasicAuthRequest(r)
|
return looksLikeBasicAuthRequest(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Basic) Priority() uint {
|
||||||
|
return 40
|
||||||
|
}
|
||||||
|
|
||||||
func looksLikeBasicAuthRequest(r *authn.Request) bool {
|
func looksLikeBasicAuthRequest(r *authn.Request) bool {
|
||||||
return getBasicAuthHeaderFromRequest(r) != ""
|
return getBasicAuthHeaderFromRequest(r) != ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,16 +27,14 @@ type loginForm struct {
|
|||||||
Password string `json:"password" binding:"Required"`
|
Password string `json:"password" binding:"Required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Form) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (c *Form) Name() string {
|
||||||
|
return authn.ClientForm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Form) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
form := loginForm{}
|
form := loginForm{}
|
||||||
if err := web.Bind(r.HTTPRequest, &form); err != nil {
|
if err := web.Bind(r.HTTPRequest, &form); err != nil {
|
||||||
return nil, errBadForm.Errorf("failed to parse request: %w", err)
|
return nil, errBadForm.Errorf("failed to parse request: %w", err)
|
||||||
}
|
}
|
||||||
return f.client.AuthenticatePassword(ctx, r, form.Username, form.Password)
|
return c.client.AuthenticatePassword(ctx, r, form.Username, form.Password)
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Form) Test(ctx context.Context, r *authn.Request) bool {
|
|
||||||
// FIXME: How should we detect this??
|
|
||||||
// Maybe create client test interface and not all clients has to implement this??
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(JWT)
|
var _ authn.ContextAwareClient = new(JWT)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrJWTInvalid = errutil.NewBase(errutil.StatusUnauthorized,
|
ErrJWTInvalid = errutil.NewBase(errutil.StatusUnauthorized,
|
||||||
@@ -45,6 +45,10 @@ type JWT struct {
|
|||||||
jwtService auth.JWTVerifierService
|
jwtService auth.JWTVerifierService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *JWT) Name() string {
|
||||||
|
return authn.ClientJWT
|
||||||
|
}
|
||||||
|
|
||||||
func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
jwtToken := s.retrieveToken(r.HTTPRequest)
|
jwtToken := s.retrieveToken(r.HTTPRequest)
|
||||||
|
|
||||||
@@ -157,6 +161,10 @@ func (s *JWT) Test(ctx context.Context, r *authn.Request) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *JWT) Priority() uint {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
|
||||||
const roleGrafanaAdmin = "GrafanaAdmin"
|
const roleGrafanaAdmin = "GrafanaAdmin"
|
||||||
|
|
||||||
func (s *JWT) extractRoleAndAdmin(claims map[string]interface{}) (org.RoleType, bool) {
|
func (s *JWT) extractRoleAndAdmin(claims map[string]interface{}) (org.RoleType, bool) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ var (
|
|||||||
errInvalidProxyHeader = errutil.NewBase(errutil.StatusInternal, "auth-proxy.invalid-proxy-header")
|
errInvalidProxyHeader = errutil.NewBase(errutil.StatusInternal, "auth-proxy.invalid-proxy-header")
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(Proxy)
|
var _ authn.ContextAwareClient = new(Proxy)
|
||||||
|
|
||||||
func ProvideProxy(cfg *setting.Cfg, clients ...authn.ProxyClient) (*Proxy, error) {
|
func ProvideProxy(cfg *setting.Cfg, clients ...authn.ProxyClient) (*Proxy, error) {
|
||||||
list, err := parseAcceptList(cfg.AuthProxyWhitelist)
|
list, err := parseAcceptList(cfg.AuthProxyWhitelist)
|
||||||
@@ -45,6 +45,10 @@ type Proxy struct {
|
|||||||
acceptedIPs []*net.IPNet
|
acceptedIPs []*net.IPNet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Proxy) Name() string {
|
||||||
|
return authn.ClientProxy
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
if !c.isAllowedIP(r) {
|
if !c.isAllowedIP(r) {
|
||||||
return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list")
|
return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list")
|
||||||
@@ -75,6 +79,10 @@ func (c *Proxy) Test(ctx context.Context, r *authn.Request) bool {
|
|||||||
return len(getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)) != 0
|
return len(getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Proxy) Priority() uint {
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Proxy) isAllowedIP(r *authn.Request) bool {
|
func (c *Proxy) isAllowedIP(r *authn.Request) bool {
|
||||||
if len(c.acceptedIPs) == 0 {
|
if len(c.acceptedIPs) == 0 {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package clients
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
|
"github.com/grafana/grafana/pkg/services/login"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
@@ -18,7 +20,7 @@ const (
|
|||||||
renderCookieName = "renderKey"
|
renderCookieName = "renderKey"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(Render)
|
var _ authn.ContextAwareClient = new(Render)
|
||||||
|
|
||||||
func ProvideRender(userService user.Service, renderService rendering.Service) *Render {
|
func ProvideRender(userService user.Service, renderService rendering.Service) *Render {
|
||||||
return &Render{userService, renderService}
|
return &Render{userService, renderService}
|
||||||
@@ -29,6 +31,10 @@ type Render struct {
|
|||||||
renderService rendering.Service
|
renderService rendering.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Render) Name() string {
|
||||||
|
return authn.ClientRender
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
key := getRenderKey(r)
|
key := getRenderKey(r)
|
||||||
renderUsr, ok := c.renderService.GetRenderUser(ctx, key)
|
renderUsr, ok := c.renderService.GetRenderUser(ctx, key)
|
||||||
@@ -36,20 +42,25 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
|
|||||||
return nil, ErrInvalidRenderKey.Errorf("found no render user for key: %s", key)
|
return nil, ErrInvalidRenderKey.Errorf("found no render user for key: %s", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var identity *authn.Identity
|
||||||
if renderUsr.UserID <= 0 {
|
if renderUsr.UserID <= 0 {
|
||||||
return &authn.Identity{
|
identity = &authn.Identity{
|
||||||
ID: authn.NamespacedID(authn.NamespaceUser, 0),
|
ID: authn.NamespacedID(authn.NamespaceUser, 0),
|
||||||
OrgID: renderUsr.OrgID,
|
OrgID: renderUsr.OrgID,
|
||||||
OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)},
|
OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)},
|
||||||
ClientParams: authn.ClientParams{},
|
}
|
||||||
}, nil
|
} else {
|
||||||
|
usr, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{UserID: renderUsr.UserID, OrgID: renderUsr.OrgID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
identity = authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{})
|
||||||
}
|
}
|
||||||
|
|
||||||
usr, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{UserID: renderUsr.UserID, OrgID: renderUsr.OrgID})
|
identity.LastSeenAt = time.Now()
|
||||||
if err != nil {
|
identity.AuthModule = login.RenderModule
|
||||||
return nil, err
|
return identity, nil
|
||||||
}
|
|
||||||
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, usr.UserID), usr, authn.ClientParams{}), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Render) Test(ctx context.Context, r *authn.Request) bool {
|
func (c *Render) Test(ctx context.Context, r *authn.Request) bool {
|
||||||
@@ -59,6 +70,10 @@ func (c *Render) Test(ctx context.Context, r *authn.Request) bool {
|
|||||||
return getRenderKey(r) != ""
|
return getRenderKey(r) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Render) Priority() uint {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
func getRenderKey(r *authn.Request) string {
|
func getRenderKey(r *authn.Request) string {
|
||||||
cookie, err := r.HTTPRequest.Cookie(renderCookieName)
|
cookie, err := r.HTTPRequest.Cookie(renderCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
|
"github.com/grafana/grafana/pkg/services/login"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRender_Authenticate(t *testing.T) {
|
func TestRender_Authenticate(t *testing.T) {
|
||||||
@@ -35,9 +38,10 @@ func TestRender_Authenticate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedIdentity: &authn.Identity{
|
expectedIdentity: &authn.Identity{
|
||||||
ID: "user:0",
|
ID: "user:0",
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
|
||||||
|
AuthModule: login.RenderModule,
|
||||||
},
|
},
|
||||||
expectedRenderUsr: &rendering.RenderUser{
|
expectedRenderUsr: &rendering.RenderUser{
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
@@ -59,6 +63,7 @@ func TestRender_Authenticate(t *testing.T) {
|
|||||||
OrgName: "test",
|
OrgName: "test",
|
||||||
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||||
IsGrafanaAdmin: boolPtr(false),
|
IsGrafanaAdmin: boolPtr(false),
|
||||||
|
AuthModule: login.RenderModule,
|
||||||
},
|
},
|
||||||
expectedRenderUsr: &rendering.RenderUser{
|
expectedRenderUsr: &rendering.RenderUser{
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
@@ -97,6 +102,8 @@ func TestRender_Authenticate(t *testing.T) {
|
|||||||
assert.Nil(t, identity)
|
assert.Nil(t, identity)
|
||||||
} else {
|
} else {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
// ignore LastSeenAt
|
||||||
|
identity.LastSeenAt = time.Time{}
|
||||||
assert.EqualValues(t, *tt.expectedIdentity, *identity)
|
assert.EqualValues(t, *tt.expectedIdentity, *identity)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(Session)
|
var _ authn.ContextAwareClient = new(Session)
|
||||||
|
|
||||||
func ProvideSession(sessionService auth.UserTokenService, userService user.Service,
|
func ProvideSession(sessionService auth.UserTokenService, userService user.Service,
|
||||||
cookieName string, maxLifetime time.Duration) *Session {
|
cookieName string, maxLifetime time.Duration) *Session {
|
||||||
@@ -36,16 +36,8 @@ type Session struct {
|
|||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) Test(ctx context.Context, r *authn.Request) bool {
|
func (s *Session) Name() string {
|
||||||
if s.loginCookieName == "" {
|
return authn.ClientSession
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := r.HTTPRequest.Cookie(s.loginCookieName); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
@@ -79,6 +71,22 @@ func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id
|
|||||||
return identity, nil
|
return identity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Session) Test(ctx context.Context, r *authn.Request) bool {
|
||||||
|
if s.loginCookieName == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.HTTPRequest.Cookie(s.loginCookieName); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Priority() uint {
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) RefreshTokenHook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
|
func (s *Session) RefreshTokenHook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
|
||||||
if identity.SessionToken == nil {
|
if identity.SessionToken == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/models/roletype"
|
"github.com/grafana/grafana/pkg/models/roletype"
|
||||||
authJWT "github.com/grafana/grafana/pkg/services/auth/jwt"
|
authJWT "github.com/grafana/grafana/pkg/services/auth/jwt"
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
)
|
)
|
||||||
@@ -25,27 +23,6 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) bool {
|
func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) bool {
|
||||||
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
|
|
||||||
identity, ok, err := h.authnService.Authenticate(ctx.Req.Context(),
|
|
||||||
authn.ClientJWT,
|
|
||||||
&authn.Request{HTTPRequest: ctx.Req, Resp: ctx.Resp, OrgID: orgId})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
newCtx := WithAuthHTTPHeader(ctx.Req.Context(), h.Cfg.JWTAuthHeaderName)
|
|
||||||
*ctx.Req = *ctx.Req.WithContext(newCtx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.WriteErr(err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SignedInUser = identity.SignedInUser()
|
|
||||||
ctx.IsSignedIn = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.Cfg.JWTAuthEnabled || h.Cfg.JWTAuthHeaderName == "" {
|
if !h.Cfg.JWTAuthEnabled || h.Cfg.JWTAuthHeaderName == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,44 +131,62 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
|
|||||||
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
|
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerName = "X-Grafana-Org-Id"
|
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
|
||||||
orgID := int64(0)
|
identity, err := h.authnService.Authenticate(ctx, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp})
|
||||||
orgIDHeader := reqContext.Req.Header.Get(headerName)
|
if err != nil {
|
||||||
if orgIDHeader != "" {
|
if errors.Is(err, auth.ErrUserTokenNotFound) || errors.Is(err, auth.ErrInvalidSessionToken) {
|
||||||
id, err := strconv.ParseInt(orgIDHeader, 10, 64)
|
// Burn the cookie in case of invalid, expired or missing token
|
||||||
if err == nil {
|
reqContext.Resp.Before(h.deleteInvalidCookieEndOfRequestFunc(reqContext))
|
||||||
orgID = id
|
}
|
||||||
|
// Hack: set all errors on LookupTokenErr, so we can check it in auth middlewares
|
||||||
|
reqContext.LookupTokenErr = err
|
||||||
} else {
|
} else {
|
||||||
reqContext.Logger.Debug("Received invalid header", "header", headerName, "value", orgIDHeader)
|
reqContext.IsSignedIn = true
|
||||||
|
reqContext.UserToken = identity.SessionToken
|
||||||
|
reqContext.SignedInUser = identity.SignedInUser()
|
||||||
|
reqContext.AllowAnonymous = identity.IsAnonymous
|
||||||
|
reqContext.IsRenderCall = identity.AuthModule == login.RenderModule
|
||||||
|
// FIXME (kallep): Add auth headers used to context
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
const headerName = "X-Grafana-Org-Id"
|
||||||
queryParameters, err := url.ParseQuery(reqContext.Req.URL.RawQuery)
|
orgID := int64(0)
|
||||||
if err != nil {
|
orgIDHeader := reqContext.Req.Header.Get(headerName)
|
||||||
reqContext.Logger.Error("Failed to parse query parameters", "error", err)
|
if orgIDHeader != "" {
|
||||||
}
|
id, err := strconv.ParseInt(orgIDHeader, 10, 64)
|
||||||
if queryParameters.Has("targetOrgId") {
|
if err == nil {
|
||||||
targetOrg, err := strconv.ParseInt(queryParameters.Get("targetOrgId"), 10, 64)
|
orgID = id
|
||||||
if err == nil {
|
} else {
|
||||||
orgID = targetOrg
|
reqContext.Logger.Debug("Received invalid header", "header", headerName, "value", orgIDHeader)
|
||||||
} else {
|
}
|
||||||
reqContext.Logger.Error("Invalid target organization ID", "error", err)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// the order in which these are tested are important
|
queryParameters, err := url.ParseQuery(reqContext.Req.URL.RawQuery)
|
||||||
// look for api key in Authorization header first
|
if err != nil {
|
||||||
// then init session and look for userId in session
|
reqContext.Logger.Error("Failed to parse query parameters", "error", err)
|
||||||
// then look for api key in session (special case for render calls via api)
|
}
|
||||||
// then test if anonymous access is enabled
|
if queryParameters.Has("targetOrgId") {
|
||||||
switch {
|
targetOrg, err := strconv.ParseInt(queryParameters.Get("targetOrgId"), 10, 64)
|
||||||
case h.initContextWithRenderAuth(reqContext):
|
if err == nil {
|
||||||
case h.initContextWithJWT(reqContext, orgID):
|
orgID = targetOrg
|
||||||
case h.initContextWithAPIKey(reqContext):
|
} else {
|
||||||
case h.initContextWithBasicAuth(reqContext, orgID):
|
reqContext.Logger.Error("Invalid target organization ID", "error", err)
|
||||||
case h.initContextWithAuthProxy(reqContext, orgID):
|
}
|
||||||
case h.initContextWithToken(reqContext, orgID):
|
}
|
||||||
case h.initContextWithAnonymousUser(reqContext):
|
// the order in which these are tested are important
|
||||||
|
// look for api key in Authorization header first
|
||||||
|
// then init session and look for userId in session
|
||||||
|
// then look for api key in session (special case for render calls via api)
|
||||||
|
// then test if anonymous access is enabled
|
||||||
|
switch {
|
||||||
|
case h.initContextWithRenderAuth(reqContext):
|
||||||
|
case h.initContextWithJWT(reqContext, orgID):
|
||||||
|
case h.initContextWithAPIKey(reqContext):
|
||||||
|
case h.initContextWithBasicAuth(reqContext, orgID):
|
||||||
|
case h.initContextWithAuthProxy(reqContext, orgID):
|
||||||
|
case h.initContextWithToken(reqContext, orgID):
|
||||||
|
case h.initContextWithAnonymousUser(reqContext):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reqContext.Logger = reqContext.Logger.New("userId", reqContext.UserID, "orgId", reqContext.OrgID, "uname", reqContext.Login)
|
reqContext.Logger = reqContext.Logger.New("userId", reqContext.UserID, "orgId", reqContext.OrgID, "uname", reqContext.Login)
|
||||||
@@ -201,20 +219,9 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ContextHandler) initContextWithAnonymousUser(reqContext *models.ReqContext) bool {
|
func (h *ContextHandler) initContextWithAnonymousUser(reqContext *models.ReqContext) bool {
|
||||||
ctx, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAnonymousUser")
|
_, span := h.tracer.Start(reqContext.Req.Context(), "initContextWithAnonymousUser")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
|
|
||||||
identity, ok, err := h.authnService.Authenticate(ctx, authn.ClientAnonymous, &authn.Request{HTTPRequest: reqContext.Req})
|
|
||||||
if !ok || err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
reqContext.SignedInUser = identity.SignedInUser()
|
|
||||||
reqContext.IsSignedIn = false
|
|
||||||
reqContext.AllowAnonymous = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.Cfg.AnonymousEnabled {
|
if !h.Cfg.AnonymousEnabled {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -276,26 +283,6 @@ func (h *ContextHandler) getAPIKey(ctx context.Context, keyString string) (*apik
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bool {
|
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 {
|
|
||||||
reqContext.WriteErr(err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
reqContext.IsSignedIn = true
|
|
||||||
reqContext.SignedInUser = identity.SignedInUser()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
header := reqContext.Req.Header.Get("Authorization")
|
header := reqContext.Req.Header.Get("Authorization")
|
||||||
parts := strings.SplitN(header, " ", 2)
|
parts := strings.SplitN(header, " ", 2)
|
||||||
var keyString string
|
var keyString string
|
||||||
@@ -405,26 +392,6 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, orgID int64) bool {
|
func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext, orgID int64) bool {
|
||||||
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
|
|
||||||
identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientBasic, &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 {
|
|
||||||
reqContext.WriteErr(err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
reqContext.IsSignedIn = true
|
|
||||||
reqContext.SignedInUser = identity.SignedInUser()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.Cfg.BasicAuthEnabled {
|
if !h.Cfg.BasicAuthEnabled {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -485,29 +452,6 @@ func (h *ContextHandler) initContextWithBasicAuth(reqContext *models.ReqContext,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, orgID int64) bool {
|
func (h *ContextHandler) initContextWithToken(reqContext *models.ReqContext, orgID int64) bool {
|
||||||
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
|
|
||||||
identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(),
|
|
||||||
authn.ClientSession, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, auth.ErrUserTokenNotFound) || errors.Is(err, auth.ErrInvalidSessionToken) {
|
|
||||||
// Burn the cookie in case of invalid, expired or missing token
|
|
||||||
reqContext.Resp.Before(h.deleteInvalidCookieEndOfRequestFunc(reqContext))
|
|
||||||
}
|
|
||||||
|
|
||||||
reqContext.LookupTokenErr = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
reqContext.IsSignedIn = true
|
|
||||||
reqContext.SignedInUser = identity.SignedInUser()
|
|
||||||
reqContext.UserToken = identity.SessionToken
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Cfg.LoginCookieName == "" {
|
if h.Cfg.LoginCookieName == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -633,24 +577,6 @@ func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *models.ReqContext) w
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext) bool {
|
func (h *ContextHandler) initContextWithRenderAuth(reqContext *models.ReqContext) bool {
|
||||||
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
|
|
||||||
identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientRender, &authn.Request{HTTPRequest: reqContext.Req})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
reqContext.WriteErr(err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
reqContext.IsSignedIn = true
|
|
||||||
reqContext.IsRenderCall = true
|
|
||||||
reqContext.LastSeenAt = time.Now()
|
|
||||||
reqContext.SignedInUser = identity.SignedInUser()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
key := reqContext.GetCookie("renderKey")
|
key := reqContext.GetCookie("renderKey")
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return false
|
return false
|
||||||
@@ -717,29 +643,6 @@ func (h *ContextHandler) handleError(ctx *models.ReqContext, err error, statusCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, orgID int64) bool {
|
func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, orgID int64) bool {
|
||||||
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
|
|
||||||
identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientProxy, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp})
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
reqContext.WriteErr(err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := WithAuthHTTPHeader(reqContext.Req.Context(), h.Cfg.AuthProxyHeaderName)
|
|
||||||
for _, header := range h.Cfg.AuthProxyHeaders {
|
|
||||||
if header != "" {
|
|
||||||
ctx = WithAuthHTTPHeader(ctx, header)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
|
||||||
reqContext.IsSignedIn = true
|
|
||||||
reqContext.SignedInUser = identity.SignedInUser()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
username := reqContext.Req.Header.Get(h.Cfg.AuthProxyHeaderName)
|
username := reqContext.Req.Header.Get(h.Cfg.AuthProxyHeaderName)
|
||||||
|
|
||||||
logger := log.New("auth.proxy")
|
logger := log.New("auth.proxy")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const (
|
|||||||
LDAPAuthModule = "ldap"
|
LDAPAuthModule = "ldap"
|
||||||
AuthProxyAuthModule = "authproxy"
|
AuthProxyAuthModule = "authproxy"
|
||||||
JWTModule = "jwt"
|
JWTModule = "jwt"
|
||||||
|
RenderModule = "render"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAuthProviderLabel(authModule string) string {
|
func GetAuthProviderLabel(authModule string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user