Commit ae27c17c by Seuf Committed by seuf

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
parent 158656f5
...@@ -263,6 +263,8 @@ enabled = false ...@@ -263,6 +263,8 @@ enabled = false
header_name = X-WEBAUTH-USER header_name = X-WEBAUTH-USER
header_property = username header_property = username
auto_sign_up = true auto_sign_up = true
ldap_sync_ttl = 60
whitelist =
#################################### Auth LDAP ########################### #################################### Auth LDAP ###########################
[auth.ldap] [auth.ldap]
......
...@@ -243,6 +243,8 @@ ...@@ -243,6 +243,8 @@
;header_name = X-WEBAUTH-USER ;header_name = X-WEBAUTH-USER
;header_property = username ;header_property = username
;auto_sign_up = true ;auto_sign_up = true
;ldap_sync_ttl = 60
;whitelist = 192.168.1.1, 192.168.2.1
#################################### Basic Auth ########################## #################################### Basic Auth ##########################
[auth.basic] [auth.basic]
......
...@@ -32,9 +32,9 @@ func AuthenticateUser(query *LoginUserQuery) error { ...@@ -32,9 +32,9 @@ func AuthenticateUser(query *LoginUserQuery) error {
} }
if setting.LdapEnabled { if setting.LdapEnabled {
for _, server := range ldapCfg.Servers { for _, server := range LdapCfg.Servers {
auther := NewLdapAuthenticator(server) auther := NewLdapAuthenticator(server)
err = auther.login(query) err = auther.Login(query)
if err == nil || err != ErrInvalidCredentials { if err == nil || err != ErrInvalidCredentials {
return err return err
} }
......
...@@ -16,16 +16,34 @@ import ( ...@@ -16,16 +16,34 @@ import (
"github.com/grafana/grafana/pkg/setting" "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 { type ldapAuther struct {
server *LdapServerConf server *LdapServerConf
conn *ldap.Conn conn ILdapConn
requireSecondBind bool requireSecondBind bool
} }
func NewLdapAuthenticator(server *LdapServerConf) *ldapAuther { var NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther {
return &ldapAuther{server: server} return &ldapAuther{server: server}
} }
var ldapDial = func(network, addr string) (ILdapConn, error) {
return ldap.Dial(network, addr)
}
func (a *ldapAuther) Dial() error { func (a *ldapAuther) Dial() error {
var err error var err error
var certPool *x509.CertPool var certPool *x509.CertPool
...@@ -60,7 +78,7 @@ func (a *ldapAuther) Dial() error { ...@@ -60,7 +78,7 @@ func (a *ldapAuther) Dial() error {
a.conn, err = ldap.DialTLS("tcp", address, tlsCfg) a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
} }
} else { } else {
a.conn, err = ldap.Dial("tcp", address) a.conn, err = ldapDial("tcp", address)
} }
if err == nil { if err == nil {
...@@ -70,7 +88,7 @@ func (a *ldapAuther) Dial() error { ...@@ -70,7 +88,7 @@ func (a *ldapAuther) Dial() error {
return err return err
} }
func (a *ldapAuther) login(query *LoginUserQuery) error { func (a *ldapAuther) Login(query *LoginUserQuery) error {
if err := a.Dial(); err != nil { if err := a.Dial(); err != nil {
return err return err
} }
...@@ -85,7 +103,7 @@ func (a *ldapAuther) login(query *LoginUserQuery) error { ...@@ -85,7 +103,7 @@ func (a *ldapAuther) login(query *LoginUserQuery) error {
if ldapUser, err := a.searchForUser(query.Username); err != nil { if ldapUser, err := a.searchForUser(query.Username); err != nil {
return err return err
} else { } else {
if ldapCfg.VerboseLogging { if LdapCfg.VerboseLogging {
log.Info("Ldap User Info: %s", spew.Sdump(ldapUser)) log.Info("Ldap User Info: %s", spew.Sdump(ldapUser))
} }
...@@ -96,16 +114,11 @@ func (a *ldapAuther) login(query *LoginUserQuery) error { ...@@ -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 return err
} else { } else {
// sync user details if syncErr := a.syncInfoAndOrgRoles(grafanaUser, ldapUser); syncErr != nil {
if err := a.syncUserInfo(grafanaUser, ldapUser); err != nil { return syncErr
return err
}
// sync org roles
if err := a.syncOrgRoles(grafanaUser, ldapUser); err != nil {
return err
} }
query.User = grafanaUser query.User = grafanaUser
return nil return nil
...@@ -113,7 +126,55 @@ func (a *ldapAuther) login(query *LoginUserQuery) error { ...@@ -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 // validate that the user has access
// if there are no ldap group mappings access is true // if there are no ldap group mappings access is true
// otherwise a single group must match // otherwise a single group must match
...@@ -145,7 +206,7 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error) ...@@ -145,7 +206,7 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error)
return userQuery.Result, nil 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{ cmd := m.CreateUserCommand{
Login: ldapUser.Username, Login: ldapUser.Username,
Email: ldapUser.Email, Email: ldapUser.Email,
...@@ -159,7 +220,7 @@ func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error) ...@@ -159,7 +220,7 @@ func (a *ldapAuther) createGrafanaUser(ldapUser *ldapUserInfo) (*m.User, error)
return &cmd.Result, nil 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) var name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
if user.Email == ldapUser.Email && user.Name == name { if user.Email == ldapUser.Email && user.Name == name {
return nil return nil
...@@ -174,7 +235,7 @@ func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *ldapUserInfo) error { ...@@ -174,7 +235,7 @@ func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *ldapUserInfo) error {
return bus.Dispatch(&updateCmd) 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 { if len(a.server.LdapGroups) == 0 {
log.Warn("Ldap: no group mappings defined") log.Warn("Ldap: no group mappings defined")
return nil return nil
...@@ -244,9 +305,27 @@ func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error { ...@@ -244,9 +305,27 @@ func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
return nil 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 err := a.conn.Bind(ldapUser.DN, userPassword); err != nil {
if ldapCfg.VerboseLogging { if LdapCfg.VerboseLogging {
log.Info("LDAP second bind failed, %v", err) log.Info("LDAP second bind failed, %v", err)
} }
...@@ -273,7 +352,7 @@ func (a *ldapAuther) initialBind(username, userPassword string) error { ...@@ -273,7 +352,7 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
} }
if err := a.conn.Bind(bindPath, userPassword); err != nil { if err := a.conn.Bind(bindPath, userPassword); err != nil {
if ldapCfg.VerboseLogging { if LdapCfg.VerboseLogging {
log.Info("LDAP initial bind failed, %v", err) log.Info("LDAP initial bind failed, %v", err)
} }
...@@ -288,7 +367,7 @@ func (a *ldapAuther) initialBind(username, userPassword string) error { ...@@ -288,7 +367,7 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
return nil return nil
} }
func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) { func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
var searchResult *ldap.SearchResult var searchResult *ldap.SearchResult
var err error var err error
...@@ -339,7 +418,7 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, 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) 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) log.Info("LDAP: Searching for user's groups: %s", filter)
} }
...@@ -368,7 +447,7 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) { ...@@ -368,7 +447,7 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
} }
} }
return &ldapUserInfo{ return &LdapUserInfo{
DN: searchResult.Entries[0].DN, DN: searchResult.Entries[0].DN,
LastName: getLdapAttr(a.server.Attr.Surname, searchResult), LastName: getLdapAttr(a.server.Attr.Surname, searchResult),
FirstName: getLdapAttr(a.server.Attr.Name, searchResult), FirstName: getLdapAttr(a.server.Attr.Name, searchResult),
......
...@@ -3,6 +3,7 @@ package login ...@@ -3,6 +3,7 @@ package login
import ( import (
"testing" "testing"
"github.com/go-ldap/ldap"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
...@@ -16,7 +17,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -16,7 +17,7 @@ func TestLdapAuther(t *testing.T) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{ ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{{}}, LdapGroups: []*LdapGroupToOrgRole{{}},
}) })
_, err := ldapAuther.getGrafanaUserFor(&ldapUserInfo{}) _, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
So(err, ShouldEqual, ErrInvalidCredentials) So(err, ShouldEqual, ErrInvalidCredentials)
}) })
...@@ -32,7 +33,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -32,7 +33,7 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(user1) sc.userQueryReturns(user1)
result, err := ldapAuther.getGrafanaUserFor(&ldapUserInfo{}) result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(result, ShouldEqual, user1) So(result, ShouldEqual, user1)
}) })
...@@ -46,7 +47,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -46,7 +47,7 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(user1) 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(err, ShouldBeNil)
So(result, ShouldEqual, user1) So(result, ShouldEqual, user1)
}) })
...@@ -62,7 +63,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -62,7 +63,7 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(nil) sc.userQueryReturns(nil)
result, err := ldapAuther.getGrafanaUserFor(&ldapUserInfo{ result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{
Username: "torkelo", Username: "torkelo",
Email: "my@email.com", Email: "my@email.com",
MemberOf: []string{"cn=editor"}, MemberOf: []string{"cn=editor"},
...@@ -93,7 +94,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -93,7 +94,7 @@ func TestLdapAuther(t *testing.T) {
}) })
sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{ err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
MemberOf: []string{"cn=users"}, MemberOf: []string{"cn=users"},
}) })
...@@ -112,7 +113,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -112,7 +113,7 @@ func TestLdapAuther(t *testing.T) {
}) })
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}}) 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"}, MemberOf: []string{"cn=users"},
}) })
...@@ -131,7 +132,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -131,7 +132,7 @@ func TestLdapAuther(t *testing.T) {
}) })
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}}) 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"}, MemberOf: []string{"cn=other"},
}) })
...@@ -150,7 +151,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -150,7 +151,7 @@ func TestLdapAuther(t *testing.T) {
}) })
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}}) 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"}, MemberOf: []string{"cn=users"},
}) })
...@@ -170,7 +171,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -170,7 +171,7 @@ func TestLdapAuther(t *testing.T) {
}) })
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}}) 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"}, MemberOf: []string{"cn=admins"},
}) })
...@@ -189,7 +190,7 @@ func TestLdapAuther(t *testing.T) { ...@@ -189,7 +190,7 @@ func TestLdapAuther(t *testing.T) {
}) })
sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{ err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
MemberOf: []string{"cn=admins"}, MemberOf: []string{"cn=admins"},
}) })
...@@ -200,6 +201,91 @@ func TestLdapAuther(t *testing.T) { ...@@ -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) { func ldapAutherScenario(desc string, fn scenarioFunc) {
...@@ -229,6 +315,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) { ...@@ -229,6 +315,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
return nil return nil
}) })
bus.AddHandler("test", func(cmd *m.UpdateUserCommand) error {
sc.updateUserCmd = cmd
return nil
})
fn(sc) fn(sc)
}) })
} }
...@@ -238,6 +329,7 @@ type scenarioContext struct { ...@@ -238,6 +329,7 @@ type scenarioContext struct {
addOrgUserCmd *m.AddOrgUserCommand addOrgUserCmd *m.AddOrgUserCommand
updateOrgUserCmd *m.UpdateOrgUserCommand updateOrgUserCmd *m.UpdateOrgUserCommand
removeOrgUserCmd *m.RemoveOrgUserCommand removeOrgUserCmd *m.RemoveOrgUserCommand
updateUserCmd *m.UpdateUserCommand
} }
func (sc *scenarioContext) userQueryReturns(user *m.User) { func (sc *scenarioContext) userQueryReturns(user *m.User) {
......
package login package login
type ldapUserInfo struct { type LdapUserInfo struct {
DN string DN string
FirstName string FirstName string
LastName string LastName string
...@@ -9,7 +9,7 @@ type ldapUserInfo struct { ...@@ -9,7 +9,7 @@ type ldapUserInfo struct {
MemberOf []string MemberOf []string
} }
func (u *ldapUserInfo) isMemberOf(group string) bool { func (u *LdapUserInfo) isMemberOf(group string) bool {
if group == "*" { if group == "*" {
return true return true
} }
......
...@@ -50,7 +50,7 @@ type LdapGroupToOrgRole struct { ...@@ -50,7 +50,7 @@ type LdapGroupToOrgRole struct {
OrgRole m.RoleType `toml:"org_role"` OrgRole m.RoleType `toml:"org_role"`
} }
var ldapCfg LdapConfig var LdapCfg LdapConfig
var ldapLogger log.Logger = log.New("ldap") var ldapLogger log.Logger = log.New("ldap")
func loadLdapConfig() { func loadLdapConfig() {
...@@ -60,19 +60,19 @@ func loadLdapConfig() { ...@@ -60,19 +60,19 @@ func loadLdapConfig() {
ldapLogger.Info("Ldap enabled, reading config file", "file", setting.LdapConfigFile) 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 { if err != nil {
ldapLogger.Crit("Failed to load ldap config file", "error", err) ldapLogger.Crit("Failed to load ldap config file", "error", err)
os.Exit(1) 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") ldapLogger.Crit("ldap enabled but no ldap servers defined in config file")
os.Exit(1) os.Exit(1)
} }
// set default org id // set default org id
for _, server := range ldapCfg.Servers { for _, server := range LdapCfg.Servers {
assertNotEmptyCfg(server.SearchFilter, "search_filter") assertNotEmptyCfg(server.SearchFilter, "search_filter")
assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns") assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns")
......
package middleware package middleware
import ( import (
"errors"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
...@@ -17,6 +23,12 @@ func initContextWithAuthProxy(ctx *Context) bool { ...@@ -17,6 +23,12 @@ func initContextWithAuthProxy(ctx *Context) bool {
return false 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) query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
if err := bus.Dispatch(query); err != nil { if err := bus.Dispatch(query); err != nil {
if err != m.ErrUserNotFound { if err != m.ErrUserNotFound {
...@@ -26,6 +38,10 @@ func initContextWithAuthProxy(ctx *Context) bool { ...@@ -26,6 +38,10 @@ func initContextWithAuthProxy(ctx *Context) bool {
if setting.AuthProxyAutoSignUp { if setting.AuthProxyAutoSignUp {
cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue) cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
if setting.LdapEnabled {
cmd.SkipOrgSetup = true
}
if err := bus.Dispatch(cmd); err != nil { if err := bus.Dispatch(cmd); err != nil {
ctx.Handle(500, "Failed to create user specified in auth proxy header", err) ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
return true return true
...@@ -46,6 +62,30 @@ func initContextWithAuthProxy(ctx *Context) bool { ...@@ -46,6 +62,30 @@ func initContextWithAuthProxy(ctx *Context) bool {
return false 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.SignedInUser = query.Result
ctx.IsSignedIn = true ctx.IsSignedIn = true
ctx.Session.Set(SESS_KEY_USERID, ctx.UserId) ctx.Session.Set(SESS_KEY_USERID, ctx.UserId)
...@@ -53,6 +93,56 @@ func initContextWithAuthProxy(ctx *Context) bool { ...@@ -53,6 +93,56 @@ func initContextWithAuthProxy(ctx *Context) bool {
return true 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 { func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
query := m.GetSignedInUserQuery{} query := m.GetSignedInUserQuery{}
if setting.AuthProxyHeaderProperty == "username" { if setting.AuthProxyHeaderProperty == "username" {
......
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) { ...@@ -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 ( ...@@ -12,8 +12,10 @@ import (
) )
const ( const (
SESS_KEY_USERID = "uid" SESS_KEY_USERID = "uid"
SESS_KEY_OAUTH_STATE = "state" 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 var sessionManager *session.Manager
......
...@@ -108,6 +108,8 @@ var ( ...@@ -108,6 +108,8 @@ var (
AuthProxyHeaderName string AuthProxyHeaderName string
AuthProxyHeaderProperty string AuthProxyHeaderProperty string
AuthProxyAutoSignUp bool AuthProxyAutoSignUp bool
AuthProxyLdapSyncTtl int
AuthProxyWhitelist string
// Basic Auth // Basic Auth
BasicAuthEnabled bool BasicAuthEnabled bool
...@@ -537,7 +539,10 @@ func NewConfigContext(args *CommandLineArgs) error { ...@@ -537,7 +539,10 @@ func NewConfigContext(args *CommandLineArgs) error {
AuthProxyHeaderName = authProxy.Key("header_name").String() AuthProxyHeaderName = authProxy.Key("header_name").String()
AuthProxyHeaderProperty = authProxy.Key("header_property").String() AuthProxyHeaderProperty = authProxy.Key("header_property").String()
AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) 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") authBasic := Cfg.Section("auth.basic")
BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
......
<!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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment