You've already forked filebrowser
mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-11-26 14:25:26 +08:00
feat: hook authentication method
This commit is contained in:
committed by
Oleg Lobanov
parent
f80b016ef0
commit
dda9a389f3
302
auth/hook.go
Normal file
302
auth/hook.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"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.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 {
|
||||
pass, err := users.HashPwd(a.Cred.Password)
|
||||
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,
|
||||
Sorting: a.Settings.Defaults.Sorting,
|
||||
Perm: a.Settings.Defaults.Perm,
|
||||
Commands: a.Settings.Defaults.Commands,
|
||||
HideDotfiles: a.Settings.Defaults.HideDotfiles,
|
||||
}
|
||||
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
|
||||
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
|
||||
|
||||
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 the password when it doesn't match the current
|
||||
if p {
|
||||
pass, err := users.HashPwd(a.Cred.Password)
|
||||
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),
|
||||
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),
|
||||
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.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 {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user