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:
Seuf 2016-02-23 14:22:28 +01:00 committed by seuf
parent 158656f570
commit ae27c17c68
13 changed files with 557 additions and 42 deletions

View File

@ -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]

View File

@ -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]

View File

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

View File

@ -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),

View File

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

View File

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

View File

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

View File

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

View 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
}

View File

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

View File

@ -14,6 +14,8 @@ import (
const (
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

View File

@ -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
View 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>