✨ Add commands to manage users
This commit is contained in:
		
							parent
							
								
									7901eb8eb4
								
							
						
					
					
						commit
						9cef5c63a2
					
				
							
								
								
									
										131
									
								
								cmd_user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								cmd_user.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 <organization> 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 <COPYRIGHT HOLDER> 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) | ||||||
|  | } | ||||||
| @ -30,5 +30,3 @@ type config struct { | |||||||
| 	DataDirectory string `name:"data-dir" default:"data" help:"Directory to store all files in."` | 	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."` | 	Db            string `name:"db" default:"ShareDAV.db" help:"Database file to use."` | ||||||
| } | } | ||||||
| 
 |  | ||||||
| var Config config |  | ||||||
|  | |||||||
| @ -35,11 +35,12 @@ package main | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"golang.org/x/net/webdav" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"golang.org/x/net/webdav" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type DirectoryMapping struct { | type DirectoryMapping struct { | ||||||
|  | |||||||
							
								
								
									
										157
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										157
									
								
								main.go
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | |||||||
| // Copyright (c) 2018, Andreas Schneider | // Copyright (c) 2020, Andreas Schneider | ||||||
| // All rights reserved. | // All rights reserved. | ||||||
| // | // | ||||||
| // Redistribution and use in source and binary forms, with or without | // Redistribution and use in source and binary forms, with or without | ||||||
| @ -26,84 +26,89 @@ | |||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"github.com/alecthomas/kong" | ||||||
| 	"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" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var configFile = flag.String("config", "sharedav.yaml", "Config file to be used.") | type app struct { | ||||||
| var genPassword = flag.Bool("genpass", false, "If set, a password will be generated and hashed.") | 	config | ||||||
| var hashPassword = flag.String("hashpass", "", "If set, the given password will be hashed.") | 	CmdUser CmdUser `cmd:"" name:"user" help:"Manage users."` | ||||||
|  | 
 | ||||||
|  | 	userStore UserStore | ||||||
|  | 	shareStore ShareStore | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| func main() { | func main() { | ||||||
| 	flag.Parse() | 	var app app | ||||||
|  | 	ctx := kong.Parse(&app) | ||||||
| 
 | 
 | ||||||
| 	if *genPassword { | 	store, err := NewDBStore(app.config.Db) | ||||||
| 		// math.rand is not secure. For determining the number of digits in a password | 	ctx.FatalIfErrorf(err) | ||||||
| 		// it should suffice, though. | 	defer store.Close() | ||||||
| 		rand.Seed(time.Now().UnixNano()) | 
 | ||||||
| 		pwLen := 32 | 	app.userStore = store | ||||||
| 		pw, err := pwgen.Generate(pwLen, rand.Intn(pwLen/2), 0, false, true) | 	app.shareStore = store | ||||||
| 		if err != nil { | 
 | ||||||
| 			panic(err) | 	ctx.FatalIfErrorf(ctx.Run(&app)) | ||||||
| 		} | 
 | ||||||
| 		hash, err := bcrypt.GenerateFromPassword([]byte(pw), 0) | 	//if *genPassword { | ||||||
| 		if err != nil { | 	//	// math.rand is not secure. For determining the number of digits in a password | ||||||
| 			panic(err) | 	//	// it should suffice, though. | ||||||
| 		} | 	//	rand.Seed(time.Now().UnixNano()) | ||||||
| 
 | 	//	pwLen := 32 | ||||||
| 		fmt.Printf("Password: %s\n", pw) | 	//	pw, err := pwgen.Generate(pwLen, rand.Intn(pwLen/2), 0, false, true) | ||||||
| 		fmt.Printf("    Hash: %s\n", hash) | 	//	if err != nil { | ||||||
| 		return | 	//		panic(err) | ||||||
| 	} else if *hashPassword != "" { | 	//	} | ||||||
| 		hash, err := bcrypt.GenerateFromPassword([]byte(*hashPassword), 0) | 	//	hash, err := bcrypt.GenerateFromPassword([]byte(pw), 0) | ||||||
| 		if err != nil { | 	//	if err != nil { | ||||||
| 			panic(err) | 	//		panic(err) | ||||||
| 		} | 	//	} | ||||||
| 		fmt.Println(string(hash)) | 	// | ||||||
| 		return | 	//	fmt.Printf("Password: %s\n", pw) | ||||||
| 	} | 	//	fmt.Printf("    Hash: %s\n", hash) | ||||||
| 
 | 	//	return | ||||||
| 	c := LoadConfig(*configFile) | 	//} else if *hashPassword != "" { | ||||||
| 
 | 	//	hash, err := bcrypt.GenerateFromPassword([]byte(*hashPassword), 0) | ||||||
| 	h := &webdav.Handler{} | 	//	if err != nil { | ||||||
| 	h.Prefix = "/share/" | 	//		panic(err) | ||||||
| 	h.LockSystem = webdav.NewMemLS() | 	//	} | ||||||
| 	h.FileSystem = BaseDir(c.BaseDirectory) | 	//	fmt.Println(string(hash)) | ||||||
| 
 | 	//	return | ||||||
| 	authenticatedWebdavHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 	//} | ||||||
| 		username, password, ok := r.BasicAuth() | 
 | ||||||
| 		directory := "" | 	//c := LoadConfig(*configFile) | ||||||
| 		if ok { | 	// | ||||||
| 			ok, directory = c.ValidateDAVUser(username, password) | 	//h := &webdav.Handler{} | ||||||
| 		} | 	//h.Prefix = "/share/" | ||||||
| 		if !ok { | 	//h.LockSystem = webdav.NewMemLS() | ||||||
| 			w.Header().Set("WWW-Authenticate", `Basic realm="ShareDAV"`) | 	//h.FileSystem = BaseDir(c.BaseDirectory) | ||||||
| 			http.Error(w, "unauthorized", http.StatusUnauthorized) | 	// | ||||||
| 			return | 	//authenticatedWebdavHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		} | 	//	username, password, ok := r.BasicAuth() | ||||||
| 
 | 	//	directory := "" | ||||||
| 		shareName := chi.URLParam(r, "share") | 	//	if ok { | ||||||
| 		directoryMapping := DirectoryMapping{ | 	//		ok, directory = c.ValidateDAVUser(username, password) | ||||||
| 			VirtualName: shareName, | 	//	} | ||||||
| 			RealName:    directory, | 	//	if !ok { | ||||||
| 		} | 	//		w.Header().Set("WWW-Authenticate", `Basic realm="ShareDAV"`) | ||||||
| 
 | 	//		http.Error(w, "unauthorized", http.StatusUnauthorized) | ||||||
| 		// Use the WebDAV handler to actually serve the request. Also enhance the context | 	//		return | ||||||
| 		// 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))) | 	//	shareName := chi.URLParam(r, "share") | ||||||
| 	}) | 	//	directoryMapping := DirectoryMapping{ | ||||||
| 
 | 	//		VirtualName: shareName, | ||||||
| 	r := chi.NewRouter() | 	//		RealName:    directory, | ||||||
| 	r.Handle("/share/{share}/*", authenticatedWebdavHandler) | 	//	} | ||||||
| 
 | 	// | ||||||
| 	http.ListenAndServe(c.ListenAddress, r) | 	//	// 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) | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								store.go
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								store.go
									
									
									
									
									
								
							| @ -124,7 +124,7 @@ func (store *DBStore) Close() error { | |||||||
| 	return store.db.Close() | 	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 ErrUserNotFound = errors.New("user not found") | ||||||
| var ErrInvalidUsername = errors.New("invalid username") | 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 { | 		if _, exists, err := tx.Set(user.key(), "", nil); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} else if exists { | 		} else if exists { | ||||||
| 			return ErrExists | 			return ErrUserExists | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if exists, err := store.setUserValues(tx, user); err != nil { | 		if exists, err := store.setUserValues(tx, user); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} else if exists { | 		} else if exists { | ||||||
| 			return ErrExists | 			return ErrUserExists | ||||||
| 		} | 		} | ||||||
| 		return nil | 		return nil | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user