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