diff --git a/cmd_user.go b/cmd_user.go new file mode 100644 index 0000000..4b628f5 --- /dev/null +++ b/cmd_user.go @@ -0,0 +1,131 @@ +// Copyright (c) 2020, Andreas Schneider +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the nor the +// names of its contributors may be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +package main + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +type CmdUser struct { + CmdList CmdUserList `cmd:"" name:"list" help:"List all users."` + CmdAdd CmdUserAdd `cmd:"" name:"add" help:"Add a user."` + CmdUpdate CmdUserUpdate `cmd:"" name:"update" help:"Update a user."` + CmdDelete CmdUserDelete `cmd:"" name:"delete" help:"Delete a user."` +} + +type CmdUserList struct{} + +func (cmd CmdUserList) Run(app *app) error { + users, err := app.userStore.GetUsers() + if err != nil { + return err + } + + for _, user := range users { + fmt.Printf("* %s (%s)\n", user.Username, user.Role) + } + + return nil +} + +type CmdUserAdd struct { + Username string `arg:"" name:"username" help:"The username to be added."` + Password string `name:"password" help:"The password of the new user."` + Role GlobalRole `name:"role" default:"user" help:"Role of the user. 'admin' or ' user'"` +} + +func (cmd CmdUserAdd) Run(app *app) error { + switch cmd.Role { + case GlobalRoleUser: + case GlobalRoleAdmin: + default: + return fmt.Errorf("invalid user role") + } + + hash, err := bcrypt.GenerateFromPassword([]byte(cmd.Password), 0) + if err != nil { + return fmt.Errorf("cannot hash password: %w", err) + } + + user := User{ + Username: cmd.Username, + Password: string(hash), + Role: cmd.Role, + } + return app.userStore.AddUser(user) +} + +type CmdUserUpdate struct { + Username string `arg:"" name:"username" help:"The username of the user to be updated."` + Password string `name:"password" help:"Update the password, if set."` + Role GlobalRole `name:"role" default:"user" help:"Update the role, if set. 'admin' or ' user'"` +} + +func (cmd CmdUserUpdate) Run(app *app) error { + user, err := app.userStore.GetUser(cmd.Username) + if err != nil { + return err + } + + changed := false + + if cmd.Role != "" && cmd.Role != user.Role { + switch cmd.Role { + case GlobalRoleUser: + case GlobalRoleAdmin: + default: + return fmt.Errorf("invalid user role") + } + + user.Role = cmd.Role + changed = true + } + + if cmd.Password != "" { + hash, err := bcrypt.GenerateFromPassword([]byte(cmd.Password), 0) + if err != nil { + return fmt.Errorf("cannot hash password: %w", err) + } + user.Password = string(hash) + changed = true + } + + if !changed { + // Nothing changed. Nothing to write. Not different from a successful write to the user. + return nil + } + + return app.userStore.Update(user) +} + +type CmdUserDelete struct { + Username string `arg:"" name:"username" help:"The username of the user to be deleted."` +} + +func (cmd CmdUserDelete) Run(app *app) error { + return app.userStore.RemoveUser(cmd.Username) +} diff --git a/config.go b/config.go index 591caaa..b7f7674 100644 --- a/config.go +++ b/config.go @@ -30,5 +30,3 @@ type config struct { DataDirectory string `name:"data-dir" default:"data" help:"Directory to store all files in."` Db string `name:"db" default:"ShareDAV.db" help:"Database file to use."` } - -var Config config diff --git a/filesystem.go b/filesystem.go index eec90eb..d3f1edb 100644 --- a/filesystem.go +++ b/filesystem.go @@ -35,11 +35,12 @@ package main import ( "context" - "golang.org/x/net/webdav" "os" "path" "path/filepath" "strings" + + "golang.org/x/net/webdav" ) type DirectoryMapping struct { diff --git a/main.go b/main.go index a8768b1..5639d40 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018, Andreas Schneider +// Copyright (c) 2020, Andreas Schneider // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -26,84 +26,89 @@ package main import ( - "context" - "flag" - "fmt" - "github.com/go-chi/chi" - pwgen "github.com/sethvargo/go-password/password" - "golang.org/x/crypto/bcrypt" - "golang.org/x/net/webdav" - "math/rand" - "net/http" - "time" + "github.com/alecthomas/kong" ) -var configFile = flag.String("config", "sharedav.yaml", "Config file to be used.") -var genPassword = flag.Bool("genpass", false, "If set, a password will be generated and hashed.") -var hashPassword = flag.String("hashpass", "", "If set, the given password will be hashed.") +type app struct { + config + CmdUser CmdUser `cmd:"" name:"user" help:"Manage users."` + + userStore UserStore + shareStore ShareStore +} func main() { - flag.Parse() + var app app + ctx := kong.Parse(&app) - if *genPassword { - // math.rand is not secure. For determining the number of digits in a password - // it should suffice, though. - rand.Seed(time.Now().UnixNano()) - pwLen := 32 - pw, err := pwgen.Generate(pwLen, rand.Intn(pwLen/2), 0, false, true) - if err != nil { - panic(err) - } - hash, err := bcrypt.GenerateFromPassword([]byte(pw), 0) - if err != nil { - panic(err) - } + store, err := NewDBStore(app.config.Db) + ctx.FatalIfErrorf(err) + defer store.Close() - fmt.Printf("Password: %s\n", pw) - fmt.Printf(" Hash: %s\n", hash) - return - } else if *hashPassword != "" { - hash, err := bcrypt.GenerateFromPassword([]byte(*hashPassword), 0) - if err != nil { - panic(err) - } - fmt.Println(string(hash)) - return - } + app.userStore = store + app.shareStore = store - c := LoadConfig(*configFile) + ctx.FatalIfErrorf(ctx.Run(&app)) - h := &webdav.Handler{} - h.Prefix = "/share/" - h.LockSystem = webdav.NewMemLS() - h.FileSystem = BaseDir(c.BaseDirectory) + //if *genPassword { + // // math.rand is not secure. For determining the number of digits in a password + // // it should suffice, though. + // rand.Seed(time.Now().UnixNano()) + // pwLen := 32 + // pw, err := pwgen.Generate(pwLen, rand.Intn(pwLen/2), 0, false, true) + // if err != nil { + // panic(err) + // } + // hash, err := bcrypt.GenerateFromPassword([]byte(pw), 0) + // if err != nil { + // panic(err) + // } + // + // fmt.Printf("Password: %s\n", pw) + // fmt.Printf(" Hash: %s\n", hash) + // return + //} else if *hashPassword != "" { + // hash, err := bcrypt.GenerateFromPassword([]byte(*hashPassword), 0) + // if err != nil { + // panic(err) + // } + // fmt.Println(string(hash)) + // return + //} - authenticatedWebdavHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - directory := "" - if ok { - ok, directory = c.ValidateDAVUser(username, password) - } - if !ok { - w.Header().Set("WWW-Authenticate", `Basic realm="ShareDAV"`) - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - - shareName := chi.URLParam(r, "share") - directoryMapping := DirectoryMapping{ - VirtualName: shareName, - RealName: directory, - } - - // Use the WebDAV handler to actually serve the request. Also enhance the context - // to contain the subdirectory (of the base directory) which contains the data for - // the authenticated user. - h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "mapping", &directoryMapping))) - }) - - r := chi.NewRouter() - r.Handle("/share/{share}/*", authenticatedWebdavHandler) - - http.ListenAndServe(c.ListenAddress, r) + //c := LoadConfig(*configFile) + // + //h := &webdav.Handler{} + //h.Prefix = "/share/" + //h.LockSystem = webdav.NewMemLS() + //h.FileSystem = BaseDir(c.BaseDirectory) + // + //authenticatedWebdavHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // username, password, ok := r.BasicAuth() + // directory := "" + // if ok { + // ok, directory = c.ValidateDAVUser(username, password) + // } + // if !ok { + // w.Header().Set("WWW-Authenticate", `Basic realm="ShareDAV"`) + // http.Error(w, "unauthorized", http.StatusUnauthorized) + // return + // } + // + // shareName := chi.URLParam(r, "share") + // directoryMapping := DirectoryMapping{ + // VirtualName: shareName, + // RealName: directory, + // } + // + // // Use the WebDAV handler to actually serve the request. Also enhance the context + // // to contain the subdirectory (of the base directory) which contains the data for + // // the authenticated user. + // h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "mapping", &directoryMapping))) + //}) + // + //r := chi.NewRouter() + //r.Handle("/share/{share}/*", authenticatedWebdavHandler) + // + //http.ListenAndServe(c.ListenAddress, r) } diff --git a/store.go b/store.go index 5004104..3aa562b 100644 --- a/store.go +++ b/store.go @@ -124,7 +124,7 @@ func (store *DBStore) Close() error { return store.db.Close() } -var ErrExists = errors.New("key already exists") +var ErrUserExists = errors.New("user already exists") var ErrUserNotFound = errors.New("user not found") var ErrInvalidUsername = errors.New("invalid username") @@ -156,13 +156,13 @@ func (store *DBStore) AddUser(user User) (err error) { if _, exists, err := tx.Set(user.key(), "", nil); err != nil { return err } else if exists { - return ErrExists + return ErrUserExists } if exists, err := store.setUserValues(tx, user); err != nil { return err } else if exists { - return ErrExists + return ErrUserExists } return nil }); err != nil {