Add LDAP support
This commit is contained in:
parent
bc4a6462ce
commit
58849bf309
@ -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
|
||||
}
|
||||
|
||||
@ -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
158
auth/ldap.go
Normal 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
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user