From ae27c17c6868812ee947097a1407cdb1b1b7c384 Mon Sep 17 00:00:00 2001 From: Seuf Date: Tue, 23 Feb 2016 14:22:28 +0100 Subject: [PATCH 1/2] 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 --- conf/defaults.ini | 2 + conf/sample.ini | 2 + pkg/login/auth.go | 4 +- pkg/login/ldap.go | 125 ++++++++++++++++++++++++------ pkg/login/ldap_test.go | 112 +++++++++++++++++++++++--- pkg/login/ldap_user.go | 4 +- pkg/login/settings.go | 8 +- pkg/middleware/auth_proxy.go | 90 +++++++++++++++++++++ pkg/middleware/auth_proxy_test.go | 123 +++++++++++++++++++++++++++++ pkg/middleware/middleware_test.go | 93 ++++++++++++++++++++++ pkg/middleware/session.go | 4 +- pkg/setting/setting.go | 5 ++ public/views/407.html | 27 +++++++ 13 files changed, 557 insertions(+), 42 deletions(-) create mode 100644 pkg/middleware/auth_proxy_test.go create mode 100644 public/views/407.html diff --git a/conf/defaults.ini b/conf/defaults.ini index 7ead103d027..a8b5997ca71 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -263,6 +263,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 e8c9c990a63..60e3336a14a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -243,6 +243,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..b25e7d6356c 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -3,6 +3,7 @@ package login import ( "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 +17,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 +33,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 +47,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 +63,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 +94,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 +113,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 +132,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 +151,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 +171,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 +190,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 +201,91 @@ 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 ldapAutherScenario(desc string, fn scenarioFunc) { @@ -229,6 +315,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 +329,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..a8b203862ce --- /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 f8e4aa374e8..50cce7f3ed9 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -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) + }) + }) }) } diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index d575189f4de..cecdeb79dc4 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -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 diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 061068e9df1..2b8c840e894 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 @@ -537,7 +539,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 @@ + + + + + + + + Grafana + + + + + + +
+
+ + Proxy authentication required + +
+ +
+

Proxy authenticaion required

+
+
+ + From 12a82bc0d4380942857355e70a0ae1a067fd37f1 Mon Sep 17 00:00:00 2001 From: Seuf Date: Mon, 12 Dec 2016 09:52:56 +0100 Subject: [PATCH 2/2] 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 --- CHANGELOG.md | 1 + pkg/login/ldap_test.go | 5 +++++ pkg/middleware/auth_proxy_test.go | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6a4a0ad57..2d8403132d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443) ### Enhancements +* **Login**: Allow role and organisation mapping with ldap after Proxy auth. [#6895](https://github.com/grafana/grafana/pull/6895) * **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655) * **Victorops**: Add VictorOps Notification Integration [#6411](https://github.com/grafana/grafana/issues/6411) * **Singlestat**: New aggregation on singlestat panel [#6740](https://github.com/grafana/grafana/pull/6740) diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index b25e7d6356c..22297c3ad11 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -1,6 +1,7 @@ package login import ( + "crypto/tls" "testing" "github.com/go-ldap/ldap" @@ -288,6 +289,10 @@ func (c *mockLdapConn) Search(*ldap.SearchRequest) (*ldap.SearchResult, error) { return c.result, nil } +func (c *mockLdapConn) StartTLS(*tls.Config) error { + return nil +} + func ldapAutherScenario(desc string, fn scenarioFunc) { Convey(desc, func() { defer bus.ClearBusHandlers() diff --git a/pkg/middleware/auth_proxy_test.go b/pkg/middleware/auth_proxy_test.go index a8b203862ce..cc9253b6a77 100644 --- a/pkg/middleware/auth_proxy_test.go +++ b/pkg/middleware/auth_proxy_test.go @@ -17,7 +17,7 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) { setting.AuthProxyLdapSyncTtl = 60 servers := []*login.LdapServerConf{{Host: "127.0.0.1"}} - login.ldapCfg = login.LdapConfig{Servers: servers} + login.LdapCfg = login.LdapConfig{Servers: servers} mockLdapAuther := mockLdapAuthenticator{} login.NewLdapAuthenticator = func(server *login.LdapServerConf) login.ILdapAuther {