diff --git a/auth/json.go b/auth/json.go index f1d6b2ad..8ad9fa47 100644 --- a/auth/json.go +++ b/auth/json.go @@ -22,11 +22,11 @@ type jsonCred struct { // JSONAuth is a json implementaion of an Auther. type JSONAuth struct { - ReCaptcha *ReCaptcha + ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"` } // 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, root string) (*users.User, error) { var cred jsonCred if r.Body == nil { diff --git a/auth/none.go b/auth/none.go index 0279c4e8..f10d065a 100644 --- a/auth/none.go +++ b/auth/none.go @@ -14,6 +14,6 @@ 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, root string) (*users.User, error) { return sto.Get(root, 1) } diff --git a/auth/proxy.go b/auth/proxy.go index dc73fb5b..176b69c5 100644 --- a/auth/proxy.go +++ b/auth/proxy.go @@ -14,11 +14,11 @@ const MethodProxyAuth settings.AuthMethod = "proxy" // ProxyAuth is a proxy implementation of an auther. type ProxyAuth struct { - Header string + Header string `json:"header"` } // 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, root string) (*users.User, error) { username := r.Header.Get(a.Header) user, err := sto.Get(root, username) if err == errors.ErrNotExist { diff --git a/cmd/config_export.go b/cmd/config_export.go new file mode 100644 index 00000000..f85d89b6 --- /dev/null +++ b/cmd/config_export.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func init() { + configCmd.AddCommand(configExportCmd) +} + +var configExportCmd = &cobra.Command{ + Use: "export ", + Short: "Export the configuration to a file.", + Args: jsonYamlArg, + Run: python(func(cmd *cobra.Command, args []string, d pythonData) { + settings, err := d.store.Settings.Get() + checkErr(err) + + auther, err := d.store.Auth.Get(settings.AuthMethod) + checkErr(err) + + data := &settingsFile{ + Settings: settings, + Auther: auther, + } + + err = marshal(args[0], data) + checkErr(err) + }, pythonConfig{}), +} diff --git a/cmd/config_import.go b/cmd/config_import.go new file mode 100644 index 00000000..c8b8ec0f --- /dev/null +++ b/cmd/config_import.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "encoding/json" + "errors" + "reflect" + + "github.com/filebrowser/filebrowser/v2/auth" + "github.com/filebrowser/filebrowser/v2/settings" + "github.com/spf13/cobra" +) + +func init() { + configCmd.AddCommand(configImportCmd) +} + +type settingsFile struct { + Settings *settings.Settings `json:"settings"` + Auther interface{} `json:"auther"` +} + +var configImportCmd = &cobra.Command{ + Use: "import ", + Short: `Import a configuration file. This will replace all the existing +configuration. Can be used with or without unexisting databases. +If used with a nonexisting database, a key will be generated +automatically. Otherwise the key will be kept the same as in the +database.`, + Args: jsonYamlArg, + Run: python(func(cmd *cobra.Command, args []string, d pythonData) { + var key []byte + if d.hadDB { + settings, err := d.store.Settings.Get() + checkErr(err) + key = settings.Key + } else { + key = generateRandomBytes(64) + } + + file := settingsFile{} + err := unmarshal(args[0], &file) + checkErr(err) + + file.Settings.Key = key + err = d.store.Settings.Save(file.Settings) + checkErr(err) + + autherInterf := cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{})) + + var auther auth.Auther + switch file.Settings.AuthMethod { + case auth.MethodJSONAuth: + auther = getAuther(auth.JSONAuth{}, autherInterf).(*auth.JSONAuth) + case auth.MethodNoAuth: + auther = getAuther(auth.NoAuth{}, autherInterf).(*auth.NoAuth) + case auth.MethodProxyAuth: + auther = getAuther(auth.ProxyAuth{}, autherInterf).(*auth.ProxyAuth) + default: + checkErr(errors.New("invalid auth method")) + } + + err = d.store.Auth.Save(auther) + checkErr(err) + printSettings(file.Settings, auther) + }, pythonConfig{allowNoDB: true}), +} + +func getAuther(sample auth.Auther, data interface{}) interface{} { + authType := reflect.TypeOf(sample) + auther := reflect.New(authType).Interface() + bytes, err := json.Marshal(data) + checkErr(err) + err = json.Unmarshal(bytes, &auther) + checkErr(err) + return auther +} diff --git a/cmd/users_export.go b/cmd/users_export.go new file mode 100644 index 00000000..711cd420 --- /dev/null +++ b/cmd/users_export.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func init() { + usersCmd.AddCommand(usersExportCmd) +} + +var usersExportCmd = &cobra.Command{ + Use: "export ", + Short: "Export all users.", + Args: jsonYamlArg, + Run: python(func(cmd *cobra.Command, args []string, d pythonData) { + list, err := d.store.Users.Gets("") + checkErr(err) + + err = marshal(args[0], list) + checkErr(err) + }, pythonConfig{}), +} diff --git a/cmd/users_import.go b/cmd/users_import.go new file mode 100644 index 00000000..a9af4d0f --- /dev/null +++ b/cmd/users_import.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "errors" + "os" + "strconv" + + "github.com/filebrowser/filebrowser/v2/users" + "github.com/spf13/cobra" +) + +func init() { + usersCmd.AddCommand(usersImportCmd) + usersImportCmd.Flags().Bool("overwrite", false, "overwrite users with the same id/username combo") +} + +var usersImportCmd = &cobra.Command{ + Use: "import ", + Short: "Import users from a file.", + Args: jsonYamlArg, + Run: python(func(cmd *cobra.Command, args []string, d pythonData) { + fd, err := os.Open(args[0]) + checkErr(err) + defer fd.Close() + + list := []*users.User{} + err = unmarshal(args[0], &list) + checkErr(err) + + for _, user := range list { + err = user.Clean("") + checkErr(err) + } + + overwrite := mustGetBool(cmd, "overwrite") + + for _, user := range list { + old, err := d.store.Users.Get("", user.ID) + + // User exists in DB. + if err == nil { + if !overwrite { + checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred")) + } + + // If the usernames mismatch, check if there is another one in the DB + // with the new username. If there is, print an error and cancel the + // operation + if user.Username != old.Username { + conflictuous, err := d.store.Users.Get("", user.Username) + if err == nil { + checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID)) + } + } + } + + err = d.store.Users.Save(user) + checkErr(err) + } + }, pythonConfig{}), +} + +func usernameConflictError(username string, original, new uint) error { + return errors.New("can't import user with ID " + strconv.Itoa(int(new)) + " and username \"" + username + "\" because the username is already registred with the user " + strconv.Itoa(int(original))) +} diff --git a/cmd/utils.go b/cmd/utils.go index 8eac6407..d1e24dc2 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -2,8 +2,12 @@ package cmd import ( "crypto/rand" + "encoding/json" + "errors" + "fmt" "log" "os" + "path/filepath" "github.com/asdine/storm" "github.com/filebrowser/filebrowser/v2/storage" @@ -11,6 +15,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" v "github.com/spf13/viper" + yaml "gopkg.in/yaml.v2" ) func vaddP(f *pflag.FlagSet, k, p string, i interface{}, u string) { @@ -39,7 +44,8 @@ func vadd(f *pflag.FlagSet, k string, i interface{}, u string) { func checkErr(err error) { if err != nil { - panic(err) + fmt.Println(err) + os.Exit(1) } } @@ -108,3 +114,76 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc { fn(cmd, args, data) } } + +func marshal(filename string, data interface{}) error { + fd, err := os.Create(filename) + checkErr(err) + defer fd.Close() + + switch ext := filepath.Ext(filename); ext { + case ".json": + encoder := json.NewEncoder(fd) + encoder.SetIndent("", " ") + return encoder.Encode(data) + case ".yml", ".yaml": + encoder := yaml.NewEncoder(fd) + return encoder.Encode(data) + default: + return errors.New("invalid format: " + ext) + } +} + +func unmarshal(filename string, data interface{}) error { + fd, err := os.Open(filename) + checkErr(err) + defer fd.Close() + + switch ext := filepath.Ext(filename); ext { + case ".json": + return json.NewDecoder(fd).Decode(data) + case ".yml", ".yaml": + return yaml.NewDecoder(fd).Decode(data) + default: + return errors.New("invalid format: " + ext) + } +} + +func jsonYamlArg(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + + switch ext := filepath.Ext(args[0]); ext { + case ".json", ".yml", ".yaml": + return nil + default: + return errors.New("invalid format: " + ext) + } +} + +func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for k, v := range in { + result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v) + } + return result +} + +func cleanUpInterfaceArray(in []interface{}) []interface{} { + result := make([]interface{}, len(in)) + for i, v := range in { + result[i] = cleanUpMapValue(v) + } + return result +} + +func cleanUpMapValue(v interface{}) interface{} { + switch v := v.(type) { + case []interface{}: + return cleanUpInterfaceArray(v) + case map[interface{}]interface{}: + return cleanUpInterfaceMap(v) + default: + return v + } +} diff --git a/users/users.go b/users/users.go index 387a4eaa..4fdbef6c 100644 --- a/users/users.go +++ b/users/users.go @@ -31,7 +31,7 @@ type User struct { Perm Permissions `json:"perm"` Commands []string `json:"commands"` Sorting files.Sorting `json:"sorting"` - Fs afero.Fs `json:"-"` + Fs afero.Fs `json:"-" yaml:"-"` Rules []rules.Rule `json:"rules"` }