Browse Source

Merge branch 'authproxy_ldap' of https://github.com/seuf/grafana into seuf-authproxy_ldap

Torkel Ödegaard 9 years ago
parent
commit
6e27db148c

+ 1 - 1
CHANGELOG.md

@@ -13,7 +13,7 @@
 * **CLI**: Make it possible to reset the admin password using the grafana-cli. [#5479](https://github.com/grafana/grafana/issues/5479)
 * **Influxdb**: Support multiple tags in InfluxDB annotations. [#4550](https://github.com/grafana/grafana/pull/4550), thx [@adrianlzt](https://github.com/adrianlzt)
 * **LDAP**:  Basic Auth now supports LDAP username and password, [#6940](https://github.com/grafana/grafana/pull/6940), thx [@utkarshcmu](https://github.com/utkarshcmu)
-
+* **LDAP**: Now works with Auth Proxy, role and organisation mapping & sync will regularly be performed. [#6895](https://github.com/grafana/grafana/pull/6895), thx [@Seuf](https://github.com/seuf)
 
 ### Bugfixes
 * **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)

+ 2 - 0
conf/defaults.ini

@@ -264,6 +264,8 @@ enabled = false
 header_name = X-WEBAUTH-USER
 header_property = username
 auto_sign_up = true
+ldap_sync_ttl = 60
+whitelist =
 
 #################################### Auth LDAP ###########################
 [auth.ldap]

+ 2 - 0
conf/sample.ini

@@ -244,6 +244,8 @@
 ;header_name = X-WEBAUTH-USER
 ;header_property = username
 ;auto_sign_up = true
+;ldap_sync_ttl = 60
+;whitelist = 192.168.1.1, 192.168.2.1
 
 #################################### Basic Auth ##########################
 [auth.basic]

+ 2 - 2
pkg/login/auth.go

@@ -32,9 +32,9 @@ func AuthenticateUser(query *LoginUserQuery) error {
 	}
 
 	if setting.LdapEnabled {
-		for _, server := range ldapCfg.Servers {
+		for _, server := range LdapCfg.Servers {
 			auther := NewLdapAuthenticator(server)
-			err = auther.login(query)
+			err = auther.Login(query)
 			if err == nil || err != ErrInvalidCredentials {
 				return err
 			}

+ 102 - 23
pkg/login/ldap.go

@@ -16,16 +16,34 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 )
 
+type ILdapConn interface {
+	Bind(username, password string) error
+	Search(*ldap.SearchRequest) (*ldap.SearchResult, error)
+	StartTLS(*tls.Config) error
+	Close()
+}
+
+type ILdapAuther interface {
+	Login(query *LoginUserQuery) error
+	SyncSignedInUser(signedInUser *m.SignedInUser) error
+	GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error)
+	SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error
+}
+
 type ldapAuther struct {
 	server            *LdapServerConf
-	conn              *ldap.Conn
+	conn              ILdapConn
 	requireSecondBind bool
 }
 
-func NewLdapAuthenticator(server *LdapServerConf) *ldapAuther {
+var NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther {
 	return &ldapAuther{server: server}
 }
 
+var ldapDial = func(network, addr string) (ILdapConn, error) {
+	return ldap.Dial(network, addr)
+}
+
 func (a *ldapAuther) Dial() error {
 	var err error
 	var certPool *x509.CertPool
@@ -60,7 +78,7 @@ func (a *ldapAuther) Dial() error {
 				a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
 			}
 		} else {
-			a.conn, err = ldap.Dial("tcp", address)
+			a.conn, err = ldapDial("tcp", address)
 		}
 
 		if err == nil {
@@ -70,7 +88,7 @@ func (a *ldapAuther) Dial() error {
 	return err
 }
 
-func (a *ldapAuther) login(query *LoginUserQuery) error {
+func (a *ldapAuther) Login(query *LoginUserQuery) error {
 	if err := a.Dial(); err != nil {
 		return err
 	}
@@ -85,7 +103,7 @@ func (a *ldapAuther) login(query *LoginUserQuery) error {
 	if ldapUser, err := a.searchForUser(query.Username); err != nil {
 		return err
 	} else {
-		if ldapCfg.VerboseLogging {
+		if LdapCfg.VerboseLogging {
 			log.Info("Ldap User Info: %s", spew.Sdump(ldapUser))
 		}
 
@@ -96,16 +114,11 @@ func (a *ldapAuther) login(query *LoginUserQuery) error {
 			}
 		}
 
-		if grafanaUser, err := a.getGrafanaUserFor(ldapUser); err != nil {
+		if grafanaUser, err := a.GetGrafanaUserFor(ldapUser); err != nil {
 			return err
 		} else {
-			// sync user details
-			if err := a.syncUserInfo(grafanaUser, ldapUser); err != nil {
-				return err
-			}
-			// sync org roles
-			if err := a.syncOrgRoles(grafanaUser, ldapUser); err != nil {
-				return err
+			if syncErr := a.syncInfoAndOrgRoles(grafanaUser, ldapUser); syncErr != nil {
+				return syncErr
 			}
 			query.User = grafanaUser
 			return nil
@@ -113,7 +126,55 @@ func (a *ldapAuther) login(query *LoginUserQuery) error {
 	}
 }
 
-func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error) {
+func (a *ldapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
+	grafanaUser := m.User{
+		Id:    signedInUser.UserId,
+		Login: signedInUser.Login,
+		Email: signedInUser.Email,
+		Name:  signedInUser.Name,
+	}
+
+	if err := a.Dial(); err != nil {
+		return err
+	}
+
+	defer a.conn.Close()
+	if err := a.serverBind(); err != nil {
+		return err
+	}
+
+	if ldapUser, err := a.searchForUser(signedInUser.Login); err != nil {
+		log.Info("ERROR while searching for user in ldap %#v", err)
+
+		return err
+	} else {
+		if err := a.syncInfoAndOrgRoles(&grafanaUser, ldapUser); err != nil {
+			return err
+		}
+
+		if LdapCfg.VerboseLogging {
+			log.Info("Ldap User Info: %s", spew.Sdump(ldapUser))
+		}
+	}
+
+	return nil
+}
+
+// Sync info for ldap user and grafana user
+func (a *ldapAuther) syncInfoAndOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
+	// sync user details
+	if err := a.syncUserInfo(user, ldapUser); err != nil {
+		return err
+	}
+	// sync org roles
+	if err := a.SyncOrgRoles(user, ldapUser); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
 	// validate that the user has access
 	// if there are no ldap group mappings access is true
 	// otherwise a single group must match
@@ -145,7 +206,7 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error)
 	return userQuery.Result, nil
 
 }
-func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error) {
+func (a *ldapAuther) createGrafanaUser(ldapUser *LdapUserInfo) (*m.User, error) {
 	cmd := m.CreateUserCommand{
 		Login: ldapUser.Username,
 		Email: ldapUser.Email,
@@ -159,7 +220,7 @@ func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error)
 	return &cmd.Result, nil
 }
 
-func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *ldapUserInfo) error {
+func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *LdapUserInfo) error {
 	var name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
 	if user.Email == ldapUser.Email && user.Name == name {
 		return nil
@@ -174,7 +235,7 @@ func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *ldapUserInfo) error {
 	return bus.Dispatch(&updateCmd)
 }
 
-func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
+func (a *ldapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
 	if len(a.server.LdapGroups) == 0 {
 		log.Warn("Ldap: no group mappings defined")
 		return nil
@@ -244,9 +305,27 @@ func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
 	return nil
 }
 
-func (a *ldapAuther) secondBind(ldapUser *ldapUserInfo, userPassword string) error {
+func (a *ldapAuther) serverBind() error {
+	// bind_dn and bind_password to bind
+	if err := a.conn.Bind(a.server.BindDN, a.server.BindPassword); err != nil {
+		if LdapCfg.VerboseLogging {
+			log.Info("LDAP initial bind failed, %v", err)
+		}
+
+		if ldapErr, ok := err.(*ldap.Error); ok {
+			if ldapErr.ResultCode == 49 {
+				return ErrInvalidCredentials
+			}
+		}
+		return err
+	}
+
+	return nil
+}
+
+func (a *ldapAuther) secondBind(ldapUser *LdapUserInfo, userPassword string) error {
 	if err := a.conn.Bind(ldapUser.DN, userPassword); err != nil {
-		if ldapCfg.VerboseLogging {
+		if LdapCfg.VerboseLogging {
 			log.Info("LDAP second bind failed, %v", err)
 		}
 
@@ -273,7 +352,7 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
 	}
 
 	if err := a.conn.Bind(bindPath, userPassword); err != nil {
-		if ldapCfg.VerboseLogging {
+		if LdapCfg.VerboseLogging {
 			log.Info("LDAP initial bind failed, %v", err)
 		}
 
@@ -288,7 +367,7 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
 	return nil
 }
 
-func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
+func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
 	var searchResult *ldap.SearchResult
 	var err error
 
@@ -339,7 +418,7 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
 			}
 			filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
 
-			if ldapCfg.VerboseLogging {
+			if LdapCfg.VerboseLogging {
 				log.Info("LDAP: Searching for user's groups: %s", filter)
 			}
 
@@ -368,7 +447,7 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
 		}
 	}
 
-	return &ldapUserInfo{
+	return &LdapUserInfo{
 		DN:        searchResult.Entries[0].DN,
 		LastName:  getLdapAttr(a.server.Attr.Surname, searchResult),
 		FirstName: getLdapAttr(a.server.Attr.Name, searchResult),

+ 107 - 10
pkg/login/ldap_test.go

@@ -1,8 +1,10 @@
 package login
 
 import (
+	"crypto/tls"
 	"testing"
 
+	"github.com/go-ldap/ldap"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
@@ -16,7 +18,7 @@ func TestLdapAuther(t *testing.T) {
 			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
 				LdapGroups: []*LdapGroupToOrgRole{{}},
 			})
-			_, err := ldapAuther.getGrafanaUserFor(&ldapUserInfo{})
+			_, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
 
 			So(err, ShouldEqual, ErrInvalidCredentials)
 		})
@@ -32,7 +34,7 @@ func TestLdapAuther(t *testing.T) {
 
 			sc.userQueryReturns(user1)
 
-			result, err := ldapAuther.getGrafanaUserFor(&ldapUserInfo{})
+			result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
 			So(err, ShouldBeNil)
 			So(result, ShouldEqual, user1)
 		})
@@ -46,7 +48,7 @@ func TestLdapAuther(t *testing.T) {
 
 			sc.userQueryReturns(user1)
 
-			result, err := ldapAuther.getGrafanaUserFor(&ldapUserInfo{MemberOf: []string{"cn=users"}})
+			result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{MemberOf: []string{"cn=users"}})
 			So(err, ShouldBeNil)
 			So(result, ShouldEqual, user1)
 		})
@@ -62,7 +64,7 @@ func TestLdapAuther(t *testing.T) {
 
 			sc.userQueryReturns(nil)
 
-			result, err := ldapAuther.getGrafanaUserFor(&ldapUserInfo{
+			result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{
 				Username: "torkelo",
 				Email:    "my@email.com",
 				MemberOf: []string{"cn=editor"},
@@ -93,7 +95,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
-			err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{
+			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
 				MemberOf: []string{"cn=users"},
 			})
 
@@ -112,7 +114,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
-			err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{
+			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
 				MemberOf: []string{"cn=users"},
 			})
 
@@ -131,7 +133,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
-			err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{
+			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
 				MemberOf: []string{"cn=other"},
 			})
 
@@ -150,7 +152,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
-			err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{
+			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
 				MemberOf: []string{"cn=users"},
 			})
 
@@ -170,7 +172,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
-			err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{
+			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
 				MemberOf: []string{"cn=admins"},
 			})
 
@@ -189,7 +191,7 @@ func TestLdapAuther(t *testing.T) {
 			})
 
 			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
-			err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{
+			err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
 				MemberOf: []string{"cn=admins"},
 			})
 
@@ -200,6 +202,95 @@ func TestLdapAuther(t *testing.T) {
 		})
 
 	})
+
+	Convey("When calling SyncSignedInUser", t, func() {
+
+		mockLdapConnection := &mockLdapConn{}
+		ldapAuther := NewLdapAuthenticator(
+			&LdapServerConf{
+				Host:       "",
+				RootCACert: "",
+				LdapGroups: []*LdapGroupToOrgRole{
+					{GroupDN: "*", OrgRole: "Admin"},
+				},
+				Attr: LdapAttributeMap{
+					Username: "username",
+					Surname:  "surname",
+					Email:    "email",
+					Name:     "name",
+					MemberOf: "memberof",
+				},
+				SearchBaseDNs: []string{"BaseDNHere"},
+			},
+		)
+
+		dialCalled := false
+		ldapDial = func(network, addr string) (ILdapConn, error) {
+			dialCalled = true
+			return mockLdapConnection, nil
+		}
+
+		entry := ldap.Entry{
+			DN: "dn", Attributes: []*ldap.EntryAttribute{
+				{Name: "username", Values: []string{"roelgerrits"}},
+				{Name: "surname", Values: []string{"Gerrits"}},
+				{Name: "email", Values: []string{"roel@test.com"}},
+				{Name: "name", Values: []string{"Roel"}},
+				{Name: "memberof", Values: []string{"admins"}},
+			}}
+		result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+		mockLdapConnection.setSearchResult(&result)
+
+		ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
+			// arrange
+			signedInUser := &m.SignedInUser{
+				Email:  "roel@test.net",
+				UserId: 1,
+				Name:   "Roel Gerrits",
+				Login:  "roelgerrits",
+			}
+
+			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
+
+			// act
+			syncErrResult := ldapAuther.SyncSignedInUser(signedInUser)
+
+			// assert
+			So(dialCalled, ShouldBeTrue)
+			So(syncErrResult, ShouldBeNil)
+			// User should be searched in ldap
+			So(mockLdapConnection.searchCalled, ShouldBeTrue)
+			// Info should be updated (email differs)
+			So(sc.updateUserCmd.Email, ShouldEqual, "roel@test.com")
+			// User should have admin privileges
+			So(sc.addOrgUserCmd.UserId, ShouldEqual, 1)
+			So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
+		})
+	})
+}
+
+type mockLdapConn struct {
+	result       *ldap.SearchResult
+	searchCalled bool
+}
+
+func (c *mockLdapConn) Bind(username, password string) error {
+	return nil
+}
+
+func (c *mockLdapConn) Close() {}
+
+func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) {
+	c.result = result
+}
+
+func (c *mockLdapConn) Search(*ldap.SearchRequest) (*ldap.SearchResult, error) {
+	c.searchCalled = true
+	return c.result, nil
+}
+
+func (c *mockLdapConn) StartTLS(*tls.Config) error {
+    return nil
 }
 
 func ldapAutherScenario(desc string, fn scenarioFunc) {
@@ -229,6 +320,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 			return nil
 		})
 
+		bus.AddHandler("test", func(cmd *m.UpdateUserCommand) error {
+			sc.updateUserCmd = cmd
+			return nil
+		})
+
 		fn(sc)
 	})
 }
@@ -238,6 +334,7 @@ type scenarioContext struct {
 	addOrgUserCmd    *m.AddOrgUserCommand
 	updateOrgUserCmd *m.UpdateOrgUserCommand
 	removeOrgUserCmd *m.RemoveOrgUserCommand
+	updateUserCmd    *m.UpdateUserCommand
 }
 
 func (sc *scenarioContext) userQueryReturns(user *m.User) {

+ 2 - 2
pkg/login/ldap_user.go

@@ -1,6 +1,6 @@
 package login
 
-type ldapUserInfo struct {
+type LdapUserInfo struct {
 	DN        string
 	FirstName string
 	LastName  string
@@ -9,7 +9,7 @@ type ldapUserInfo struct {
 	MemberOf  []string
 }
 
-func (u *ldapUserInfo) isMemberOf(group string) bool {
+func (u *LdapUserInfo) isMemberOf(group string) bool {
 	if group == "*" {
 		return true
 	}

+ 4 - 4
pkg/login/settings.go

@@ -50,7 +50,7 @@ type LdapGroupToOrgRole struct {
 	OrgRole m.RoleType `toml:"org_role"`
 }
 
-var ldapCfg LdapConfig
+var LdapCfg LdapConfig
 var ldapLogger log.Logger = log.New("ldap")
 
 func loadLdapConfig() {
@@ -60,19 +60,19 @@ func loadLdapConfig() {
 
 	ldapLogger.Info("Ldap enabled, reading config file", "file", setting.LdapConfigFile)
 
-	_, err := toml.DecodeFile(setting.LdapConfigFile, &ldapCfg)
+	_, err := toml.DecodeFile(setting.LdapConfigFile, &LdapCfg)
 	if err != nil {
 		ldapLogger.Crit("Failed to load ldap config file", "error", err)
 		os.Exit(1)
 	}
 
-	if len(ldapCfg.Servers) == 0 {
+	if len(LdapCfg.Servers) == 0 {
 		ldapLogger.Crit("ldap enabled but no ldap servers defined in config file")
 		os.Exit(1)
 	}
 
 	// set default org id
-	for _, server := range ldapCfg.Servers {
+	for _, server := range LdapCfg.Servers {
 		assertNotEmptyCfg(server.SearchFilter, "search_filter")
 		assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns")
 

+ 90 - 0
pkg/middleware/auth_proxy.go

@@ -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" {

+ 123 - 0
pkg/middleware/auth_proxy_test.go

@@ -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
+}

+ 93 - 0
pkg/middleware/middleware_test.go

@@ -213,6 +213,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)
+			})
+		})
 	})
 }
 

+ 4 - 2
pkg/middleware/session.go

@@ -12,8 +12,10 @@ import (
 )
 
 const (
-	SESS_KEY_USERID      = "uid"
-	SESS_KEY_OAUTH_STATE = "state"
+	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

+ 5 - 0
pkg/setting/setting.go

@@ -108,6 +108,8 @@ var (
 	AuthProxyHeaderName     string
 	AuthProxyHeaderProperty string
 	AuthProxyAutoSignUp     bool
+	AuthProxyLdapSyncTtl    int
+	AuthProxyWhitelist      string
 
 	// Basic Auth
 	BasicAuthEnabled bool
@@ -546,7 +548,10 @@ func NewConfigContext(args *CommandLineArgs) error {
 	AuthProxyHeaderName = authProxy.Key("header_name").String()
 	AuthProxyHeaderProperty = authProxy.Key("header_property").String()
 	AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true)
+	AuthProxyLdapSyncTtl = authProxy.Key("ldap_sync_ttl").MustInt()
+	AuthProxyWhitelist = authProxy.Key("whitelist").String()
 
+	// basic auth
 	authBasic := Cfg.Section("auth.basic")
 	BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
 

+ 27 - 0
public/views/407.html

@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width">
+
+    <title>Grafana</title>
+
+    <link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.dark.min.css" title="Dark">
+    <link rel="icon" type="image/png" href="[[.AppSubUrl]]/img/fav32.png">
+  </head>
+
+	<body>
+		<div class="gf-box" style="margin: 200px auto 0 auto; width: 500px;">
+			<div class="gf-box-header">
+				<span class="gf-box-title">
+					Proxy authentication required
+				</span>
+			</div>
+
+			<div class="gf-box-body">
+				<h4>Proxy authenticaion required</h4>
+			</div>
+		</div>
+  </body>
+</html>