mirror of
https://github.com/grafana/grafana.git
synced 2026-07-29 15:59:50 -05:00
Auth Proxy improvements
- adds the option to use ldap groups for authorization in combination with an auth proxy - adds an option to limit where auth proxy requests come from by configure a list of ip's - fixes a security issue, session could be reused
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@@ -17,6 +23,12 @@ func initContextWithAuthProxy(ctx *Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// if auth proxy ip(s) defined, check if request comes from one of those
|
||||
if err := checkAuthenticationProxy(ctx, proxyHeaderValue); err != nil {
|
||||
ctx.Handle(407, "Proxy authentication required", err)
|
||||
return true
|
||||
}
|
||||
|
||||
query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
if err != m.ErrUserNotFound {
|
||||
@@ -26,6 +38,10 @@ func initContextWithAuthProxy(ctx *Context) bool {
|
||||
|
||||
if setting.AuthProxyAutoSignUp {
|
||||
cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
|
||||
if setting.LdapEnabled {
|
||||
cmd.SkipOrgSetup = true
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
|
||||
return true
|
||||
@@ -46,6 +62,30 @@ func initContextWithAuthProxy(ctx *Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Make sure that we cannot share a session between different users!
|
||||
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
|
||||
// remove session
|
||||
if err := ctx.Session.Destory(ctx); err != nil {
|
||||
log.Error(3, "Failed to destory session, err")
|
||||
}
|
||||
|
||||
// initialize a new session
|
||||
if err := ctx.Session.Start(ctx); err != nil {
|
||||
log.Error(3, "Failed to start session", err)
|
||||
}
|
||||
}
|
||||
|
||||
// When ldap is enabled, sync userinfo and org roles
|
||||
if err := syncGrafanaUserWithLdapUser(ctx, query); err != nil {
|
||||
if err == login.ErrInvalidCredentials {
|
||||
ctx.Handle(500, "Unable to authenticate user", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Handle(500, "Failed to sync user", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.SignedInUser = query.Result
|
||||
ctx.IsSignedIn = true
|
||||
ctx.Session.Set(SESS_KEY_USERID, ctx.UserId)
|
||||
@@ -53,6 +93,56 @@ func initContextWithAuthProxy(ctx *Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQuery) error {
|
||||
if setting.LdapEnabled {
|
||||
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
|
||||
|
||||
var lastLdapSync int64
|
||||
if lastLdapSyncInSession := ctx.Session.Get(SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
|
||||
lastLdapSync = lastLdapSyncInSession.(int64)
|
||||
}
|
||||
|
||||
if lastLdapSync < expireEpoch {
|
||||
ldapCfg := login.LdapCfg
|
||||
|
||||
for _, server := range ldapCfg.Servers {
|
||||
auther := login.NewLdapAuthenticator(server)
|
||||
if err := auther.SyncSignedInUser(query.Result); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Session.Set(SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAuthenticationProxy(ctx *Context, proxyHeaderValue string) error {
|
||||
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) > 0 {
|
||||
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
|
||||
remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")
|
||||
sourceIP := remoteAddrSplit[0]
|
||||
|
||||
found := false
|
||||
for _, proxyIP := range proxies {
|
||||
if sourceIP == strings.TrimSpace(proxyIP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
msg := fmt.Sprintf("Request for user (%s) is not from the authentication proxy", proxyHeaderValue)
|
||||
err := errors.New(msg)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
|
||||
query := m.GetSignedInUserQuery{}
|
||||
if setting.AuthProxyHeaderProperty == "username" {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAuthProxyWithLdapEnabled(t *testing.T) {
|
||||
Convey("When calling sync grafana user with ldap user", t, func() {
|
||||
|
||||
setting.LdapEnabled = true
|
||||
setting.AuthProxyLdapSyncTtl = 60
|
||||
|
||||
servers := []*login.LdapServerConf{{Host: "127.0.0.1"}}
|
||||
login.ldapCfg = login.LdapConfig{Servers: servers}
|
||||
mockLdapAuther := mockLdapAuthenticator{}
|
||||
|
||||
login.NewLdapAuthenticator = func(server *login.LdapServerConf) login.ILdapAuther {
|
||||
return &mockLdapAuther
|
||||
}
|
||||
|
||||
signedInUser := m.SignedInUser{}
|
||||
query := m.GetSignedInUserQuery{Result: &signedInUser}
|
||||
|
||||
Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() {
|
||||
// arrange
|
||||
session := mockSession{}
|
||||
ctx := Context{Session: &session}
|
||||
So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
|
||||
|
||||
// act
|
||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
||||
|
||||
// assert
|
||||
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
|
||||
So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
Convey("When session variable not expired, don't sync and don't change session var", func() {
|
||||
// arrange
|
||||
session := mockSession{}
|
||||
ctx := Context{Session: &session}
|
||||
now := time.Now().Unix()
|
||||
session.Set(SESS_KEY_LASTLDAPSYNC, now)
|
||||
|
||||
// act
|
||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
||||
|
||||
// assert
|
||||
So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldEqual, now)
|
||||
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("When lastldapsync is expired, session variable should be updated", func() {
|
||||
// arrange
|
||||
session := mockSession{}
|
||||
ctx := Context{Session: &session}
|
||||
expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix()
|
||||
session.Set(SESS_KEY_LASTLDAPSYNC, expiredTime)
|
||||
|
||||
// act
|
||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
||||
|
||||
// assert
|
||||
So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
|
||||
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type mockSession struct {
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (s *mockSession) Start(c *Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockSession) Set(k interface{}, v interface{}) error {
|
||||
s.value = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockSession) Get(k interface{}) interface{} {
|
||||
return s.value
|
||||
}
|
||||
|
||||
func (s *mockSession) ID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *mockSession) Release() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockSession) Destory(c *Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockLdapAuthenticator struct {
|
||||
syncSignedInUserCalled bool
|
||||
}
|
||||
|
||||
func (a *mockLdapAuthenticator) Login(query *login.LoginUserQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuthenticator) SyncSignedInUser(signedInUser *m.SignedInUser) error {
|
||||
a.syncSignedInUserCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuthenticator) GetGrafanaUserFor(ldapUser *login.LdapUserInfo) (*m.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (a *mockLdapAuthenticator) SyncOrgRoles(user *m.User, ldapUser *login.LdapUserInfo) error {
|
||||
return nil
|
||||
}
|
||||
@@ -208,6 +208,99 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("When auth_proxy is enabled and request RemoteAddr is not trusted", func(sc *scenarioContext) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyWhitelist = "192.168.1.1, 192.168.2.1"
|
||||
|
||||
sc.fakeReq("GET", "/")
|
||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||
sc.req.RemoteAddr = "192.168.3.1:12345"
|
||||
sc.exec()
|
||||
|
||||
Convey("should return 407 status code", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 407)
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("When auth_proxy is enabled and request RemoteAddr is trusted", func(sc *scenarioContext) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyWhitelist = "192.168.1.1, 192.168.2.1"
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReq("GET", "/")
|
||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||
sc.req.RemoteAddr = "192.168.2.1:12345"
|
||||
sc.exec()
|
||||
|
||||
Convey("Should init context with user info", func() {
|
||||
So(sc.context.IsSignedIn, ShouldBeTrue)
|
||||
So(sc.context.UserId, ShouldEqual, 33)
|
||||
So(sc.context.OrgId, ShouldEqual, 4)
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyWhitelist = ""
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
|
||||
return nil
|
||||
})
|
||||
|
||||
// create session
|
||||
sc.fakeReq("GET", "/").handler(func(c *Context) {
|
||||
c.Session.Set(SESS_KEY_USERID, int64(33))
|
||||
}).exec()
|
||||
|
||||
oldSessionID := sc.context.Session.ID()
|
||||
|
||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||
sc.exec()
|
||||
|
||||
newSessionID := sc.context.Session.ID()
|
||||
|
||||
Convey("Should not share session with other user", func() {
|
||||
So(oldSessionID, ShouldNotEqual, newSessionID)
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("When auth_proxy and ldap enabled call sync with ldap user", func(sc *scenarioContext) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyWhitelist = ""
|
||||
setting.LdapEnabled = true
|
||||
|
||||
called := false
|
||||
syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQuery) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReq("GET", "/")
|
||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||
sc.exec()
|
||||
|
||||
Convey("Should call syncGrafanaUserWithLdapUser", func() {
|
||||
So(called, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SESS_KEY_USERID = "uid"
|
||||
SESS_KEY_USERID = "uid"
|
||||
SESS_KEY_OAUTH_STATE = "state"
|
||||
SESS_KEY_APIKEY = "apikey_id" // used for render requests with api keys
|
||||
SESS_KEY_LASTLDAPSYNC = "last_ldap_sync"
|
||||
)
|
||||
|
||||
var sessionManager *session.Manager
|
||||
|
||||
Reference in New Issue
Block a user