2021-09-13 13:47:06 +00:00
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"strings"
|
|
|
|
|
2024-12-17 00:01:55 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
2025-01-21 14:02:43 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
2024-12-17 00:01:55 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/users"
|
2021-09-13 13:47:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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.
|
2024-11-21 00:15:30 +00:00
|
|
|
func (a *HookAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error) {
|
2021-09-13 13:47:06 +00:00
|
|
|
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
|
2023-12-01 23:47:00 +00:00
|
|
|
a.Settings = &settings.Config
|
|
|
|
a.Server = &settings.Config.Server
|
2021-09-13 13:47:06 +00:00
|
|
|
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.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, " ")
|
|
|
|
envMapping := func(key string) string {
|
|
|
|
switch key {
|
|
|
|
case "USERNAME":
|
|
|
|
return a.Cred.Username
|
|
|
|
case "PASSWORD":
|
|
|
|
return a.Cred.Password
|
|
|
|
default:
|
|
|
|
return os.Getenv(key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for i, arg := range command {
|
|
|
|
if i == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
command[i] = os.Expand(arg, envMapping)
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
|
|
|
|
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.Split(s, "\n") {
|
|
|
|
v := strings.SplitN(val, "=", 2) //nolint: gomnd
|
|
|
|
|
|
|
|
// skips non key and value format
|
|
|
|
if len(v) != 2 { //nolint: gomnd
|
|
|
|
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.Cred.Username)
|
|
|
|
if err != nil && err != errors.ErrNotExist {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if u == nil {
|
|
|
|
// create user with the provided credentials
|
|
|
|
d := &users.User{
|
2025-01-27 00:21:12 +00:00
|
|
|
Username: a.Cred.Username,
|
|
|
|
Password: a.Cred.Password,
|
|
|
|
Scope: a.Settings.UserDefaults.Scope,
|
|
|
|
Locale: a.Settings.UserDefaults.Locale,
|
|
|
|
ViewMode: a.Settings.UserDefaults.ViewMode,
|
|
|
|
SingleClick: a.Settings.UserDefaults.SingleClick,
|
|
|
|
Sorting: a.Settings.UserDefaults.Sorting,
|
|
|
|
Perm: a.Settings.UserDefaults.Perm,
|
|
|
|
Commands: a.Settings.UserDefaults.Commands,
|
|
|
|
ShowHidden: a.Settings.UserDefaults.ShowHidden,
|
2021-09-13 13:47:06 +00:00
|
|
|
}
|
|
|
|
u = a.GetUser(d)
|
|
|
|
|
|
|
|
userHome, err := a.Settings.MakeUserDir(u.Username, u.Scope, a.Server.Root)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
|
|
|
|
}
|
|
|
|
u.Scope = userHome
|
2025-01-21 14:02:43 +00:00
|
|
|
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", u.Username, userHome))
|
2021-09-13 13:47:06 +00:00
|
|
|
|
|
|
|
err = a.Users.Save(u)
|
|
|
|
if 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 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
|
2023-09-30 17:50:20 +00:00
|
|
|
isAdmin := d.Perm.Admin
|
2024-11-21 00:15:30 +00:00
|
|
|
perms := users.Permissions{
|
2021-09-13 13:47:06 +00:00
|
|
|
Admin: isAdmin,
|
2023-09-30 17:50:20 +00:00
|
|
|
Execute: isAdmin || d.Perm.Execute,
|
|
|
|
Create: isAdmin || d.Perm.Create,
|
|
|
|
Rename: isAdmin || d.Perm.Rename,
|
|
|
|
Modify: isAdmin || d.Perm.Modify,
|
|
|
|
Delete: isAdmin || d.Perm.Delete,
|
|
|
|
Share: isAdmin || d.Perm.Share,
|
|
|
|
Download: isAdmin || d.Perm.Download,
|
2021-09-13 13:47:06 +00:00
|
|
|
}
|
|
|
|
user := users.User{
|
|
|
|
ID: d.ID,
|
|
|
|
Username: d.Username,
|
|
|
|
Password: d.Password,
|
2023-09-30 17:50:20 +00:00
|
|
|
Scope: d.Scope,
|
|
|
|
Locale: d.Locale,
|
2023-09-02 15:52:34 +00:00
|
|
|
ViewMode: d.ViewMode,
|
2023-09-30 17:50:20 +00:00
|
|
|
SingleClick: d.SingleClick,
|
2023-12-01 23:47:00 +00:00
|
|
|
Sorting: users.Sorting{
|
2023-09-30 17:50:20 +00:00
|
|
|
Asc: d.Sorting.Asc,
|
|
|
|
By: d.Sorting.By,
|
2021-09-13 13:47:06 +00:00
|
|
|
},
|
2023-09-30 17:50:20 +00:00
|
|
|
Commands: d.Commands,
|
2025-01-27 00:21:12 +00:00
|
|
|
ShowHidden: d.ShowHidden,
|
2021-09-13 13:47:06 +00:00
|
|
|
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.sorting.by",
|
|
|
|
"user.sorting.asc",
|
|
|
|
"user.commands",
|
2025-01-27 00:21:12 +00:00
|
|
|
"user.showHidden",
|
2021-09-13 13:47:06 +00:00
|
|
|
"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 {
|
|
|
|
for _, val := range validHookFields {
|
|
|
|
if field == val {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|