diff --git a/CHANGELOG.md b/CHANGELOG.md index d852a7514e4..a33102ef8b0 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/conf/defaults.ini b/conf/defaults.ini index bf0e567cc8f..9b2b1c89827 100644 --- a/conf/defaults.ini +++ b/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] diff --git a/conf/sample.ini b/conf/sample.ini index 43eba5b62cd..adce6f9fac2 100644 --- a/conf/sample.ini +++ b/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] diff --git a/pkg/login/auth.go b/pkg/login/auth.go index f2034476f81..a71837e25bc 100644 --- a/pkg/login/auth.go +++ b/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 } diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 97a34f129b1..97aaea8908e 100644 --- a/pkg/login/ldap.go +++ b/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), diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index 2e4ed3a32e1..22297c3ad11 100644 --- a/pkg/login/ldap_test.go +++ b/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) { diff --git a/pkg/login/ldap_user.go b/pkg/login/ldap_user.go index 5a57efe2687..9f1cf3c96b6 100644 --- a/pkg/login/ldap_user.go +++ b/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 } diff --git a/pkg/login/settings.go b/pkg/login/settings.go index e0713302a6d..5d4d740548d 100644 --- a/pkg/login/settings.go +++ b/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") diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index 648c3319305..e02e31f9152 100644 --- a/pkg/middleware/auth_proxy.go +++ b/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" { diff --git a/pkg/middleware/auth_proxy_test.go b/pkg/middleware/auth_proxy_test.go new file mode 100644 index 00000000000..cc9253b6a77 --- /dev/null +++ b/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 +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 5db4e59d29e..5cffa0a2884 100644 --- a/pkg/middleware/middleware_test.go +++ b/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) + }) + }) }) } diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 96a18e76a67..0700612ecc2 100644 --- a/pkg/middleware/session.go +++ b/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 diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index a23b714a783..0de7b87b164 100644 --- a/pkg/setting/setting.go +++ b/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) diff --git a/public/views/407.html b/public/views/407.html new file mode 100644 index 00000000000..67105dcf5e5 --- /dev/null +++ b/public/views/407.html @@ -0,0 +1,27 @@ + + +
+ + + + +