From 58849bf3096ca16877e54aac1f2983a14940f011 Mon Sep 17 00:00:00 2001 From: defkev Date: Tue, 15 Dec 2020 03:47:13 +0100 Subject: [PATCH] Add LDAP support --- auth/auth.go | 3 +- auth/json.go | 2 +- auth/ldap.go | 158 ++++++++++++++++++++++++++++ auth/none.go | 2 +- auth/proxy.go | 2 +- auth/storage.go | 9 +- cmd/config.go | 64 +++++++++++ frontend/src/components/Sidebar.vue | 2 +- http/auth.go | 2 +- storage/bolt/auth.go | 2 + storage/bolt/bolt.go | 2 +- 11 files changed, 237 insertions(+), 11 deletions(-) create mode 100644 auth/ldap.go diff --git a/auth/auth.go b/auth/auth.go index 72dc8f41..17e1e65f 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -4,12 +4,13 @@ import ( "net/http" "github.com/filebrowser/filebrowser/v2/users" + "github.com/filebrowser/filebrowser/v2/settings" ) // Auther is the authentication interface. type Auther interface { // Auth is called to authenticate a request. - Auth(r *http.Request, s *users.Storage, root string) (*users.User, error) + Auth(r *http.Request, s *users.Storage, set *settings.Storage, root string) (*users.User, error) // LoginPage indicates if this auther needs a login page. LoginPage() bool } diff --git a/auth/json.go b/auth/json.go index 641b73cd..989c2c7d 100644 --- a/auth/json.go +++ b/auth/json.go @@ -26,7 +26,7 @@ type JSONAuth struct { } // Auth authenticates the user via a json in content body. -func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { +func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, set *settings.Storage, root string) (*users.User, error) { var cred jsonCred if r.Body == nil { diff --git a/auth/ldap.go b/auth/ldap.go new file mode 100644 index 00000000..a52f63b5 --- /dev/null +++ b/auth/ldap.go @@ -0,0 +1,158 @@ +package auth + +import ( + "net/http" + "encoding/json" + "crypto/tls" + "fmt" + "strings" + + "github.com/filebrowser/filebrowser/v2/settings" + "github.com/filebrowser/filebrowser/v2/users" + "github.com/filebrowser/filebrowser/v2/errors" + "github.com/go-ldap/ldap/v3" +) + +const MethodLDAPAuth settings.AuthMethod = "ldap" + +type LDAPAuth struct { + Server string `json:"server"` + StartTLS bool `json:"starttls"` + SkipVerify bool `json:"skipverify"` + BaseDN string `json:"basedn"` + UserOU string `json:"userou"` + GroupOU string `json:"groupou"` + UserCN string `json:"usercn"` + AdminCN string `json:"admincn"` + UserHome string `json:"userhome"` +} + +func (a LDAPAuth) Auth(r *http.Request, sto *users.Storage, set *settings.Storage, root string) (user *users.User, err error) { + var cred jsonCred + + if r.Body == nil { + return nil, errors.ErrEmptyRequest + } + + err = json.NewDecoder(r.Body).Decode(&cred) + if err != nil { + return nil, err + } + + p := strings.Split(a.Server, ":") + var l *ldap.Conn + if p[0] == "ldaps" { + l, err = ldap.DialURL(a.Server, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: a.SkipVerify})) + } else { + l, err = ldap.DialURL(a.Server) + if err != nil { + return nil, err + } + if a.StartTLS { + err = l.StartTLS(&tls.Config{InsecureSkipVerify: a.SkipVerify}) + } + } + if err != nil { + return nil, err + } + bind := fmt.Sprintf("uid=%s,ou=%s,%s", cred.Username, a.UserOU, a.BaseDN) + err = l.Bind(bind, cred.Password) + if err != nil { + return nil, err + } + + var s *settings.Settings + s, err = set.Get() + if err != nil { + return nil, err + } + + var isadmin bool + var hashome string = s.Defaults.Scope + if a.AdminCN != "" || a.UserCN != "" || a.UserHome != "" { + searchRequest := ldap.NewSearchRequest( + bind, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, + 0, + false, + "(&(objectClass=*))", + []string{"memberOf", a.UserHome}, + nil, + ) + searchResult, err := l.Search(searchRequest) + if err != nil { + return nil, err + } + l.Close() + + var isuser bool + admingrp := fmt.Sprintf("cn=%s,ou=%s,%s", a.AdminCN, a.GroupOU, a.BaseDN) + usergrp := fmt.Sprintf("cn=%s,ou=%s,%s", a.UserCN, a.GroupOU, a.BaseDN) + for _, group := range searchResult.Entries[0].GetAttributeValues("memberOf") { + switch group { + case admingrp: + isadmin = true + case usergrp: + isuser = true + } + } + if a.UserHome != "" { + hashome = searchResult.Entries[0].GetAttributeValue(a.UserHome) + } + + // Deny entry to non-users if user group is enabled, admins always have access + if !isadmin && a.UserCN != "" && !isuser { + return nil, errors.ErrPermissionDenied + } + } + + user, err = sto.Get(root, cred.Username) + if err != nil { + if err == errors.ErrNotExist { + user = &users.User{ + Username: cred.Username, + Password: "much5af3v3rys3cur3", // No point hashing the password since we don't use it + LockPassword: true, // Prevent user password change which would only lead to confusion + } + s.Defaults.Apply(user) + user.Perm.Admin = isadmin + if user.Scope != hashome { + user.Scope = hashome + } else { + home, err := s.MakeUserDir(cred.Username, user.Scope, root) + if err != nil { + return nil, err + } + user.Scope = home + } + err = sto.Save(user) + if err != nil { + return nil, err + } + } else { + return nil, err + } + } else { + // Keep profile in sync with LDAP + var update bool + if user.Perm.Admin != isadmin { + user.Perm.Admin = isadmin + update = true + } + if user.Scope != hashome { + user.Scope = hashome + update = true + } + if update { + sto.Update(user) + } + } + + return +} + +func (a LDAPAuth) LoginPage() bool { + return true +} \ No newline at end of file diff --git a/auth/none.go b/auth/none.go index 7e47baf7..e099d279 100644 --- a/auth/none.go +++ b/auth/none.go @@ -14,7 +14,7 @@ const MethodNoAuth settings.AuthMethod = "noauth" type NoAuth struct{} // Auth uses authenticates user 1. -func (a NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { +func (a NoAuth) Auth(r *http.Request, sto *users.Storage, set *settings.Storage, root string) (*users.User, error) { return sto.Get(root, uint(1)) } diff --git a/auth/proxy.go b/auth/proxy.go index 21d072c7..35204c23 100644 --- a/auth/proxy.go +++ b/auth/proxy.go @@ -18,7 +18,7 @@ type ProxyAuth struct { } // Auth authenticates the user via an HTTP header. -func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { +func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, set *settings.Storage, root string) (*users.User, error) { username := r.Header.Get(a.Header) user, err := sto.Get(root, username) if err == errors.ErrNotExist { diff --git a/auth/storage.go b/auth/storage.go index c0723ba5..85f1763d 100644 --- a/auth/storage.go +++ b/auth/storage.go @@ -13,13 +13,14 @@ type StorageBackend interface { // Storage is a auth storage. type Storage struct { - back StorageBackend - users *users.Storage + back StorageBackend + users *users.Storage + settings *settings.Storage } // NewStorage creates a auth storage from a backend. -func NewStorage(back StorageBackend, userStore *users.Storage) *Storage { - return &Storage{back: back, users: userStore} +func NewStorage(back StorageBackend, userStore *users.Storage, settingsStore *settings.Storage) *Storage { + return &Storage{back: back, users: userStore, settings: settingsStore} } // Get wraps a StorageBackend.Get. diff --git a/cmd/config.go b/cmd/config.go index 5e4da979..e92c822d 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -36,6 +36,16 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type") flags.String("auth.header", "", "HTTP header for auth.method=proxy") + flags.String("ldap.server", "ldap://localhost", "LDAP scheme://server:port, if port is omitted the respective default will be used") + flags.Bool("ldap.starttls", true, "LDAP attempt to negotiate a secure connection on an insecure scheme") + flags.Bool("ldap.skipverify", false, "LDAP skip server certificate verification on secure connection") + flags.String("ldap.basedn", "dc=example,dc=com", "LDAP base distinguished name") + flags.String("ldap.userou", "people", "LDAP user organizational unit, will be appended to the BaseDN for authentification binds") + flags.String("ldap.groupou", "groups", "LDAP group organizational unit, will be appended to the BaseDN for memberOf queries") + flags.String("ldap.usercn", "", "LDAP user group, will be appended to the GroupOU, if set only members of this group can login") + flags.String("ldap.admincn", "file_browser_admins", "LDAP admin group common name, will be appended to the GroupOU, members of this group get admin permission") + flags.String("ldap.userhome", "homeDirectory", "LDAP attribute to use as a users scope relative to the servers root, set to nothing to disable") + flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China") flags.String("recaptcha.key", "", "ReCaptcha site key") flags.String("recaptcha.secret", "", "ReCaptcha secret") @@ -113,6 +123,60 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings. auther = jsonAuth } + if method == auth.MethodLDAPAuth { + server := mustGetString(flags, "ldap.server") + if server == "" { + server = defaultAuther["server"].(string) + } + starttls := mustGetBool(flags, "ldap.starttls") + if !starttls { + if i, ok := defaultAuther["starttls"].(bool); ok { + starttls = i + } + } + skipverify := mustGetBool(flags, "ldap.skipverify") + if !skipverify { + if i, ok := defaultAuther["skipverify"].(bool); ok { + skipverify = i + } + } + basedn := mustGetString(flags, "ldap.basedn") + if basedn == "" { + basedn = defaultAuther["basedn"].(string) + } + userou := mustGetString(flags, "ldap.userou") + if userou == "" { + userou = defaultAuther["userou"].(string) + } + groupou := mustGetString(flags, "ldap.groupou") + if userou == "" { + userou = defaultAuther["groupou"].(string) + } + usercn := mustGetString(flags, "ldap.usercn") + if userou == "" { + userou = defaultAuther["usercn"].(string) + } + admincn := mustGetString(flags, "ldap.admincn") + if userou == "" { + userou = defaultAuther["admincn"].(string) + } + userhome := mustGetString(flags, "ldap.userhome") + if userhome == "" { + userhome = defaultAuther["userhome"].(string) + } + auther = &auth.LDAPAuth{ + Server: server, + StartTLS: starttls, + SkipVerify: skipverify, + BaseDN: basedn, + UserOU: userou, + GroupOU: groupou, + UserCN: usercn, + AdminCN: admincn, + UserHome: userhome, + } + } + if auther == nil { panic(errors.ErrInvalidAuthMethod) } diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 753952ed..5c019f9c 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -24,7 +24,7 @@ {{ $t('sidebar.settings') }} - diff --git a/http/auth.go b/http/auth.go index c73897c2..400f9785 100644 --- a/http/auth.go +++ b/http/auth.go @@ -99,7 +99,7 @@ var loginHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, e return http.StatusInternalServerError, err } - user, err := auther.Auth(r, d.store.Users, d.server.Root) + user, err := auther.Auth(r, d.store.Users, d.store.Settings, d.server.Root) if err == os.ErrPermission { return http.StatusForbidden, nil } else if err != nil { diff --git a/storage/bolt/auth.go b/storage/bolt/auth.go index 6078f63c..b49b0245 100644 --- a/storage/bolt/auth.go +++ b/storage/bolt/auth.go @@ -22,6 +22,8 @@ func (s authBackend) Get(t settings.AuthMethod) (auth.Auther, error) { auther = &auth.ProxyAuth{} case auth.MethodNoAuth: auther = &auth.NoAuth{} + case auth.MethodLDAPAuth: + auther = &auth.LDAPAuth{} default: return nil, errors.ErrInvalidAuthMethod } diff --git a/storage/bolt/bolt.go b/storage/bolt/bolt.go index ce67adc0..89afd90d 100644 --- a/storage/bolt/bolt.go +++ b/storage/bolt/bolt.go @@ -15,7 +15,7 @@ func NewStorage(db *storm.DB) (*storage.Storage, error) { userStore := users.NewStorage(usersBackend{db: db}) shareStore := share.NewStorage(shareBackend{db: db}) settingsStore := settings.NewStorage(settingsBackend{db: db}) - authStore := auth.NewStorage(authBackend{db: db}, userStore) + authStore := auth.NewStorage(authBackend{db: db}, userStore, settingsStore) err := save(db, "version", 2) if err != nil {