Feature: LDAP refactoring (#16950)
* incapsulates multipleldap logic under one module * abstracts users upsert and get logic * changes some of the text error messages and import sort sequence * heavily refactors the LDAP module – LDAP module now only deals with LDAP related behaviour * integrates affected auth_proxy module and their tests * refactoring of the auth_proxy logic
This commit is contained in:
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -21,10 +23,14 @@ const (
|
||||
CachePrefix = "auth-proxy-sync-ttl:%s"
|
||||
)
|
||||
|
||||
var (
|
||||
getLDAPConfig = ldap.GetConfig
|
||||
isLDAPEnabled = ldap.IsEnabled
|
||||
)
|
||||
// getLDAPConfig gets LDAP config
|
||||
var getLDAPConfig = ldap.GetConfig
|
||||
|
||||
// isLDAPEnabled checks if LDAP is enabled
|
||||
var isLDAPEnabled = ldap.IsEnabled
|
||||
|
||||
// newLDAP creates multiple LDAP instance
|
||||
var newLDAP = multildap.New
|
||||
|
||||
// AuthProxy struct
|
||||
type AuthProxy struct {
|
||||
@@ -33,13 +39,13 @@ type AuthProxy struct {
|
||||
orgID int64
|
||||
header string
|
||||
|
||||
LDAP func(server *ldap.ServerConfig) ldap.IAuth
|
||||
|
||||
enabled bool
|
||||
whitelistIP string
|
||||
headerType string
|
||||
headers map[string]string
|
||||
cacheTTL int
|
||||
enabled bool
|
||||
LdapAllowSignup bool
|
||||
AuthProxyAutoSignUp bool
|
||||
whitelistIP string
|
||||
headerType string
|
||||
headers map[string]string
|
||||
cacheTTL int
|
||||
}
|
||||
|
||||
// Error auth proxy specific error
|
||||
@@ -78,13 +84,13 @@ func New(options *Options) *AuthProxy {
|
||||
orgID: options.OrgID,
|
||||
header: header,
|
||||
|
||||
LDAP: ldap.New,
|
||||
|
||||
enabled: setting.AuthProxyEnabled,
|
||||
headerType: setting.AuthProxyHeaderProperty,
|
||||
headers: setting.AuthProxyHeaders,
|
||||
whitelistIP: setting.AuthProxyWhitelist,
|
||||
cacheTTL: setting.AuthProxyLdapSyncTtl,
|
||||
enabled: setting.AuthProxyEnabled,
|
||||
headerType: setting.AuthProxyHeaderProperty,
|
||||
headers: setting.AuthProxyHeaders,
|
||||
whitelistIP: setting.AuthProxyWhitelist,
|
||||
cacheTTL: setting.AuthProxyLdapSyncTtl,
|
||||
LdapAllowSignup: setting.LdapAllowSignup,
|
||||
AuthProxyAutoSignUp: setting.AuthProxyAutoSignUp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,34 +150,22 @@ func (auth *AuthProxy) IsAllowedIP() (bool, *Error) {
|
||||
return false, newError("Proxy authentication required", err)
|
||||
}
|
||||
|
||||
// InCache checks if we have user in cache
|
||||
func (auth *AuthProxy) InCache() bool {
|
||||
userID, _ := auth.GetUserIDViaCache()
|
||||
|
||||
if userID == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// getKey forms a key for the cache
|
||||
func (auth *AuthProxy) getKey() string {
|
||||
return fmt.Sprintf(CachePrefix, auth.header)
|
||||
}
|
||||
|
||||
// GetUserID gets user id with whatever means possible
|
||||
func (auth *AuthProxy) GetUserID() (int64, *Error) {
|
||||
if auth.InCache() {
|
||||
// Login logs in user id with whatever means possible
|
||||
func (auth *AuthProxy) Login() (int64, *Error) {
|
||||
|
||||
id, _ := auth.GetUserViaCache()
|
||||
if id != 0 {
|
||||
// Error here means absent cache - we don't need to handle that
|
||||
id, _ := auth.GetUserIDViaCache()
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
if isLDAPEnabled() {
|
||||
id, err := auth.GetUserIDViaLDAP()
|
||||
id, err := auth.LoginViaLDAP()
|
||||
|
||||
if err == ldap.ErrInvalidCredentials {
|
||||
return 0, newError(
|
||||
@@ -181,16 +175,16 @@ func (auth *AuthProxy) GetUserID() (int64, *Error) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, newError("Failed to sync user", err)
|
||||
return 0, newError("Failed to get the user", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
id, err := auth.GetUserIDViaHeader()
|
||||
id, err := auth.LoginViaHeader()
|
||||
if err != nil {
|
||||
return 0, newError(
|
||||
"Failed to login as user specified in auth proxy header",
|
||||
"Failed to log in as user, specified in auth proxy header",
|
||||
err,
|
||||
)
|
||||
}
|
||||
@@ -198,8 +192,8 @@ func (auth *AuthProxy) GetUserID() (int64, *Error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetUserIDViaCache gets the user from cache
|
||||
func (auth *AuthProxy) GetUserIDViaCache() (int64, error) {
|
||||
// GetUserViaCache gets user id from cache
|
||||
func (auth *AuthProxy) GetUserViaCache() (int64, error) {
|
||||
var (
|
||||
cacheKey = auth.getKey()
|
||||
userID, err = auth.store.Get(cacheKey)
|
||||
@@ -212,33 +206,34 @@ func (auth *AuthProxy) GetUserIDViaCache() (int64, error) {
|
||||
return userID.(int64), nil
|
||||
}
|
||||
|
||||
// GetUserIDViaLDAP gets user via LDAP request
|
||||
func (auth *AuthProxy) GetUserIDViaLDAP() (int64, *Error) {
|
||||
query := &models.LoginUserQuery{
|
||||
ReqContext: auth.ctx,
|
||||
Username: auth.header,
|
||||
}
|
||||
|
||||
// LoginViaLDAP logs in user via LDAP request
|
||||
func (auth *AuthProxy) LoginViaLDAP() (int64, *Error) {
|
||||
config, err := getLDAPConfig()
|
||||
if err != nil {
|
||||
return 0, newError("Failed to get LDAP config", nil)
|
||||
}
|
||||
if len(config.Servers) == 0 {
|
||||
return 0, newError("No LDAP servers available", nil)
|
||||
|
||||
extUser, err := newLDAP(config.Servers).User(auth.header)
|
||||
if err != nil {
|
||||
return 0, newError(err.Error(), nil)
|
||||
}
|
||||
|
||||
for _, server := range config.Servers {
|
||||
author := auth.LDAP(server)
|
||||
if err := author.SyncUser(query); err != nil {
|
||||
return 0, newError(err.Error(), nil)
|
||||
}
|
||||
// Have to sync grafana and LDAP user during log in
|
||||
user, err := user.Upsert(&user.UpsertArgs{
|
||||
ReqContext: auth.ctx,
|
||||
SignupAllowed: auth.LdapAllowSignup,
|
||||
ExternalUser: extUser,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, newError(err.Error(), nil)
|
||||
}
|
||||
|
||||
return query.User.Id, nil
|
||||
return user.Id, nil
|
||||
}
|
||||
|
||||
// GetUserIDViaHeader gets user from the header only
|
||||
func (auth *AuthProxy) GetUserIDViaHeader() (int64, error) {
|
||||
// LoginViaHeader logs in user from the header only
|
||||
// TODO: refactor - cyclomatic complexity should be much lower
|
||||
func (auth *AuthProxy) LoginViaHeader() (int64, error) {
|
||||
extUser := &models.ExternalUserInfo{
|
||||
AuthModule: "authproxy",
|
||||
AuthId: auth.header,
|
||||
@@ -269,18 +264,16 @@ func (auth *AuthProxy) GetUserIDViaHeader() (int64, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// add/update user in grafana
|
||||
cmd := &models.UpsertUserCommand{
|
||||
result, err := user.Upsert(&user.UpsertArgs{
|
||||
ReqContext: auth.ctx,
|
||||
SignupAllowed: true,
|
||||
ExternalUser: extUser,
|
||||
SignupAllowed: setting.AuthProxyAutoSignUp,
|
||||
}
|
||||
err := bus.Dispatch(cmd)
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return cmd.Result.Id, nil
|
||||
return result.Id, nil
|
||||
}
|
||||
|
||||
// GetSignedUser get full signed user info
|
||||
@@ -298,21 +291,18 @@ func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, *Error
|
||||
}
|
||||
|
||||
// Remember user in cache
|
||||
func (auth *AuthProxy) Remember() *Error {
|
||||
func (auth *AuthProxy) Remember(id int64) *Error {
|
||||
key := auth.getKey()
|
||||
|
||||
// Make sure we do not rewrite the expiration time
|
||||
if auth.InCache() {
|
||||
// Check if user already in cache
|
||||
userID, _ := auth.store.Get(key)
|
||||
if userID != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
key = auth.getKey()
|
||||
value, _ = auth.GetUserIDViaCache()
|
||||
expiration = time.Duration(-auth.cacheTTL) * time.Minute
|
||||
|
||||
err = auth.store.Set(key, value, expiration)
|
||||
)
|
||||
expiration := time.Duration(-auth.cacheTTL) * time.Minute
|
||||
|
||||
err := auth.store.Set(key, id, expiration)
|
||||
if err != nil {
|
||||
return newError(err.Error(), nil)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package authproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
@@ -8,24 +9,40 @@ import (
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/multildap"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type TestLDAP struct {
|
||||
ldap.Auth
|
||||
ID int64
|
||||
syncCalled bool
|
||||
type TestMultiLDAP struct {
|
||||
multildap.MultiLDAP
|
||||
ID int64
|
||||
userCalled bool
|
||||
loginCalled bool
|
||||
}
|
||||
|
||||
func (stub *TestLDAP) SyncUser(query *models.LoginUserQuery) error {
|
||||
stub.syncCalled = true
|
||||
query.User = &models.User{
|
||||
Id: stub.ID,
|
||||
func (stub *TestMultiLDAP) Login(query *models.LoginUserQuery) (
|
||||
*models.ExternalUserInfo, error,
|
||||
) {
|
||||
stub.loginCalled = true
|
||||
result := &models.ExternalUserInfo{
|
||||
UserId: stub.ID,
|
||||
}
|
||||
return nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (stub *TestMultiLDAP) User(login string) (
|
||||
*models.ExternalUserInfo,
|
||||
error,
|
||||
) {
|
||||
stub.userCalled = true
|
||||
result := &models.ExternalUserInfo{
|
||||
UserId: stub.ID,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func TestMiddlewareContext(t *testing.T) {
|
||||
@@ -44,7 +61,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
Convey("gets data from the cache", func() {
|
||||
Convey("logs in user from the cache", func() {
|
||||
store := remotecache.NewFakeStore(t)
|
||||
key := fmt.Sprintf(CachePrefix, name)
|
||||
store.Set(key, int64(33), 0)
|
||||
@@ -55,53 +72,64 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
OrgID: 4,
|
||||
})
|
||||
|
||||
id, err := auth.GetUserID()
|
||||
id, err := auth.Login()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(id, ShouldEqual, 33)
|
||||
})
|
||||
|
||||
Convey("LDAP", func() {
|
||||
Convey("gets data from the LDAP", func() {
|
||||
Convey("logs in via LDAP", func() {
|
||||
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
||||
cmd.Result = &models.User{
|
||||
Id: 42,
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
isLDAPEnabled = func() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
stub := &TestMultiLDAP{
|
||||
ID: 42,
|
||||
}
|
||||
|
||||
getLDAPConfig = func() (*ldap.Config, error) {
|
||||
config := &ldap.Config{
|
||||
Servers: []*ldap.ServerConfig{
|
||||
{},
|
||||
{
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
},
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return stub
|
||||
}
|
||||
|
||||
defer func() {
|
||||
newLDAP = multildap.New
|
||||
isLDAPEnabled = ldap.IsEnabled
|
||||
getLDAPConfig = ldap.GetConfig
|
||||
}()
|
||||
|
||||
store := remotecache.NewFakeStore(t)
|
||||
|
||||
auth := New(&Options{
|
||||
server := New(&Options{
|
||||
Store: store,
|
||||
Ctx: ctx,
|
||||
OrgID: 4,
|
||||
})
|
||||
|
||||
stub := &TestLDAP{
|
||||
ID: 42,
|
||||
}
|
||||
|
||||
auth.LDAP = func(server *ldap.ServerConfig) ldap.IAuth {
|
||||
return stub
|
||||
}
|
||||
|
||||
id, err := auth.GetUserID()
|
||||
id, err := server.Login()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(id, ShouldEqual, 42)
|
||||
So(stub.syncCalled, ShouldEqual, true)
|
||||
So(stub.userCalled, ShouldEqual, true)
|
||||
})
|
||||
|
||||
Convey("gets nice error if ldap is enabled but not configured", func() {
|
||||
@@ -110,13 +138,11 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
}
|
||||
|
||||
getLDAPConfig = func() (*ldap.Config, error) {
|
||||
config := &ldap.Config{
|
||||
Servers: []*ldap.ServerConfig{},
|
||||
}
|
||||
return config, nil
|
||||
return nil, errors.New("Something went wrong")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
newLDAP = multildap.New
|
||||
isLDAPEnabled = ldap.IsEnabled
|
||||
getLDAPConfig = ldap.GetConfig
|
||||
}()
|
||||
@@ -129,20 +155,20 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
OrgID: 4,
|
||||
})
|
||||
|
||||
stub := &TestLDAP{
|
||||
stub := &TestMultiLDAP{
|
||||
ID: 42,
|
||||
}
|
||||
|
||||
auth.LDAP = func(server *ldap.ServerConfig) ldap.IAuth {
|
||||
newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return stub
|
||||
}
|
||||
|
||||
id, err := auth.GetUserID()
|
||||
id, err := auth.Login()
|
||||
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "Failed to sync user")
|
||||
So(err.Error(), ShouldContainSubstring, "Failed to get the user")
|
||||
So(id, ShouldNotEqual, 42)
|
||||
So(stub.syncCalled, ShouldEqual, false)
|
||||
So(stub.loginCalled, ShouldEqual, false)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user