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