Просмотр исходного кода

feat(ldap): work on reading ldap config from toml file, #1450

Torkel Ödegaard 10 лет назад
Родитель
Сommit
0b5ba55131
14 измененных файлов с 167 добавлено и 100 удалено
  1. 1 1
      .bra.toml
  2. 6 1
      build.go
  3. 2 16
      conf/defaults.ini
  4. 31 0
      conf/ldap.toml
  5. 5 0
      conf/sample.ini
  6. 2 0
      main.go
  7. 3 3
      pkg/api/login.go
  8. 0 27
      pkg/auth/settings.go
  9. 7 6
      pkg/login/auth.go
  10. 21 43
      pkg/login/ldap.go
  11. 20 1
      pkg/login/ldap_test.go
  12. 1 1
      pkg/login/ldap_user.go
  13. 65 0
      pkg/login/settings.go
  14. 3 1
      pkg/setting/setting.go

+ 1 - 1
.bra.toml

@@ -9,7 +9,7 @@ watch_dirs = [
 	"$WORKDIR/public/views",
 	"$WORKDIR/conf",
 ]
-watch_exts = [".go", ".ini"]
+watch_exts = [".go", "conf/*"]
 build_delay = 1500
 cmds = [
   ["go", "build", "-o", "./bin/grafana-server"],

+ 6 - 1
build.go

@@ -128,6 +128,7 @@ type linuxPackageOptions struct {
 	binPath                string
 	configDir              string
 	configFilePath         string
+	ldapFilePath           string
 	etcDefaultPath         string
 	etcDefaultFilePath     string
 	initdScriptFilePath    string
@@ -148,6 +149,7 @@ func createLinuxPackages() {
 		binPath:                "/usr/sbin/grafana-server",
 		configDir:              "/etc/grafana",
 		configFilePath:         "/etc/grafana/grafana.ini",
+		ldapFilePath:           "/etc/grafana/ldap.toml",
 		etcDefaultPath:         "/etc/default",
 		etcDefaultFilePath:     "/etc/default/grafana-server",
 		initdScriptFilePath:    "/etc/init.d/grafana-server",
@@ -167,6 +169,7 @@ func createLinuxPackages() {
 		binPath:                "/usr/sbin/grafana-server",
 		configDir:              "/etc/grafana",
 		configFilePath:         "/etc/grafana/grafana.ini",
+		ldapFilePath:           "/etc/grafana/ldap.toml",
 		etcDefaultPath:         "/etc/sysconfig",
 		etcDefaultFilePath:     "/etc/sysconfig/grafana-server",
 		initdScriptFilePath:    "/etc/init.d/grafana-server",
@@ -204,8 +207,10 @@ func createPackage(options linuxPackageOptions) {
 	runPrint("cp", "-a", filepath.Join(workingDir, "tmp")+"/.", filepath.Join(packageRoot, options.homeDir))
 	// remove bin path
 	runPrint("rm", "-rf", filepath.Join(packageRoot, options.homeDir, "bin"))
-	// copy sample ini file to /etc/opt/grafana
+	// copy sample ini file to /etc/grafana
 	runPrint("cp", "conf/sample.ini", filepath.Join(packageRoot, options.configFilePath))
+	// copy sample ldap toml config file to /etc/grafana/ldap.toml
+	runPrint("cp", "conf/sample.ini", filepath.Join(packageRoot, ldapFilePath))
 
 	args := []string{
 		"-s", "dir",

+ 2 - 16
conf/defaults.ini

@@ -181,22 +181,8 @@ auto_sign_up = true
 
 #################################### Auth LDAP ##########################
 [auth.ldap]
-enabled = true
-hosts = ldap://127.0.0.1:389
-use_ssl = false
-bind_path = cn=%s,dc=grafana,dc=org
-bind_password =
-search_bases = dc=grafana,dc=org
-search_filter = (cn=%s)
-attr_username = cn
-attr_name = givenName
-attr_surname = sn
-attr_email = email
-attr_member_of = memberOf
-
-[auth.ldap.member.to.role.map]
--: cn=admins,dc=grafana,dc=org -> "Admin" in "Main Org."
--: cn=users,dc=grafana,dc=org  -> "Viewer" in "Main Org."
+enabled = false
+config_file = /etc/grafana/ldap.toml
 
 #################################### SMTP / Emailing ##########################
 [smtp]

+ 31 - 0
conf/ldap.toml

@@ -0,0 +1,31 @@
+verbose_logging = true
+
+[[servers]]
+host = "127.0.0.1"
+port = 389
+use_ssl = false
+
+bind_dn = "cn=admin,dc=grafana,dc=org"
+bind_password = "grafana"
+
+search_filter = "(cn=%s)"
+search_base_dns = ["dc=grafana,dc=org"]
+
+[servers.attributes]
+name = "givenName"
+surname = "sn"
+username = "cn"
+member_of = "memberOf"
+email =  "email"
+
+[[servers.group_mappings]]
+group_dn = "cn=admins,dc=grafana,dc=org"
+org_role = "Admin"
+
+[[server.ldap_group_to_org_role_mappings]]
+group_dn = "cn=users,dc=grafana,dc=org"
+org_role = "Editor"
+
+[[servers.group_mappings]]
+group_dn = "*"
+org_role = "Viewer"

+ 5 - 0
conf/sample.ini

@@ -178,6 +178,11 @@
 [auth.basic]
 ;enabled = true
 
+#################################### Auth LDAP ##########################
+[auth.ldap]
+enabled = false
+config_file = /etc/grafana/ldap.toml
+
 #################################### SMTP / Emailing ##########################
 [smtp]
 ;enabled = false

+ 2 - 0
main.go

@@ -12,6 +12,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/cmd"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/services/eventpublisher"
@@ -54,6 +55,7 @@ func main() {
 	initRuntime()
 
 	search.Init()
+	login.Init()
 	social.NewOAuthService()
 	eventpublisher.Init()
 	plugins.Init()

+ 3 - 3
pkg/api/login.go

@@ -4,9 +4,9 @@ import (
 	"net/url"
 
 	"github.com/grafana/grafana/pkg/api/dtos"
-	"github.com/grafana/grafana/pkg/auth"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
@@ -88,13 +88,13 @@ func LoginApiPing(c *middleware.Context) {
 }
 
 func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response {
-	authQuery := auth.AuthenticateUserQuery{
+	authQuery := login.LoginUserQuery{
 		Username: cmd.User,
 		Password: cmd.Password,
 	}
 
 	if err := bus.Dispatch(&authQuery); err != nil {
-		if err == auth.ErrInvalidCredentials {
+		if err == login.ErrInvalidCredentials {
 			return ApiError(401, "Invalid username or password", err)
 		}
 

+ 0 - 27
pkg/auth/settings.go

@@ -1,27 +0,0 @@
-package auth
-
-import m "github.com/grafana/grafana/pkg/models"
-
-type LdapGroupToOrgRole struct {
-	GroupDN string
-	OrgId   int64
-	OrgRole m.RoleType
-}
-
-type LdapServerConf struct {
-	Host         string
-	Port         string
-	UseSSL       bool
-	BindDN       string
-	BindPassword string
-	AttrUsername string
-	AttrName     string
-	AttrSurname  string
-	AttrEmail    string
-	AttrMemberOf string
-
-	SearchFilter  string
-	SearchBaseDNs []string
-
-	LdapGroups []*LdapGroupToOrgRole
-}

+ 7 - 6
pkg/auth/auth.go → pkg/login/auth.go

@@ -1,4 +1,4 @@
-package auth
+package login
 
 import (
 	"errors"
@@ -13,24 +13,25 @@ var (
 	ErrInvalidCredentials = errors.New("Invalid Username or Password")
 )
 
-type AuthenticateUserQuery struct {
+type LoginUserQuery struct {
 	Username string
 	Password string
 	User     *m.User
 }
 
-func init() {
+func Init() {
 	bus.AddHandler("auth", AuthenticateUser)
+	loadLdapConfig()
 }
 
-func AuthenticateUser(query *AuthenticateUserQuery) error {
+func AuthenticateUser(query *LoginUserQuery) error {
 	err := loginUsingGrafanaDB(query)
 	if err == nil || err != ErrInvalidCredentials {
 		return err
 	}
 
 	if setting.LdapEnabled {
-		for _, server := range ldapServers {
+		for _, server := range ldapCfg.Servers {
 			auther := NewLdapAuthenticator(server)
 			err = auther.login(query)
 			if err == nil || err != ErrInvalidCredentials {
@@ -42,7 +43,7 @@ func AuthenticateUser(query *AuthenticateUserQuery) error {
 	return err
 }
 
-func loginUsingGrafanaDB(query *AuthenticateUserQuery) error {
+func loginUsingGrafanaDB(query *LoginUserQuery) error {
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
 
 	if err := bus.Dispatch(&userQuery); err != nil {

+ 21 - 43
pkg/auth/ldap.go → pkg/login/ldap.go

@@ -1,40 +1,17 @@
-package auth
+package login
 
 import (
 	"errors"
 	"fmt"
 	"strings"
 
+	"github.com/davecgh/go-spew/spew"
 	"github.com/go-ldap/ldap"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 )
 
-var ldapServers []*LdapServerConf
-
-func init() {
-	ldapServers = []*LdapServerConf{
-		{
-			UseSSL:        false,
-			Host:          "127.0.0.1",
-			Port:          "389",
-			BindDN:        "cn=admin,dc=grafana,dc=org",
-			BindPassword:  "grafana",
-			AttrName:      "givenName",
-			AttrSurname:   "sn",
-			AttrUsername:  "cn",
-			AttrMemberOf:  "memberOf",
-			AttrEmail:     "email",
-			SearchFilter:  "(cn=%s)",
-			SearchBaseDNs: []string{"dc=grafana,dc=org"},
-			LdapGroups: []*LdapGroupToOrgRole{
-				{GroupDN: "cn=users,dc=grafana,dc=org", OrgId: 1, OrgRole: m.ROLE_VIEWER},
-			},
-		},
-	}
-}
-
 type ldapAuther struct {
 	server *LdapServerConf
 	conn   *ldap.Conn
@@ -45,7 +22,7 @@ func NewLdapAuthenticator(server *LdapServerConf) *ldapAuther {
 }
 
 func (a *ldapAuther) Dial() error {
-	address := fmt.Sprintf("%s:%s", a.server.Host, a.server.Port)
+	address := fmt.Sprintf("%s:%d", a.server.Host, a.server.Port)
 	var err error
 	if a.server.UseSSL {
 		a.conn, err = ldap.DialTLS("tcp", address, nil)
@@ -56,7 +33,7 @@ func (a *ldapAuther) Dial() error {
 	return err
 }
 
-func (a *ldapAuther) login(query *AuthenticateUserQuery) error {
+func (a *ldapAuther) login(query *LoginUserQuery) error {
 	if err := a.Dial(); err != nil {
 		return err
 	}
@@ -71,10 +48,9 @@ func (a *ldapAuther) login(query *AuthenticateUserQuery) error {
 	if ldapUser, err := a.searchForUser(query.Username); err != nil {
 		return err
 	} else {
-		log.Info("Surname: %s", ldapUser.LastName)
-		log.Info("givenName: %s", ldapUser.FirstName)
-		log.Info("email: %s", ldapUser.Email)
-		log.Info("memberOf: %s", ldapUser.MemberOf)
+		if ldapCfg.VerboseLogging {
+			log.Info("Ldap User Info: %s", spew.Sdump(ldapUser))
+		}
 
 		// check if a second user bind is needed
 		if a.server.BindPassword != "" {
@@ -164,6 +140,8 @@ func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
 						return err
 					}
 				}
+				// ignore subsequent ldap group mapping matches
+				break
 			} else {
 				// remove role
 				cmd := m.RemoveOrgUserCommand{OrgId: org.OrgId, UserId: user.Id}
@@ -244,11 +222,11 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
 			Scope:        ldap.ScopeWholeSubtree,
 			DerefAliases: ldap.NeverDerefAliases,
 			Attributes: []string{
-				a.server.AttrUsername,
-				a.server.AttrSurname,
-				a.server.AttrEmail,
-				a.server.AttrName,
-				a.server.AttrMemberOf,
+				a.server.Attr.Username,
+				a.server.Attr.Surname,
+				a.server.Attr.Email,
+				a.server.Attr.Name,
+				a.server.Attr.MemberOf,
 			},
 			Filter: fmt.Sprintf(a.server.SearchFilter, username),
 		}
@@ -264,20 +242,20 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
 	}
 
 	if len(searchResult.Entries) == 0 {
-		return nil, errors.New("Ldap search matched no entry, please review your filter setting.")
+		return nil, ErrInvalidCredentials
 	}
 
 	if len(searchResult.Entries) > 1 {
-		return nil, errors.New("Ldap search matched mopre than one entry, please review your filter setting")
+		return nil, errors.New("Ldap search matched more than one entry, please review your filter setting")
 	}
 
 	return &ldapUserInfo{
 		DN:        searchResult.Entries[0].DN,
-		LastName:  getLdapAttr(a.server.AttrSurname, searchResult),
-		FirstName: getLdapAttr(a.server.AttrName, searchResult),
-		Username:  getLdapAttr(a.server.AttrUsername, searchResult),
-		Email:     getLdapAttr(a.server.AttrEmail, searchResult),
-		MemberOf:  getLdapAttrArray(a.server.AttrMemberOf, searchResult),
+		LastName:  getLdapAttr(a.server.Attr.Surname, searchResult),
+		FirstName: getLdapAttr(a.server.Attr.Name, searchResult),
+		Username:  getLdapAttr(a.server.Attr.Username, searchResult),
+		Email:     getLdapAttr(a.server.Attr.Email, searchResult),
+		MemberOf:  getLdapAttrArray(a.server.Attr.MemberOf, searchResult),
 	}, nil
 }
 

+ 20 - 1
pkg/auth/ldap_test.go → pkg/login/ldap_test.go

@@ -1,4 +1,4 @@
-package auth
+package login
 
 import (
 	"testing"
@@ -139,6 +139,25 @@ func TestLdapAuther(t *testing.T) {
 			})
 		})
 
+		ldapAutherScenario("given multiple matching ldap groups", func(sc *scenarioContext) {
+			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
+				LdapGroups: []*LdapGroupToOrgRole{
+					{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
+					{GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
+				},
+			})
+
+			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
+			err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{
+				MemberOf: []string{"cn=admins"},
+			})
+
+			Convey("Should take first match, and ignore subsequent matches", func() {
+				So(err, ShouldBeNil)
+				So(sc.updateOrgUserCmd, ShouldBeNil)
+			})
+		})
+
 	})
 }
 

+ 1 - 1
pkg/auth/ldap_user.go → pkg/login/ldap_user.go

@@ -1,4 +1,4 @@
-package auth
+package login
 
 type ldapUserInfo struct {
 	DN        string

+ 65 - 0
pkg/login/settings.go

@@ -0,0 +1,65 @@
+package login
+
+import (
+	"github.com/BurntSushi/toml"
+	"github.com/grafana/grafana/pkg/log"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
+)
+
+type LdapConfig struct {
+	Servers        []*LdapServerConf `toml:"servers"`
+	VerboseLogging bool              `toml:"verbose_logging"`
+}
+
+type LdapServerConf struct {
+	Host         string           `toml:"host"`
+	Port         int              `toml:"port"`
+	UseSSL       bool             `toml:"use_ssl"`
+	BindDN       string           `toml:"bind_dn"`
+	BindPassword string           `toml:"bind_password"`
+	Attr         LdapAttributeMap `toml:"attributes"`
+
+	SearchFilter  string   `toml:"search_filter"`
+	SearchBaseDNs []string `toml:"search_base_dns"`
+
+	LdapGroups []*LdapGroupToOrgRole `toml:"group_mappings"`
+}
+
+type LdapAttributeMap struct {
+	Username string `toml:"username"`
+	Name     string `toml:"name"`
+	Surname  string `toml:"surname"`
+	Email    string `toml:"email"`
+	MemberOf string `toml:"member_of"`
+}
+
+type LdapGroupToOrgRole struct {
+	GroupDN string     `toml:"group_dn"`
+	OrgId   int64      `toml:"org_id"`
+	OrgRole m.RoleType `toml:"org_role"`
+}
+
+var ldapCfg LdapConfig
+
+func loadLdapConfig() {
+	if !setting.LdapEnabled {
+		return
+	}
+
+	log.Info("Login: Ldap enabled, reading config file: %s", setting.LdapConfigFile)
+
+	_, err := toml.DecodeFile(setting.LdapConfigFile, &ldapCfg)
+	if err != nil {
+		log.Fatal(3, "Failed to load ldap config file: %s", err)
+	}
+
+	// set default org id
+	for _, server := range ldapCfg.Servers {
+		for _, groupMap := range server.LdapGroups {
+			if groupMap.OrgId == 0 {
+				groupMap.OrgId = 1
+			}
+		}
+	}
+}

+ 3 - 1
pkg/setting/setting.go

@@ -118,7 +118,8 @@ var (
 	GoogleAnalyticsId string
 
 	// LDAP
-	LdapEnabled bool
+	LdapEnabled    bool
+	LdapConfigFile string
 
 	// SMTP email settings
 	Smtp SmtpSettings
@@ -417,6 +418,7 @@ func NewConfigContext(args *CommandLineArgs) {
 
 	ldapSec := Cfg.Section("auth.ldap")
 	LdapEnabled = ldapSec.Key("enabled").MustBool(false)
+	LdapConfigFile = ldapSec.Key("config_file").String()
 
 	readSessionConfig()
 	readSmtpSettings()