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

This commit is contained in:
Torkel Ödegaard 2015-07-15 10:08:23 +02:00
parent 262a09bb2d
commit 0b5ba55131
14 changed files with 167 additions and 100 deletions

View File

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

View File

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

View File

@ -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
conf/ldap.toml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

65
pkg/login/settings.go Normal file
View File

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

View File

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