mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-25 08:54:13 +00:00
Proxy- and hook-authenticated users were auto-provisioned by applying the
default scope (".") and passing it straight to MakeUserDir, which normalizes
"." to "/". With CreateUserDir enabled, every provisioned user therefore
received the server root as its scope instead of a per-user home directory,
letting one user read, overwrite and delete another user's files.
The signup handler already cleared the scope before deriving the home
directory. Centralize that logic into Settings.CreateUserHome (clear the
scope when CreateUserDir is on and no explicit scope was supplied, derive the
home dir, then reject a scope already owned by another user) and use it from
signup, proxy and hook auth so the three provisioning paths cannot diverge.
Refs GHSA-j7jh-37pf-mf8h, GHSA-j2fc-28fx-hc8q
287 lines
8 KiB
Go
287 lines
8 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"slices"
|
|
"strings"
|
|
|
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
|
"github.com/filebrowser/filebrowser/v2/files"
|
|
"github.com/filebrowser/filebrowser/v2/settings"
|
|
"github.com/filebrowser/filebrowser/v2/users"
|
|
)
|
|
|
|
// MethodHookAuth is used to identify hook auth.
|
|
const MethodHookAuth settings.AuthMethod = "hook"
|
|
|
|
type hookCred struct {
|
|
Password string `json:"password"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
// HookAuth is a hook implementation of an Auther.
|
|
type HookAuth struct {
|
|
Users users.Store `json:"-"`
|
|
Settings *settings.Settings `json:"-"`
|
|
Server *settings.Server `json:"-"`
|
|
Cred hookCred `json:"-"`
|
|
Fields hookFields `json:"-"`
|
|
Command string `json:"command"`
|
|
}
|
|
|
|
// Auth authenticates the user via a json in content body.
|
|
func (a *HookAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
|
|
var cred hookCred
|
|
|
|
if r.Body == nil {
|
|
return nil, os.ErrPermission
|
|
}
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&cred)
|
|
if err != nil {
|
|
return nil, os.ErrPermission
|
|
}
|
|
|
|
a.Users = usr
|
|
a.Settings = stg
|
|
a.Server = srv
|
|
a.Cred = cred
|
|
|
|
action, err := a.RunCommand()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch action {
|
|
case "auth":
|
|
u, err := a.SaveUser()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return u, nil
|
|
case "block":
|
|
return nil, os.ErrPermission
|
|
case "pass":
|
|
u, err := a.Users.Get(a.Server.Root, a.Server.FollowExternalSymlinks, a.Cred.Username)
|
|
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
|
|
return nil, os.ErrPermission
|
|
}
|
|
return u, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid hook action: %s", action)
|
|
}
|
|
}
|
|
|
|
// LoginPage tells that hook auth requires a login page.
|
|
func (a *HookAuth) LoginPage() bool {
|
|
return true
|
|
}
|
|
|
|
// RunCommand starts the hook command and returns the action
|
|
func (a *HookAuth) RunCommand() (string, error) {
|
|
command := strings.Split(a.Command, " ")
|
|
|
|
cmd := exec.Command(command[0], command[1:]...)
|
|
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
a.GetValues(string(out))
|
|
|
|
return a.Fields.Values["hook.action"], nil
|
|
}
|
|
|
|
// GetValues creates a map with values from the key-value format string
|
|
func (a *HookAuth) GetValues(s string) {
|
|
m := map[string]string{}
|
|
|
|
// make line breaks consistent on Windows platform
|
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
|
|
|
// iterate input lines
|
|
for val := range strings.Lines(s) {
|
|
v := strings.SplitN(val, "=", 2)
|
|
|
|
// skips non key and value format
|
|
if len(v) != 2 {
|
|
continue
|
|
}
|
|
|
|
fieldKey := strings.TrimSpace(v[0])
|
|
fieldValue := strings.TrimSpace(v[1])
|
|
|
|
if a.Fields.IsValid(fieldKey) {
|
|
m[fieldKey] = fieldValue
|
|
}
|
|
}
|
|
|
|
a.Fields.Values = m
|
|
}
|
|
|
|
// SaveUser updates the existing user or creates a new one when not found
|
|
func (a *HookAuth) SaveUser() (*users.User, error) {
|
|
u, err := a.Users.Get(a.Server.Root, a.Server.FollowExternalSymlinks, a.Cred.Username)
|
|
if err != nil && !errors.Is(err, fberrors.ErrNotExist) {
|
|
return nil, err
|
|
}
|
|
|
|
if u == nil {
|
|
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// create user with the provided credentials
|
|
d := &users.User{
|
|
Username: a.Cred.Username,
|
|
Password: pass,
|
|
Scope: a.Settings.Defaults.Scope,
|
|
Locale: a.Settings.Defaults.Locale,
|
|
ViewMode: a.Settings.Defaults.ViewMode,
|
|
SingleClick: a.Settings.Defaults.SingleClick,
|
|
RedirectAfterCopyMove: a.Settings.Defaults.RedirectAfterCopyMove,
|
|
Sorting: a.Settings.Defaults.Sorting,
|
|
Perm: a.Settings.Defaults.Perm,
|
|
Commands: a.Settings.Defaults.Commands,
|
|
DateFormat: a.Settings.Defaults.DateFormat,
|
|
HideDotfiles: a.Settings.Defaults.HideDotfiles,
|
|
}
|
|
u = a.GetUser(d)
|
|
|
|
// A scope explicitly returned by the hook takes precedence over the
|
|
// automatic per-user home directory derivation.
|
|
_, explicitScope := a.Fields.Values["user.scope"]
|
|
if err := a.Settings.CreateUserHome(u, a.Users, a.Server.Root, explicitScope); err != nil {
|
|
return nil, err
|
|
}
|
|
log.Printf("user: %s, home dir: [%s].", u.Username, u.Scope)
|
|
|
|
if err := a.Users.Save(u); err != nil {
|
|
return nil, err
|
|
}
|
|
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
|
|
u = a.GetUser(u)
|
|
|
|
// update the password when it doesn't match the current
|
|
if p {
|
|
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.Password = pass
|
|
}
|
|
|
|
// update user with provided fields
|
|
err := a.Users.Update(u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
// GetUser returns a User filled with hook values or provided defaults
|
|
func (a *HookAuth) GetUser(d *users.User) *users.User {
|
|
// adds all permissions when user is admin
|
|
isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
|
|
perms := users.Permissions{
|
|
Admin: isAdmin,
|
|
Execute: isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
|
|
Create: isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
|
|
Rename: isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
|
|
Modify: isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
|
|
Delete: isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
|
|
Share: isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
|
|
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
|
|
}
|
|
user := users.User{
|
|
ID: d.ID,
|
|
Username: d.Username,
|
|
Password: d.Password,
|
|
Scope: a.Fields.GetString("user.scope", d.Scope),
|
|
Locale: a.Fields.GetString("user.locale", d.Locale),
|
|
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
|
|
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
|
RedirectAfterCopyMove: a.Fields.GetBoolean("user.redirectAfterCopyMove", d.RedirectAfterCopyMove),
|
|
Sorting: files.Sorting{
|
|
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
|
|
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
|
|
},
|
|
Commands: a.Fields.GetArray("user.commands", d.Commands),
|
|
DateFormat: a.Fields.GetBoolean("user.dateFormat", d.DateFormat),
|
|
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
|
|
Perm: perms,
|
|
LockPassword: true,
|
|
}
|
|
|
|
return &user
|
|
}
|
|
|
|
// hookFields is used to access fields from the hook
|
|
type hookFields struct {
|
|
Values map[string]string
|
|
}
|
|
|
|
// validHookFields contains names of the fields that can be used
|
|
var validHookFields = []string{
|
|
"hook.action",
|
|
"user.scope",
|
|
"user.locale",
|
|
"user.viewMode",
|
|
"user.singleClick",
|
|
"user.redirectAfterCopyMove",
|
|
"user.sorting.by",
|
|
"user.sorting.asc",
|
|
"user.commands",
|
|
"user.hideDotfiles",
|
|
"user.perm.admin",
|
|
"user.perm.execute",
|
|
"user.perm.create",
|
|
"user.perm.rename",
|
|
"user.perm.modify",
|
|
"user.perm.delete",
|
|
"user.perm.share",
|
|
"user.perm.download",
|
|
}
|
|
|
|
// IsValid checks if the provided field is on the valid fields list
|
|
func (hf *hookFields) IsValid(field string) bool {
|
|
return slices.Contains(validHookFields, field)
|
|
}
|
|
|
|
// GetString returns the string value or provided default
|
|
func (hf *hookFields) GetString(k, dv string) string {
|
|
val, ok := hf.Values[k]
|
|
if ok {
|
|
return val
|
|
}
|
|
return dv
|
|
}
|
|
|
|
// GetBoolean returns the bool value or provided default
|
|
func (hf *hookFields) GetBoolean(k string, dv bool) bool {
|
|
val, ok := hf.Values[k]
|
|
if ok {
|
|
return val == "true"
|
|
}
|
|
return dv
|
|
}
|
|
|
|
// GetArray returns the array value or provided default
|
|
func (hf *hookFields) GetArray(k string, dv []string) []string {
|
|
val, ok := hf.Values[k]
|
|
if ok && strings.TrimSpace(val) != "" {
|
|
return strings.Split(val, " ")
|
|
}
|
|
return dv
|
|
}
|