diff --git a/conf/defaults.ini b/conf/defaults.ini index 258a0198155..e3e5f6eb5f9 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -174,6 +174,18 @@ header_name = X-WEBAUTH-USER header_property = username auto_sign_up = true +#################################### Auth LDAP ########################## +[auth.ldap] +enabled = true +hosts = ldap://localhost.com:389 +use_ssl = false +base_dn = dc=grafana,dc=org +bind_path = cn=%username%,dc=grafana,dc=org +attr_username = cn +attr_name = cn +attr_surname = sn +attr_email = email + #################################### Logging ########################## [log] # Either "console", "file", default is "console" diff --git a/pkg/api/api.go b/pkg/api/api.go index 6ecaa51652e..0d8bceed4f8 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -19,7 +19,7 @@ func Register(r *macaron.Macaron) { // not logged in views r.Get("/", reqSignedIn, Index) r.Get("/logout", Logout) - r.Post("/login", bind(dtos.LoginCommand{}), LoginPost) + r.Post("/login", bind(dtos.LoginCommand{}), wrap(LoginPost)) r.Get("/login/:name", OAuthLogin) r.Get("/login", LoginView) diff --git a/pkg/api/ldap/ldap.go b/pkg/api/ldap/ldap.go deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/pkg/api/ldapauth/ldapauth.go b/pkg/api/ldapauth/ldapauth.go new file mode 100644 index 00000000000..ea6c0421e12 --- /dev/null +++ b/pkg/api/ldapauth/ldapauth.go @@ -0,0 +1,56 @@ +package ldapauth + +import ( + "errors" + "fmt" + "net/url" + + "github.com/gogits/gogs/modules/ldap" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + ErrInvalidCredentials = errors.New("Invalid Username or Password") +) + +func Login(username, password string) error { + url, err := url.Parse(setting.LdapUrls[0]) + if err != nil { + return err + } + + log.Info("Host: %v", url.Host) + conn, err := ldap.Dial("tcp", url.Host) + if err != nil { + return err + } + + defer conn.Close() + + bindFormat := "cn=%s,dc=grafana,dc=org" + + nx := fmt.Sprintf(bindFormat, username) + err = conn.Bind(nx, password) + + if err != nil { + if ldapErr, ok := err.(*ldap.Error); ok { + if ldapErr.ResultCode == 49 { + return ErrInvalidCredentials + } + } + return err + } + return nil + + // search := ldap.NewSearchRequest(url.Path, + // ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + // fmt.Sprintf(ls.Filter, name), + // []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}, + // nil) + // sr, err := l.Search(search) + // if err != nil { + // log.Debug("LDAP Authen OK but not in filter %s", name) + // return "", "", "", "", false + // } +} diff --git a/pkg/api/login.go b/pkg/api/login.go index 0fc5651d5f9..f6a511bffbe 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -4,6 +4,8 @@ import ( "net/url" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/api/ldapauth" + "github.com/grafana/grafana/pkg/auth" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" @@ -86,21 +88,28 @@ func LoginApiPing(c *middleware.Context) { c.JsonOK("Logged in") } -func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) { - userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.User} - err := bus.Dispatch(&userQuery) - - if err != nil { - c.JsonApiErr(401, "Invalid username or password", err) - return +func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response { + sourcesQuery := auth.GetAuthSourcesQuery{} + if err := bus.Dispatch(&sourcesQuery); err != nil { + return ApiError(500, "Could not get login sources", err) } - user := userQuery.Result + var err error + var user *m.User - passwordHashed := util.EncodePassword(cmd.Password, user.Salt) - if passwordHashed != user.Password { - c.JsonApiErr(401, "Invalid username or password", err) - return + for _, authSource := range sourcesQuery.Sources { + user, err = authSource.AuthenticateUser(cmd.User, cmd.Password) + if err == nil { + break + } + // handle non invalid credentials error, otherwise try next auth source + if err != auth.ErrInvalidCredentials { + return ApiError(500, "Error while trying to authenticate user", err) + } + } + + if err != nil { + return ApiError(401, "Invalid username or password", err) } loginUserWithUser(user, c) @@ -116,7 +125,20 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) { metrics.M_Api_Login_Post.Inc(1) - c.JSON(200, result) + return Json(200, result) +} + +func LoginUsingLdap(c *middleware.Context, cmd dtos.LoginCommand) Response { + err := ldapauth.Login(cmd.User, cmd.Password) + + if err != nil { + if err == ldapauth.ErrInvalidCredentials { + return ApiError(401, "Invalid username or password", err) + } + return ApiError(500, "Ldap login failed", err) + } + + return Empty(401) } func loginUserWithUser(user *m.User, c *middleware.Context) { diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 00000000000..a236663ed44 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,73 @@ +package auth + +import ( + "errors" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/util" +) + +var ( + ErrInvalidCredentials = errors.New("Invalid Username or Password") +) + +type LoginSettings struct { + LdapEnabled bool +} + +type LdapFilterToOrg struct { + Filter string + OrgId int + OrgRole string +} + +type LdapSettings struct { + Enabled bool + Hosts []string + UseSSL bool + BindDN string + AttrUsername string + AttrName string + AttrSurname string + AttrMail string + Filters []LdapFilterToOrg +} + +type AuthSource interface { + AuthenticateUser(username, password string) (*m.User, error) +} + +type GetAuthSourcesQuery struct { + Sources []AuthSource +} + +func init() { + bus.AddHandler("auth", GetAuthSources) +} + +func GetAuthSources(query *GetAuthSourcesQuery) error { + query.Sources = []AuthSource{&GrafanaDBAuthSource{}} + return nil +} + +type GrafanaDBAuthSource struct { +} + +func (s *GrafanaDBAuthSource) AuthenticateUser(username, password string) (*m.User, error) { + userQuery := m.GetUserByLoginQuery{LoginOrEmail: username} + err := bus.Dispatch(&userQuery) + + if err != nil { + return nil, ErrInvalidCredentials + } + + user := userQuery.Result + + passwordHashed := util.EncodePassword(password, user.Salt) + if passwordHashed != user.Password { + return nil, ErrInvalidCredentials + } + + return user, nil +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6768f9aabd9..3cb4792ca82 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -114,6 +114,10 @@ var ( ReportingEnabled bool GoogleAnalyticsId string + + // LDAP + LdapEnabled bool + LdapUrls []string ) type CommandLineArgs struct { @@ -406,6 +410,10 @@ func NewConfigContext(args *CommandLineArgs) { ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String() + ldapSec := Cfg.Section("auth.ldap") + LdapEnabled = ldapSec.Key("enabled").MustBool(false) + LdapUrls = ldapSec.Key("urls").Strings(" ") + readSessionConfig() } diff --git a/pkg/setting/setting_ldap.go b/pkg/setting/setting_ldap.go new file mode 100644 index 00000000000..26592159f1f --- /dev/null +++ b/pkg/setting/setting_ldap.go @@ -0,0 +1,19 @@ +package setting + +type LdapFilterToOrg struct { + Filter string + OrgId int + OrgRole string +} + +type LdapSettings struct { + Enabled bool + Hosts []string + UseSSL bool + BindDN string + AttrUsername string + AttrName string + AttrSurname string + AttrMail string + Filters []LdapFilterToOrg +}