Add LDAP support

This commit is contained in:
defkev 2020-12-15 03:47:13 +01:00
parent bc4a6462ce
commit 58849bf309
11 changed files with 237 additions and 11 deletions

View File

@ -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
}

View File

@ -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 {

158
auth/ldap.go Normal file
View File

@ -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
}

View File

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

View File

@ -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 {

View File

@ -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.

View File

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

View File

@ -24,7 +24,7 @@
<span>{{ $t('sidebar.settings') }}</span>
</router-link>
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
<button v-if="authMethod == 'json' || authMethod == 'ldap'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
<i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.logout') }}</span>
</button>

View File

@ -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 {

View File

@ -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
}

View File

@ -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 {