More auth
This commit is contained in:
		
							parent
							
								
									393f95aeac
								
							
						
					
					
						commit
						460162737a
					
				
					 9 changed files with 264 additions and 230 deletions
				
			
		
							
								
								
									
										20
									
								
								auth/auth.go
									
										
									
									
									
								
							
							
						
						
									
										20
									
								
								auth/auth.go
									
										
									
									
									
								
							|  | @ -15,6 +15,7 @@ type Manager interface { | ||||||
| 	User(username string) (*User, error) | 	User(username string) (*User, error) | ||||||
| 	ChangePassword(username, password string) error | 	ChangePassword(username, password string) error | ||||||
| 	ChangeRole(username string, role Role) error | 	ChangeRole(username string, role Role) error | ||||||
|  | 	DefaultAccess() (read bool, write bool) | ||||||
| 	AllowAccess(username string, topic string, read bool, write bool) error | 	AllowAccess(username string, topic string, read bool, write bool) error | ||||||
| 	ResetAccess(username string, topic string) error | 	ResetAccess(username string, topic string) error | ||||||
| } | } | ||||||
|  | @ -42,21 +43,14 @@ const ( | ||||||
| type Role string | type Role string | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	RoleAdmin = Role("admin") | 	RoleAdmin     = Role("admin") | ||||||
| 	RoleUser  = Role("user") | 	RoleUser      = Role("user") | ||||||
| 	RoleNone  = Role("none") | 	RoleAnonymous = Role("anonymous") | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var Everyone = &User{ | const ( | ||||||
| 	Name: "", | 	Everyone = "*" | ||||||
| 	Role: RoleNone, | ) | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var Roles = []Role{ |  | ||||||
| 	RoleAdmin, |  | ||||||
| 	RoleUser, |  | ||||||
| 	RoleNone, |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| func AllowedRole(role Role) bool { | func AllowedRole(role Role) bool { | ||||||
| 	return role == RoleUser || role == RoleAdmin | 	return role == RoleUser || role == RoleAdmin | ||||||
|  |  | ||||||
|  | @ -21,10 +21,6 @@ INSERT INTO access VALUES ('','write-all',1,1); | ||||||
| 
 | 
 | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| const ( |  | ||||||
| 	bcryptCost = 11 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Auther-related queries | // Auther-related queries | ||||||
| const ( | const ( | ||||||
| 	createAuthTablesQueries = ` | 	createAuthTablesQueries = ` | ||||||
|  | @ -51,26 +47,28 @@ const ( | ||||||
| 	selectTopicPermsQuery = ` | 	selectTopicPermsQuery = ` | ||||||
| 		SELECT read, write  | 		SELECT read, write  | ||||||
| 		FROM access  | 		FROM access  | ||||||
| 		WHERE user IN ('', ?) AND topic = ? | 		WHERE user IN ('*', ?) AND topic = ? | ||||||
| 		ORDER BY user DESC | 		ORDER BY user DESC | ||||||
| 	` | 	` | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Manager-related queries | // Manager-related queries | ||||||
| const ( | const ( | ||||||
| 	insertUserQuery           = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)` | 	insertUserQuery      = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)` | ||||||
| 	selectUsernamesQuery      = `SELECT user FROM user ORDER BY role, user` | 	selectUsernamesQuery = `SELECT user FROM user ORDER BY role, user` | ||||||
| 	selectUserTopicPermsQuery = `SELECT topic, read, write FROM access WHERE user = ?` | 	updateUserPassQuery  = `UPDATE user SET pass = ? WHERE user = ?` | ||||||
| 	updateUserPassQuery       = `UPDATE user SET pass = ? WHERE user = ?` | 	updateUserRoleQuery  = `UPDATE user SET role = ? WHERE user = ?` | ||||||
| 	updateUserRoleQuery       = `UPDATE user SET role = ? WHERE user = ?` | 	deleteUserQuery      = `DELETE FROM user WHERE user = ?` | ||||||
| 	upsertAccessQuery         = ` | 
 | ||||||
|  | 	upsertUserAccessQuery = ` | ||||||
| 		INSERT INTO access (user, topic, read, write)  | 		INSERT INTO access (user, topic, read, write)  | ||||||
| 		VALUES (?, ?, ?, ?) | 		VALUES (?, ?, ?, ?) | ||||||
| 		ON CONFLICT (user, topic) DO UPDATE SET read=excluded.read, write=excluded.write | 		ON CONFLICT (user, topic) DO UPDATE SET read=excluded.read, write=excluded.write | ||||||
| 	` | 	` | ||||||
| 	deleteUserQuery      = `DELETE FROM user WHERE user = ?` | 	selectUserAccessQuery  = `SELECT topic, read, write FROM access WHERE user = ?` | ||||||
| 	deleteAllAccessQuery = `DELETE FROM access WHERE user = ?` | 	deleteAllAccessQuery   = `DELETE FROM access` | ||||||
| 	deleteAccessQuery    = `DELETE FROM access WHERE user = ? AND topic = ?` | 	deleteUserAccessQuery  = `DELETE FROM access WHERE user = ?` | ||||||
|  | 	deleteTopicAccessQuery = `DELETE FROM access WHERE user = ? AND topic = ?` | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type SQLiteAuth struct { | type SQLiteAuth struct { | ||||||
|  | @ -106,6 +104,9 @@ func setupNewAuthDB(db *sql.DB) error { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) { | func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) { | ||||||
|  | 	if username == Everyone { | ||||||
|  | 		return nil, ErrUnauthorized | ||||||
|  | 	} | ||||||
| 	rows, err := a.db.Query(selectUserQuery, username) | 	rows, err := a.db.Query(selectUserQuery, username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -135,7 +136,7 @@ func (a *SQLiteAuth) Authorize(user *User, topic string, perm Permission) error | ||||||
| 	// Select the read/write permissions for this user/topic combo. The query may return two | 	// Select the read/write permissions for this user/topic combo. The query may return two | ||||||
| 	// rows (one for everyone, and one for the user), but prioritizes the user. The value for | 	// rows (one for everyone, and one for the user), but prioritizes the user. The value for | ||||||
| 	// user.Name may be empty (= everyone). | 	// user.Name may be empty (= everyone). | ||||||
| 	var username string | 	username := Everyone | ||||||
| 	if user != nil { | 	if user != nil { | ||||||
| 		username = user.Name | 		username = user.Name | ||||||
| 	} | 	} | ||||||
|  | @ -166,7 +167,7 @@ func (a *SQLiteAuth) resolvePerms(read, write bool, perm Permission) error { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *SQLiteAuth) AddUser(username, password string, role Role) error { | func (a *SQLiteAuth) AddUser(username, password string, role Role) error { | ||||||
| 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) | 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -180,7 +181,7 @@ func (a *SQLiteAuth) RemoveUser(username string) error { | ||||||
| 	if _, err := a.db.Exec(deleteUserQuery, username); err != nil { | 	if _, err := a.db.Exec(deleteUserQuery, username); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil { | 	if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
|  | @ -211,10 +212,18 @@ func (a *SQLiteAuth) Users() ([]*User, error) { | ||||||
| 		} | 		} | ||||||
| 		users = append(users, user) | 		users = append(users, user) | ||||||
| 	} | 	} | ||||||
|  | 	everyone, err := a.everyoneUser() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	users = append(users, everyone) | ||||||
| 	return users, nil | 	return users, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *SQLiteAuth) User(username string) (*User, error) { | func (a *SQLiteAuth) User(username string) (*User, error) { | ||||||
|  | 	if username == Everyone { | ||||||
|  | 		return a.everyoneUser() | ||||||
|  | 	} | ||||||
| 	urows, err := a.db.Query(selectUserQuery, username) | 	urows, err := a.db.Query(selectUserQuery, username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -229,26 +238,10 @@ func (a *SQLiteAuth) User(username string) (*User, error) { | ||||||
| 	} else if err := urows.Err(); err != nil { | 	} else if err := urows.Err(); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	arows, err := a.db.Query(selectUserTopicPermsQuery, username) | 	grants, err := a.readGrants(username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	defer arows.Close() |  | ||||||
| 	grants := make([]Grant, 0) |  | ||||||
| 	for arows.Next() { |  | ||||||
| 		var topic string |  | ||||||
| 		var read, write bool |  | ||||||
| 		if err := arows.Scan(&topic, &read, &write); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} else if err := arows.Err(); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		grants = append(grants, Grant{ |  | ||||||
| 			Topic: topic, |  | ||||||
| 			Read:  read, |  | ||||||
| 			Write: write, |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 	return &User{ | 	return &User{ | ||||||
| 		Name:   username, | 		Name:   username, | ||||||
| 		Pass:   hash, | 		Pass:   hash, | ||||||
|  | @ -257,8 +250,45 @@ func (a *SQLiteAuth) User(username string) (*User, error) { | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (a *SQLiteAuth) everyoneUser() (*User, error) { | ||||||
|  | 	grants, err := a.readGrants(Everyone) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return &User{ | ||||||
|  | 		Name:   Everyone, | ||||||
|  | 		Pass:   "", | ||||||
|  | 		Role:   RoleAnonymous, | ||||||
|  | 		Grants: grants, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *SQLiteAuth) readGrants(username string) ([]Grant, error) { | ||||||
|  | 	rows, err := a.db.Query(selectUserAccessQuery, username) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer rows.Close() | ||||||
|  | 	grants := make([]Grant, 0) | ||||||
|  | 	for rows.Next() { | ||||||
|  | 		var topic string | ||||||
|  | 		var read, write bool | ||||||
|  | 		if err := rows.Scan(&topic, &read, &write); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} else if err := rows.Err(); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		grants = append(grants, Grant{ | ||||||
|  | 			Topic: topic, | ||||||
|  | 			Read:  read, | ||||||
|  | 			Write: write, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	return grants, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (a *SQLiteAuth) ChangePassword(username, password string) error { | func (a *SQLiteAuth) ChangePassword(username, password string) error { | ||||||
| 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) | 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -273,29 +303,32 @@ func (a *SQLiteAuth) ChangeRole(username string, role Role) error { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if role == RoleAdmin { | 	if role == RoleAdmin { | ||||||
| 		if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil { | 		if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (a *SQLiteAuth) DefaultAccess() (read bool, write bool) { | ||||||
|  | 	return a.defaultRead, a.defaultWrite | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (a *SQLiteAuth) AllowAccess(username string, topic string, read bool, write bool) error { | func (a *SQLiteAuth) AllowAccess(username string, topic string, read bool, write bool) error { | ||||||
| 	if _, err := a.db.Exec(upsertAccessQuery, username, topic, read, write); err != nil { | 	if _, err := a.db.Exec(upsertUserAccessQuery, username, topic, read, write); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *SQLiteAuth) ResetAccess(username string, topic string) error { | func (a *SQLiteAuth) ResetAccess(username string, topic string) error { | ||||||
| 	if topic == "" { | 	if username == "" && topic == "" { | ||||||
| 		if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil { | 		_, err := a.db.Exec(deleteAllAccessQuery, username) | ||||||
| 			return err | 		return err | ||||||
| 		} | 	} else if topic == "" { | ||||||
| 	} else { | 		_, err := a.db.Exec(deleteUserAccessQuery, username) | ||||||
| 		if _, err := a.db.Exec(deleteAccessQuery, username, topic); err != nil { | 		return err | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	return nil | 	_, err := a.db.Exec(deleteTopicAccessQuery, username, topic) | ||||||
|  | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										174
									
								
								cmd/access.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								cmd/access.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,174 @@ | ||||||
|  | package cmd | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/urfave/cli/v2" | ||||||
|  | 	"heckel.io/ntfy/auth" | ||||||
|  | 	"heckel.io/ntfy/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 
 | ||||||
|  | ntfy access                        # Shows access control list | ||||||
|  | ntfy access phil                   # Shows access for user phil | ||||||
|  | ntfy access phil mytopic           # Shows access for user phil and topic mytopic | ||||||
|  | ntfy access phil mytopic rw        # Allow read-write access to mytopic for user phil | ||||||
|  | ntfy access everyone mytopic rw    # Allow anonymous read-write access to mytopic | ||||||
|  | ntfy access --reset                # Reset entire access control list | ||||||
|  | ntfy access --reset phil           # Reset all access for user phil | ||||||
|  | ntfy access --reset phil mytopic   # Reset access for user phil and topic mytopic | ||||||
|  | 
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	userEveryone = "everyone" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var flagsAccess = append( | ||||||
|  | 	userCommandFlags(), | ||||||
|  | 	&cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"}, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var cmdAccess = &cli.Command{ | ||||||
|  | 	Name:      "access", | ||||||
|  | 	Usage:     "Grant/revoke access to a topic, or show access", | ||||||
|  | 	UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]", | ||||||
|  | 	Flags:     flagsAccess, | ||||||
|  | 	Before:    initConfigFileInputSource("config", flagsAccess), | ||||||
|  | 	Action:    execUserAccess, | ||||||
|  | 	Category:  categoryServer, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func execUserAccess(c *cli.Context) error { | ||||||
|  | 	manager, err := createAuthManager(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	username := c.Args().Get(0) | ||||||
|  | 	if username == userEveryone { | ||||||
|  | 		username = auth.Everyone | ||||||
|  | 	} | ||||||
|  | 	topic := c.Args().Get(1) | ||||||
|  | 	perms := c.Args().Get(2) | ||||||
|  | 	reset := c.Bool("reset") | ||||||
|  | 	if reset { | ||||||
|  | 		return resetAccess(c, manager, username, topic) | ||||||
|  | 	} else if perms == "" { | ||||||
|  | 		return showAccess(c, manager, username) | ||||||
|  | 	} | ||||||
|  | 	return changeAccess(c, manager, username, topic, perms) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func changeAccess(c *cli.Context, manager auth.Manager, username string, topic string, perms string) error { | ||||||
|  | 	if !util.InStringList([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) { | ||||||
|  | 		return errors.New("permission must be one of: read-write, read-only, write-only, or deny (or the aliases: read, ro, write, wo, none)") | ||||||
|  | 	} | ||||||
|  | 	read := util.InStringList([]string{"read-write", "rw", "read-only", "read", "ro"}, perms) | ||||||
|  | 	write := util.InStringList([]string{"read-write", "rw", "write-only", "write", "wo"}, perms) | ||||||
|  | 	if err := manager.AllowAccess(username, topic, read, write); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if read && write { | ||||||
|  | 		fmt.Fprintf(c.App.Writer, "Granted read-write access to topic %s\n\n", topic) | ||||||
|  | 	} else if read { | ||||||
|  | 		fmt.Fprintf(c.App.Writer, "Granted read-only access to topic %s\n\n", topic) | ||||||
|  | 	} else if write { | ||||||
|  | 		fmt.Fprintf(c.App.Writer, "Granted write-only access to topic %s\n\n", topic) | ||||||
|  | 	} else { | ||||||
|  | 		fmt.Fprintf(c.App.Writer, "Revoked all access to topic %s\n\n", topic) | ||||||
|  | 	} | ||||||
|  | 	return showUserAccess(c, manager, username) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func resetAccess(c *cli.Context, manager auth.Manager, username, topic string) error { | ||||||
|  | 	if username == "" { | ||||||
|  | 		return resetAllAccess(c, manager) | ||||||
|  | 	} else if topic == "" { | ||||||
|  | 		return resetUserAccess(c, manager, username) | ||||||
|  | 	} | ||||||
|  | 	return resetUserTopicAccess(c, manager, username, topic) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func resetAllAccess(c *cli.Context, manager auth.Manager) error { | ||||||
|  | 	if err := manager.ResetAccess("", ""); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	fmt.Fprintln(c.App.Writer, "Reset access for all users") | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func resetUserAccess(c *cli.Context, manager auth.Manager, username string) error { | ||||||
|  | 	if err := manager.ResetAccess(username, ""); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	fmt.Fprintf(c.App.Writer, "Reset access for user %s\n\n", username) | ||||||
|  | 	return showUserAccess(c, manager, username) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func resetUserTopicAccess(c *cli.Context, manager auth.Manager, username string, topic string) error { | ||||||
|  | 	if err := manager.ResetAccess(username, topic); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	fmt.Fprintf(c.App.Writer, "Reset access for user %s and topic %s\n\n", username, topic) | ||||||
|  | 	return showUserAccess(c, manager, username) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func showAccess(c *cli.Context, manager auth.Manager, username string) error { | ||||||
|  | 	if username == "" { | ||||||
|  | 		return showAllAccess(c, manager) | ||||||
|  | 	} | ||||||
|  | 	return showUserAccess(c, manager, username) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func showAllAccess(c *cli.Context, manager auth.Manager) error { | ||||||
|  | 	users, err := manager.Users() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return showUsers(c, manager, users) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func showUserAccess(c *cli.Context, manager auth.Manager, username string) error { | ||||||
|  | 	users, err := manager.User(username) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return showUsers(c, manager, []*auth.User{users}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func showUsers(c *cli.Context, manager auth.Manager, users []*auth.User) error { | ||||||
|  | 	for _, user := range users { | ||||||
|  | 		fmt.Fprintf(c.App.Writer, "User %s (%s)\n", user.Name, user.Role) | ||||||
|  | 		if user.Role == auth.RoleAdmin { | ||||||
|  | 			fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n") | ||||||
|  | 		} else if len(user.Grants) > 0 { | ||||||
|  | 			for _, grant := range user.Grants { | ||||||
|  | 				if grant.Read && grant.Write { | ||||||
|  | 					fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.Topic) | ||||||
|  | 				} else if grant.Read { | ||||||
|  | 					fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.Topic) | ||||||
|  | 				} else if grant.Write { | ||||||
|  | 					fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.Topic) | ||||||
|  | 				} else { | ||||||
|  | 					fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.Topic) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n") | ||||||
|  | 		} | ||||||
|  | 		if user.Name == auth.Everyone { | ||||||
|  | 			defaultRead, defaultWrite := manager.DefaultAccess() | ||||||
|  | 			if defaultRead && defaultWrite { | ||||||
|  | 				fmt.Fprintln(c.App.ErrWriter, "- read-write access to all (other) topics (server config)") | ||||||
|  | 			} else if defaultRead { | ||||||
|  | 				fmt.Fprintln(c.App.ErrWriter, "- read-only access to all (other) topics (server config)") | ||||||
|  | 			} else if defaultWrite { | ||||||
|  | 				fmt.Fprintln(c.App.ErrWriter, "- write-only access to all (other) topics (server config)") | ||||||
|  | 			} else { | ||||||
|  | 				fmt.Fprintln(c.App.ErrWriter, "- no access to any (other) topics (server config)") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -37,8 +37,7 @@ func New() *cli.App { | ||||||
| 			// Server commands | 			// Server commands | ||||||
| 			cmdServe, | 			cmdServe, | ||||||
| 			cmdUser, | 			cmdUser, | ||||||
| 			cmdAllow, | 			cmdAccess, | ||||||
| 			cmdDeny, |  | ||||||
| 
 | 
 | ||||||
| 			// Client commands | 			// Client commands | ||||||
| 			cmdPublish, | 			cmdPublish, | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								cmd/user.go
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								cmd/user.go
									
										
									
									
									
								
							|  | @ -159,31 +159,7 @@ func execUserList(c *cli.Context) error { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return showUsers(c, users) | 	return showUsers(c, manager, users) | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func showUsers(c *cli.Context, users []*auth.User) error { |  | ||||||
| 	for _, user := range users { |  | ||||||
| 		fmt.Fprintf(c.App.Writer, "User %s (%s)\n", user.Name, user.Role) |  | ||||||
| 		if user.Role == auth.RoleAdmin { |  | ||||||
| 			fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n") |  | ||||||
| 		} else if len(user.Grants) > 0 { |  | ||||||
| 			for _, grant := range user.Grants { |  | ||||||
| 				if grant.Read && grant.Write { |  | ||||||
| 					fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.Topic) |  | ||||||
| 				} else if grant.Read { |  | ||||||
| 					fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.Topic) |  | ||||||
| 				} else if grant.Write { |  | ||||||
| 					fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.Topic) |  | ||||||
| 				} else { |  | ||||||
| 					fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.Topic) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} else { |  | ||||||
| 			fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n") |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func createAuthManager(c *cli.Context) (auth.Manager, error) { | func createAuthManager(c *cli.Context) (auth.Manager, error) { | ||||||
|  |  | ||||||
|  | @ -1,108 +0,0 @@ | ||||||
| package cmd |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"github.com/urfave/cli/v2" |  | ||||||
| 	"heckel.io/ntfy/auth" |  | ||||||
| 	"heckel.io/ntfy/util" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	userEveryone = "everyone" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var flagsAllow = append( |  | ||||||
| 	userCommandFlags(), |  | ||||||
| 	&cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"}, |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var cmdAllow = &cli.Command{ |  | ||||||
| 	Name:      "allow", |  | ||||||
| 	Usage:     "Grant a user access to a topic", |  | ||||||
| 	UsageText: "ntfy allow USERNAME TOPIC [read-write|read-only|write-only|none]", |  | ||||||
| 	Flags:     flagsAllow, |  | ||||||
| 	Before:    initConfigFileInputSource("config", flagsAllow), |  | ||||||
| 	Action:    execUserAllow, |  | ||||||
| 	Category:  categoryServer, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func execUserAllow(c *cli.Context) error { |  | ||||||
| 	username := c.Args().Get(0) |  | ||||||
| 	topic := c.Args().Get(1) |  | ||||||
| 	perms := c.Args().Get(2) |  | ||||||
| 	reset := c.Bool("reset") |  | ||||||
| 	if username == "" { |  | ||||||
| 		return errors.New("username expected, type 'ntfy allow --help' for help") |  | ||||||
| 	} else if !reset && topic == "" { |  | ||||||
| 		return errors.New("topic expected, type 'ntfy allow --help' for help") |  | ||||||
| 	} else if !util.InStringList([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none"}, perms) { |  | ||||||
| 		return errors.New("permission must be one of: read-write, read-only, write-only, or none (or the aliases: read, ro, write, wo)") |  | ||||||
| 	} |  | ||||||
| 	if username == userEveryone { |  | ||||||
| 		username = "" |  | ||||||
| 	} |  | ||||||
| 	read := util.InStringList([]string{"", "read-write", "rw", "read-only", "read", "ro"}, perms) |  | ||||||
| 	write := util.InStringList([]string{"", "read-write", "rw", "write-only", "write", "wo"}, perms) |  | ||||||
| 	manager, err := createAuthManager(c) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	if reset { |  | ||||||
| 		return doAccessReset(c, manager, username, topic) |  | ||||||
| 	} |  | ||||||
| 	return doAccessAllow(c, manager, username, topic, read, write) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func doAccessAllow(c *cli.Context, manager auth.Manager, username string, topic string, read bool, write bool) error { |  | ||||||
| 	if err := manager.AllowAccess(username, topic, read, write); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	if username == "" { |  | ||||||
| 		if read && write { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "Anonymous users granted full access to topic %s\n", topic) |  | ||||||
| 		} else if read { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "Anonymous users granted read-only access to topic %s\n", topic) |  | ||||||
| 		} else if write { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "Anonymous users granted write-only access to topic %s\n", topic) |  | ||||||
| 		} else { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "Revoked all access to topic %s for all anonymous users\n", topic) |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		if read && write { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "User %s now has read-write access to topic %s\n", username, topic) |  | ||||||
| 		} else if read { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "User %s now has read-only access to topic %s\n", username, topic) |  | ||||||
| 		} else if write { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "User %s now has write-only access to topic %s\n", username, topic) |  | ||||||
| 		} else { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "Revoked all access to topic %s for user %s\n", topic, username) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	user, err := manager.User(username) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	fmt.Fprintln(c.App.Writer) |  | ||||||
| 	return showUsers(c, []*auth.User{user}) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func doAccessReset(c *cli.Context, manager auth.Manager, username, topic string) error { |  | ||||||
| 	if err := manager.ResetAccess(username, topic); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	if username == "" { |  | ||||||
| 		if topic == "" { |  | ||||||
| 			fmt.Fprintln(c.App.Writer, "Reset access for all anonymous users and all topics") |  | ||||||
| 		} else { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "Reset access to topic %s for all anonymous users\n", topic) |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		if topic == "" { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "Reset access for user %s to all topics\n", username) |  | ||||||
| 		} else { |  | ||||||
| 			fmt.Fprintf(c.App.Writer, "Reset access for user %s and topic %s\n", username, topic) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | @ -1,35 +0,0 @@ | ||||||
| package cmd |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"github.com/urfave/cli/v2" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var flagsDeny = userCommandFlags() |  | ||||||
| var cmdDeny = &cli.Command{ |  | ||||||
| 	Name:      "deny", |  | ||||||
| 	Usage:     "Revoke user access from a topic", |  | ||||||
| 	UsageText: "ntfy deny USERNAME TOPIC", |  | ||||||
| 	Flags:     flagsDeny, |  | ||||||
| 	Before:    initConfigFileInputSource("config", flagsDeny), |  | ||||||
| 	Action:    execUserDeny, |  | ||||||
| 	Category:  categoryServer, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func execUserDeny(c *cli.Context) error { |  | ||||||
| 	username := c.Args().Get(0) |  | ||||||
| 	topic := c.Args().Get(1) |  | ||||||
| 	if username == "" { |  | ||||||
| 		return errors.New("username expected, type 'ntfy deny --help' for help") |  | ||||||
| 	} else if topic == "" { |  | ||||||
| 		return errors.New("topic expected, type 'ntfy deny --help' for help") |  | ||||||
| 	} |  | ||||||
| 	if username == userEveryone { |  | ||||||
| 		username = "" |  | ||||||
| 	} |  | ||||||
| 	manager, err := createAuthManager(c) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return doAccessAllow(c, manager, username, topic, false, false) |  | ||||||
| } |  | ||||||
|  | @ -41,6 +41,7 @@ var ( | ||||||
| 	errHTTPBadRequestWebSocketsUpgradeHeaderMissing  = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", ""} | 	errHTTPBadRequestWebSocketsUpgradeHeaderMissing  = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", ""} | ||||||
| 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", ""} | 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", ""} | ||||||
| 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", ""} | 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", ""} | ||||||
|  | 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", ""} | ||||||
| 	errHTTPTooManyRequestsLimitRequests              = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | 	errHTTPTooManyRequestsLimitRequests              = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | ||||||
| 	errHTTPTooManyRequestsLimitEmails                = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | 	errHTTPTooManyRequestsLimitEmails                = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | ||||||
| 	errHTTPTooManyRequestsLimitSubscriptions         = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | 	errHTTPTooManyRequestsLimitSubscriptions         = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"} | ||||||
|  |  | ||||||
|  | @ -1144,7 +1144,7 @@ func (s *Server) withAuth(next handleFunc, perm auth.Permission) handleFunc { | ||||||
| 		} | 		} | ||||||
| 		if err := s.auth.Authorize(user, t.ID, perm); err != nil { | 		if err := s.auth.Authorize(user, t.ID, perm); err != nil { | ||||||
| 			log.Printf("unauthorized: %s", err.Error()) | 			log.Printf("unauthorized: %s", err.Error()) | ||||||
| 			return errHTTPUnauthorized | 			return errHTTPForbidden | ||||||
| 		} | 		} | ||||||
| 		return next(w, r, v) | 		return next(w, r, v) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue