diff --git a/conf/configuration.go b/conf/configuration.go index 609a9e5be..451af55fb 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -43,6 +43,7 @@ type configOptions struct { Scanner scannerOptions LastFM lastfmOptions Spotify spotifyOptions + LDAP ldapOptions // DevFlags. These are used to enable/disable debugging and incomplete features DevLogSourceLine bool @@ -64,6 +65,16 @@ type spotifyOptions struct { Secret string } +type ldapOptions struct { + Host string + BindDN string + BindPassword string + Base string + SearchFilter string + Mail string + Name string +} + var Server = &configOptions{} func LoadFromFile(confFile string) { @@ -126,6 +137,14 @@ func init() { viper.SetDefault("spotify.id", "") viper.SetDefault("spotify.secret", "") + viper.SetDefault("ldap.host", "ldap://localhost:389") + viper.SetDefault("ldap.binddn", "") + viper.SetDefault("ldap.bindpassword", "") + viper.SetDefault("ldap.base", "") + viper.SetDefault("ldap.searchfilter", "(&(objectClass=inetOrgPerson)(uid=%s))") + viper.SetDefault("ldap.mail", "mail") + viper.SetDefault("ldap.name", "cn") + // DevFlags. These are used to enable/disable debugging and incomplete features viper.SetDefault("devlogsourceline", false) viper.SetDefault("devautocreateadminpassword", "") diff --git a/go.mod b/go.mod index c55db5fc9..70cb6987e 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-chi/cors v1.1.1 github.com/go-chi/httprate v0.4.0 github.com/go-chi/jwtauth v4.0.4+incompatible + github.com/go-ldap/ldap v3.0.3+incompatible github.com/golangci/golangci-lint v1.31.0 github.com/google/uuid v1.1.2 github.com/google/wire v0.4.0 @@ -42,6 +43,7 @@ require ( golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0 google.golang.org/protobuf v1.25.0 // indirect + gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/djherbis/atime.v1 v1.0.0 // indirect gopkg.in/djherbis/stream.v1 v1.3.1 // indirect gopkg.in/ini.v1 v1.57.0 // indirect diff --git a/go.sum b/go.sum index 591bd3ec6..1931f11c1 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,8 @@ github.com/go-critic/go-critic v0.5.2/go.mod h1:cc0+HvdE3lFpqLecgqMaJcvWWH77sLdB github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk= +github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= @@ -772,6 +774,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server/app/auth.go b/server/app/auth.go index 2fe29accd..c17a701aa 100644 --- a/server/app/auth.go +++ b/server/app/auth.go @@ -4,10 +4,12 @@ import ( "context" "encoding/json" "errors" + "fmt" "net/http" "strings" "time" + "github.com/deluan/navidrome/conf" "github.com/deluan/navidrome/consts" "github.com/deluan/navidrome/core/auth" "github.com/deluan/navidrome/log" @@ -16,6 +18,7 @@ import ( "github.com/deluan/rest" "github.com/dgrijalva/jwt-go" "github.com/go-chi/jwtauth" + "github.com/go-ldap/ldap" "github.com/google/uuid" ) @@ -127,7 +130,11 @@ func createDefaultUser(ctx context.Context, ds model.DataStore, username, passwo } func validateLogin(userRepo model.UserRepository, userName, password string) (*model.User, error) { - u, err := userRepo.FindByUsername(userName) + u, err := validateLoginLDAP(userRepo, userName, password) + if u != nil && err == nil { + return u, nil + } + u, err = userRepo.FindByUsername(userName) if err == model.ErrNotFound { return nil, nil } @@ -144,6 +151,86 @@ func validateLogin(userRepo model.UserRepository, userName, password string) (*m return u, nil } +func validateLoginLDAP(userRepo model.UserRepository, userName, password string) (*model.User, error) { + binduserdn := conf.Server.LDAP.BindDN + bindpassword := conf.Server.LDAP.BindPassword + mailAttr := conf.Server.LDAP.Mail + nameAttr := conf.Server.LDAP.Name + + l, err := ldap.DialURL(conf.Server.LDAP.Host) + if err != nil { + log.Error(err) + } + defer l.Close() + + // First bind with a read only user + err = l.Bind(binduserdn, bindpassword) + if err != nil { + log.Error(err) + } + + // Search for the given username + searchRequest := ldap.NewSearchRequest( + conf.Server.LDAP.Base, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf(conf.Server.LDAP.SearchFilter, ldap.EscapeFilter(userName)), + []string{"dn", nameAttr, mailAttr}, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + log.Error(err) + } + + if len(sr.Entries) != 1 { + log.Error("User does not exist or too many entries returned") + return nil, nil + } + + dn := sr.Entries[0].DN + mail := sr.Entries[0].GetAttributeValue(mailAttr) + name := sr.Entries[0].GetAttributeValue(nameAttr) + + authenticated := true + // Bind as the user to verify their password + err = l.Bind(dn, password) + + if err != nil { + log.Error(err) + authenticated = false + } + + // Rebind as the read only user for any further queries + err = l.Bind(binduserdn, bindpassword) + if err != nil { + log.Error(err) + } + + if !authenticated { + return nil, nil + } + + u, err := userRepo.FindByUsername(userName) + if err == model.ErrNotFound { + u = &model.User{UserName: userName} + } + u.Name = name + u.Email = mail + u.Password = password + err = userRepo.Put(u) + if err != nil { + log.Error("Could not update User", "user", userName) + } + + err = userRepo.UpdateLastLoginAt(u.ID) + if err != nil { + log.Error("Could not update LastLoginAt", "user", userName) + } + + return u, nil +} + func contextWithUser(ctx context.Context, ds model.DataStore, claims jwt.MapClaims) context.Context { userName := claims["sub"].(string) user, _ := ds.User(ctx).FindByUsername(userName)