diff --git a/.gitignore b/.gitignore index ace40cd7..c4e881d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.db +*.db.*.bak *db-wal *db-shm *.sql diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index 4548904c..17be3d90 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -38,6 +38,7 @@ import ( "go.senan.xyz/gonic/infocache/artistinfocache" "go.senan.xyz/gonic/jukebox" "go.senan.xyz/gonic/lastfm" + "go.senan.xyz/gonic/ldap" "go.senan.xyz/gonic/listenbrainz" "go.senan.xyz/gonic/playlist" "go.senan.xyz/gonic/podcast" @@ -83,6 +84,24 @@ func main() { confExcludePattern := flag.String("exclude-pattern", "", "regex pattern to exclude files from scan (optional)") + var ldapConfig ldap.Config + flag.StringVar(&ldapConfig.BindUser, "ldap-bind-user", "", "the bind user to bind to LDAP with (required for LDAP)") + flag.StringVar(&ldapConfig.BindPass, "ldap-bind-pass", "", "the password of the LDAP bind user (required for LDAP)") + flag.StringVar(&ldapConfig.BaseDN, "ldap-base-dn", "", "the base DN for LDAP objects (required for LDAP)") + + flag.StringVar(&ldapConfig.Filter, "ldap-filter", "", "the filter to select LDAP objects with (optional)") + flag.StringVar(&ldapConfig.AdminFilter, "ldap-admin-filter", "(memberof=cn=admin)", "the filter to select LDAP objects with (optional)") + + flag.StringVar(&ldapConfig.FQDN, "ldap-fqdn", "", "the name of the server to connect to (required for LDAP)") + flag.UintVar(&ldapConfig.Port, "ldap-port", 389, "what port the LDAP server is hosted on (optional)") + flag.BoolVar(&ldapConfig.TLS, "ldap-tls", false, "whether gonic will connect to the LDAP server using TLS (optional)") + + if ldapConfig.FQDN != "" { + if ldapConfig.BindUser == "" || ldapConfig.BindPass == "" || ldapConfig.BaseDN == "" { + log.Fatal("a server was provided for an LDAP connection, but configuration is incomplete") + } + } + var confMultiValueGenre, confMultiValueArtist, confMultiValueAlbumArtist multiValueSetting flag.Var(&confMultiValueGenre, "multi-value-genre", "setting for mutli-valued genre scanning (optional)") flag.Var(&confMultiValueArtist, "multi-value-artist", "setting for mutli-valued track artist scanning (optional)") @@ -250,11 +269,11 @@ func main() { return url.String() } - ctrlAdmin, err := ctrladmin.New(dbc, sessDB, scannr, podcast, lastfmClient, resolveProxyPath) + ctrlAdmin, err := ctrladmin.New(dbc, sessDB, scannr, podcast, lastfmClient, resolveProxyPath, ldapConfig) if err != nil { log.Panicf("error creating admin controller: %v\n", err) } - ctrlSubsonic, err := ctrlsubsonic.New(dbc, scannr, musicPaths, *confPodcastPath, cacheDirAudio, cacheDirCovers, jukebx, playlistStore, scrobblers, podcast, transcoder, lastfmClient, artistInfoCache, albumInfoCache, resolveProxyPath) + ctrlSubsonic, err := ctrlsubsonic.New(dbc, scannr, musicPaths, *confPodcastPath, cacheDirAudio, cacheDirCovers, jukebx, playlistStore, scrobblers, podcast, transcoder, lastfmClient, artistInfoCache, albumInfoCache, resolveProxyPath, ldapConfig) if err != nil { log.Panicf("error creating subsonic controller: %v\n", err) } diff --git a/db/db.go b/db/db.go index ab5a9485..981d25da 100644 --- a/db/db.go +++ b/db/db.go @@ -296,7 +296,7 @@ type User struct { ID int `gorm:"primary_key"` CreatedAt time.Time Name string `gorm:"not null; unique_index" sql:"default: null"` - Password string `gorm:"not null" sql:"default: null"` + Password string `sql:"default: null"` LastFMSession string `sql:"default: null"` ListenBrainzURL string `sql:"default: null"` ListenBrainzToken string `sql:"default: null"` diff --git a/go.mod b/go.mod index a332edce..aff2910e 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/fatih/structs v1.1.0 github.com/fsnotify/fsnotify v1.7.0 + github.com/go-ldap/ldap/v3 v3.4.6 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 @@ -34,10 +35,12 @@ require ( ) require ( + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/PuerkitoBio/goquery v1.9.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gorilla/context v1.1.2 // indirect diff --git a/go.sum b/go.sum index 86a47da1..ac221d9c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= @@ -31,6 +33,10 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= +github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -48,6 +54,7 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -135,7 +142,10 @@ github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981/go.mod h1:Rx8XB1 github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -186,6 +196,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -210,6 +222,7 @@ gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g= diff --git a/ldap/ldap.go b/ldap/ldap.go new file mode 100644 index 00000000..8be9565d --- /dev/null +++ b/ldap/ldap.go @@ -0,0 +1,226 @@ +package ldap + +import ( + "errors" + "fmt" + "log" + "time" + + "go.senan.xyz/gonic/db" + + "github.com/go-ldap/ldap/v3" +) + +var ldapStore = make(LDAPStore) + +// LDAPStore maps users to a cached password +type LDAPStore map[string]CachedLDAPpassword + +// Add caches a password username set. +func (store LDAPStore) Add(username, password string) { + store[username] = CachedLDAPpassword{ + Password: password, + ExpiresAt: time.Now().Add(time.Hour * 8), // Keep the password valid for 8 hours. + } +} + +// IsValid checks if a user's password is stored in the cache and checks if a +// given password is valid. +func (store LDAPStore) IsValid(username, password string) bool { + cached, ok := store[username] + if !ok { + return false + } + + if cached.Password != password { + return false + } + + return cached.IsValid() +} + +// CachedLDAPpassword stores an LDAP user's password and a time at which the +// server should no longer accept it. +type CachedLDAPpassword struct { + Password string + ExpiresAt time.Time +} + +func (password CachedLDAPpassword) IsValid() bool { + return password.ExpiresAt.After(time.Now()) +} + +// Cofig stores the user's LDAP server options. +type Config struct { + BindUser string + BindPass string + BaseDN string + + Filter string + AdminFilter string + + FQDN string + Port uint + TLS bool +} + +func (c Config) IsSetup() bool { + // This is basically checking if LDAP is setup, if ldapFQDN isn't set we can + // assume that the user hasn't configured LDAP. + return c.FQDN != "" +} + +func CheckLDAPcreds(username string, password string, dbc *db.DB, config Config) (bool, error) { + if !config.IsSetup() { + return false, nil + } + + if ldapStore.IsValid(username, password) { + log.Println("Password authenticated via cache!") + return true, nil + } + + log.Println("Checking password against LDAP server ...") + + // Now, we can try to connect to the LDAP server. + l, err := createLDAPconnection(config) + if err != nil { + // Return a generic error. + log.Println("Failed to connect to LDAP server:", err) + return false, errors.New("failed to connect to LDAP server") + } + defer l.Close() + + // Create the user if it doesn't exist on the database already. + err = createUserFromLDAP(username, dbc, config, l) + if err != nil { + log.Println("Failed to create user from LDAP:", err) + return false, err + } + + // After we have a connection, let's try binding + _, err = l.SimpleBind(&ldap.SimpleBindRequest{ + Username: fmt.Sprintf("uid=%s,%s", username, config.BaseDN), + Password: password, + }) + + if err == nil { + // Authentication was OK + ldapStore.Add(username, password) + return true, nil + } + + log.Println("Failed to bind to LDAP server:", err) + return false, nil +} + +// Creates a user from creds +func createUserFromLDAP(username string, dbc *db.DB, config Config, l *ldap.Conn) error { + user := dbc.GetUserByName(username) + if user != nil { + return nil + } + + if !config.IsSetup() { + return nil + } + + isAdmin := doesLDAPAdminExist(username, config, l) + log.Println(username, isAdmin) + + if doesLDAPUserExist(username, config, l) && !isAdmin { + return errors.New("no such user") + } + + newUser := db.User{ + Name: username, + Password: "", // no password because we want auth to fail. + IsAdmin: isAdmin, + } + + err := dbc.Create(&newUser).Error + if err != nil { + return err + } + + log.Println("User created via LDAP:", username) + return nil +} + +// doesLDAPAdminExist checks if an admin exists on the server. +func doesLDAPAdminExist(username string, config Config, l *ldap.Conn) bool { + filter := fmt.Sprintf("(&(uid=%s)%s)", ldap.EscapeFilter(username), config.AdminFilter) + + searchReq := ldap.NewSearchRequest( + config.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, + []string{"dn"}, + nil, + ) + + result, err := l.Search(searchReq) + if err != nil { + log.Println("failed to query LDAP server:", err) + return false + } + + if len(result.Entries) == 1 { + return true + } + + return false +} + +// doesLDAPUserExist checks if a user exists on the server. +func doesLDAPUserExist(username string, config Config, l *ldap.Conn) bool { + filter := fmt.Sprintf("(&(uid=%s)%s)", ldap.EscapeFilter(username), config.Filter) + + searchReq := ldap.NewSearchRequest( + config.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, + []string{"dn"}, + nil, + ) + + result, err := l.Search(searchReq) + if err != nil { + log.Println("failed to query LDAP server:", err) + return false + } + + if len(result.Entries) == 1 { + return true + } + + return false +} + +// Creates a connection to an LDAP server. +func createLDAPconnection(config Config) (*ldap.Conn, error) { + protocol := "ldap" + if config.TLS { + protocol = "ldaps" + } + + // Now, we can try to connect to the LDAP server. + l, err := ldap.DialURL(fmt.Sprintf("%s://%s:%d", protocol, config.FQDN, config.Port)) + if err != nil { + // Warn the server and return the error. + log.Println("Failed to connect to LDAP server", err) + return nil, err + } + + // After we have a connection, let's try binding + _, err = l.SimpleBind(&ldap.SimpleBindRequest{ + Username: fmt.Sprintf("uid=%s,%s", config.BindUser, config.BaseDN), + Password: config.BindPass, + }) + if err != nil { + log.Println("Failed to bind to LDAP:", err) + return nil, errors.New("wrong username or password") + } + + return l, nil +} diff --git a/server/ctrladmin/ctrl.go b/server/ctrladmin/ctrl.go index 063869f6..2a00384a 100644 --- a/server/ctrladmin/ctrl.go +++ b/server/ctrladmin/ctrl.go @@ -27,6 +27,7 @@ import ( "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/handlerutil" "go.senan.xyz/gonic/lastfm" + "go.senan.xyz/gonic/ldap" "go.senan.xyz/gonic/podcast" "go.senan.xyz/gonic/scanner" "go.senan.xyz/gonic/server/ctrladmin/adminui" @@ -48,11 +49,12 @@ type Controller struct { podcasts *podcast.Podcasts lastfmClient *lastfm.Client resolveProxyPath ProxyPathResolver + ldapConfig ldap.Config } type ProxyPathResolver func(in string) string -func New(dbc *db.DB, sessDB *gormstore.Store, scanner *scanner.Scanner, podcasts *podcast.Podcasts, lastfmClient *lastfm.Client, resolveProxyPath ProxyPathResolver) (*Controller, error) { +func New(dbc *db.DB, sessDB *gormstore.Store, scanner *scanner.Scanner, podcasts *podcast.Podcasts, lastfmClient *lastfm.Client, resolveProxyPath ProxyPathResolver, ldapConfig ldap.Config) (*Controller, error) { c := Controller{ ServeMux: http.NewServeMux(), @@ -62,6 +64,7 @@ func New(dbc *db.DB, sessDB *gormstore.Store, scanner *scanner.Scanner, podcasts podcasts: podcasts, lastfmClient: lastfmClient, resolveProxyPath: resolveProxyPath, + ldapConfig: ldapConfig, } resp := respHandler(adminui.TemplatesFS, resolveProxyPath) diff --git a/server/ctrladmin/handlers_raw.go b/server/ctrladmin/handlers_raw.go index 49d9e2a8..d304afd7 100644 --- a/server/ctrladmin/handlers_raw.go +++ b/server/ctrladmin/handlers_raw.go @@ -1,28 +1,46 @@ package ctrladmin import ( + "log" "net/http" "github.com/gorilla/sessions" + "go.senan.xyz/gonic/ldap" ) func (c *Controller) ServeLoginDo(w http.ResponseWriter, r *http.Request) { session := r.Context().Value(CtxSession).(*sessions.Session) username := r.FormValue("username") password := r.FormValue("password") + user := c.dbc.GetUserByName(username) if username == "" || password == "" { sessAddFlashW(session, []string{"please provide username and password"}) sessLogSave(session, w, r) http.Redirect(w, r, r.Referer(), http.StatusSeeOther) return } - user := c.dbc.GetUserByName(username) - if user == nil || password != user.Password { + + if c.ldapConfig.IsSetup() { + ok, err := ldap.CheckLDAPcreds(username, password, c.dbc, c.ldapConfig) + if err != nil { + log.Println("Failed to check LDAP credentials:", err) + sessAddFlashW(session, []string{"failed to check LDAP credentials"}) + } else if !ok { + sessAddFlashW(session, []string{"invalid username / password"}) + } + + sessLogSave(session, w, r) + http.Redirect(w, r, r.Referer(), http.StatusSeeOther) + return + } else if user == nil || user.Password != password { sessAddFlashW(session, []string{"invalid username / password"}) sessLogSave(session, w, r) http.Redirect(w, r, r.Referer(), http.StatusSeeOther) return } + + user = c.dbc.GetUserByName(username) + // put the user name into the session. future endpoints after this one // are wrapped with WithUserSession() which will get the name from the // session and put the row into the request context diff --git a/server/ctrlsubsonic/ctrl.go b/server/ctrlsubsonic/ctrl.go index 12d0bab2..4f583ff1 100644 --- a/server/ctrlsubsonic/ctrl.go +++ b/server/ctrlsubsonic/ctrl.go @@ -18,6 +18,7 @@ import ( "go.senan.xyz/gonic/infocache/artistinfocache" "go.senan.xyz/gonic/jukebox" "go.senan.xyz/gonic/lastfm" + "go.senan.xyz/gonic/ldap" "go.senan.xyz/gonic/playlist" "go.senan.xyz/gonic/podcast" "go.senan.xyz/gonic/scanner" @@ -69,7 +70,7 @@ type Controller struct { resolveProxyPath ProxyPathResolver } -func New(dbc *db.DB, scannr *scanner.Scanner, musicPaths []MusicPath, podcastsPath string, cacheAudioPath string, cacheCoverPath string, jukebox *jukebox.Jukebox, playlistStore *playlist.Store, scrobblers []scrobble.Scrobbler, podcasts *podcast.Podcasts, transcoder transcode.Transcoder, lastFMClient *lastfm.Client, artistInfoCache *artistinfocache.ArtistInfoCache, albumInfoCache *albuminfocache.AlbumInfoCache, resolveProxyPath ProxyPathResolver) (*Controller, error) { +func New(dbc *db.DB, scannr *scanner.Scanner, musicPaths []MusicPath, podcastsPath string, cacheAudioPath string, cacheCoverPath string, jukebox *jukebox.Jukebox, playlistStore *playlist.Store, scrobblers []scrobble.Scrobbler, podcasts *podcast.Podcasts, transcoder transcode.Transcoder, lastFMClient *lastfm.Client, artistInfoCache *artistinfocache.ArtistInfoCache, albumInfoCache *albuminfocache.AlbumInfoCache, resolveProxyPath ProxyPathResolver, ldapConfig ldap.Config) (*Controller, error) { c := Controller{ ServeMux: http.NewServeMux(), @@ -93,7 +94,7 @@ func New(dbc *db.DB, scannr *scanner.Scanner, musicPaths []MusicPath, podcastsPa chain := handlerutil.Chain( withParams, withRequiredParams, - withUser(dbc), + withUser(dbc, ldapConfig), ) chainRaw := handlerutil.Chain( chain, @@ -223,7 +224,7 @@ func withRequiredParams(next http.Handler) http.Handler { }) } -func withUser(dbc *db.DB) handlerutil.Middleware { +func withUser(dbc *db.DB, ldapConfig ldap.Config) handlerutil.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { params := r.Context().Value(CtxParams).(params.Params) @@ -240,22 +241,48 @@ func withUser(dbc *db.DB) handlerutil.Middleware { "please provide `t` and `s`, or just `p`")) return } + user := dbc.GetUserByName(username) + + if ldapConfig.IsSetup() { + // Complete auth using LDAP + log.Println("Authenticating using LDAP ...") + + ok, err := ldap.CheckLDAPcreds(username, password, dbc, ldapConfig) + if err != nil { + log.Println("Failed to check LDAP creds:", err) + _ = writeResp(w, r, spec.NewError(40, "invalid password")) + return + } + + if !ok { + _ = writeResp(w, r, spec.NewError(40, "invalid password")) + return + } + + withUser := context.WithValue(r.Context(), CtxUser, user) + next.ServeHTTP(w, r.WithContext(withUser)) + return + } + + log.Println("Authenticating using built-in ...") if user == nil { - _ = writeResp(w, r, spec.NewError(40, - "invalid username %q", username)) + _ = writeResp(w, r, spec.NewError(40, "invalid password")) return } + var credsOk bool if tokenAuth { credsOk = checkCredsToken(user.Password, token, salt) } else { credsOk = checkCredsBasic(user.Password, password) } + if !credsOk { _ = writeResp(w, r, spec.NewError(40, "invalid password")) return } + withUser := context.WithValue(r.Context(), CtxUser, user) next.ServeHTTP(w, r.WithContext(withUser)) })