Philipp Heckel 4 лет назад
Родитель
Сommit
393f95aeac
6 измененных файлов с 183 добавлено и 43 удалено
  1. 16 3
      auth/auth.go
  2. 99 17
      auth/auth_sqlite.go
  3. 37 1
      cmd/user.go
  4. 27 18
      cmd/user_allow.go
  5. 3 3
      cmd/user_deny.go
  6. 1 1
      server/server.go

+ 16 - 3
auth/auth.go

@@ -11,6 +11,8 @@ type Auther interface {
 type Manager interface {
 type Manager interface {
 	AddUser(username, password string, role Role) error
 	AddUser(username, password string, role Role) error
 	RemoveUser(username string) error
 	RemoveUser(username string) error
+	Users() ([]*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
 	AllowAccess(username string, topic string, read bool, write bool) error
 	AllowAccess(username string, topic string, read bool, write bool) error
@@ -18,8 +20,16 @@ type Manager interface {
 }
 }
 
 
 type User struct {
 type User struct {
-	Name string
-	Role Role
+	Name   string
+	Pass   string // hashed
+	Role   Role
+	Grants []Grant
+}
+
+type Grant struct {
+	Topic string
+	Read  bool
+	Write bool
 }
 }
 
 
 type Permission int
 type Permission int
@@ -52,4 +62,7 @@ func AllowedRole(role Role) bool {
 	return role == RoleUser || role == RoleAdmin
 	return role == RoleUser || role == RoleAdmin
 }
 }
 
 
-var ErrUnauthorized = errors.New("unauthorized")
+var (
+	ErrUnauthorized = errors.New("unauthorized")
+	ErrNotFound     = errors.New("not found")
+)

+ 99 - 17
auth/auth_sqlite.go

@@ -58,17 +58,19 @@ const (
 
 
 // Manager-related queries
 // Manager-related queries
 const (
 const (
-	insertUser     = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
-	updateUserPass = `UPDATE user SET pass = ? WHERE user = ?`
-	updateUserRole = `UPDATE user SET role = ? WHERE user = ?`
-	upsertAccess   = `
+	insertUserQuery           = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
+	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 = ?`
+	updateUserRoleQuery       = `UPDATE user SET role = ? WHERE user = ?`
+	upsertAccessQuery         = `
 		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
 	`
 	`
-	deleteUser      = `DELETE FROM user WHERE user = ?`
-	deleteAllAccess = `DELETE FROM access WHERE user = ?`
-	deleteAccess    = `DELETE FROM access WHERE user = ? AND topic = ?`
+	deleteUserQuery      = `DELETE FROM user WHERE user = ?`
+	deleteAllAccessQuery = `DELETE FROM access WHERE user = ?`
+	deleteAccessQuery    = `DELETE FROM access WHERE user = ? AND topic = ?`
 )
 )
 
 
 type SQLiteAuth struct {
 type SQLiteAuth struct {
@@ -127,13 +129,17 @@ func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) {
 }
 }
 
 
 func (a *SQLiteAuth) Authorize(user *User, topic string, perm Permission) error {
 func (a *SQLiteAuth) Authorize(user *User, topic string, perm Permission) error {
-	if user.Role == RoleAdmin {
+	if user != nil && user.Role == RoleAdmin {
 		return nil // Admin can do everything
 		return nil // Admin can do everything
 	}
 	}
 	// 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).
-	rows, err := a.db.Query(selectTopicPermsQuery, user.Name, topic)
+	var username string
+	if user != nil {
+		username = user.Name
+	}
+	rows, err := a.db.Query(selectTopicPermsQuery, username, topic)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -164,42 +170,118 @@ func (a *SQLiteAuth) AddUser(username, password string, role Role) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if _, err = a.db.Exec(insertUser, username, hash, role); err != nil {
+	if _, err = a.db.Exec(insertUserQuery, username, hash, role); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
 func (a *SQLiteAuth) RemoveUser(username string) error {
 func (a *SQLiteAuth) RemoveUser(username string) error {
-	if _, err := a.db.Exec(deleteUser, username); err != nil {
+	if _, err := a.db.Exec(deleteUserQuery, username); err != nil {
 		return err
 		return err
 	}
 	}
-	if _, err := a.db.Exec(deleteAllAccess, username); err != nil {
+	if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
+func (a *SQLiteAuth) Users() ([]*User, error) {
+	rows, err := a.db.Query(selectUsernamesQuery)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	usernames := make([]string, 0)
+	for rows.Next() {
+		var username string
+		if err := rows.Scan(&username); err != nil {
+			return nil, err
+		} else if err := rows.Err(); err != nil {
+			return nil, err
+		}
+		usernames = append(usernames, username)
+	}
+	rows.Close()
+	users := make([]*User, 0)
+	for _, username := range usernames {
+		user, err := a.User(username)
+		if err != nil {
+			return nil, err
+		}
+		users = append(users, user)
+	}
+	return users, nil
+}
+
+func (a *SQLiteAuth) User(username string) (*User, error) {
+	urows, err := a.db.Query(selectUserQuery, username)
+	if err != nil {
+		return nil, err
+	}
+	defer urows.Close()
+	var hash, role string
+	if !urows.Next() {
+		return nil, ErrNotFound
+	}
+	if err := urows.Scan(&hash, &role); err != nil {
+		return nil, err
+	} else if err := urows.Err(); err != nil {
+		return nil, err
+	}
+	arows, err := a.db.Query(selectUserTopicPermsQuery, username)
+	if err != nil {
+		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{
+		Name:   username,
+		Pass:   hash,
+		Role:   Role(role),
+		Grants: 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), bcryptCost)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if _, err := a.db.Exec(updateUserPass, hash, username); err != nil {
+	if _, err := a.db.Exec(updateUserPassQuery, hash, username); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
 func (a *SQLiteAuth) ChangeRole(username string, role Role) error {
 func (a *SQLiteAuth) ChangeRole(username string, role Role) error {
-	if _, err := a.db.Exec(updateUserRole, string(role), username); err != nil {
+	if _, err := a.db.Exec(updateUserRoleQuery, string(role), username); err != nil {
 		return err
 		return err
 	}
 	}
+	if role == RoleAdmin {
+		if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil {
+			return err
+		}
+	}
 	return nil
 	return nil
 }
 }
 
 
 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(upsertAccess, username, topic, read, write); err != nil {
+	if _, err := a.db.Exec(upsertAccessQuery, username, topic, read, write); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
@@ -207,11 +289,11 @@ func (a *SQLiteAuth) AllowAccess(username string, topic string, read bool, write
 
 
 func (a *SQLiteAuth) ResetAccess(username string, topic string) error {
 func (a *SQLiteAuth) ResetAccess(username string, topic string) error {
 	if topic == "" {
 	if topic == "" {
-		if _, err := a.db.Exec(deleteAllAccess, username); err != nil {
+		if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil {
 			return err
 			return err
 		}
 		}
 	} else {
 	} else {
-		if _, err := a.db.Exec(deleteAccess, username, topic); err != nil {
+		if _, err := a.db.Exec(deleteAccessQuery, username, topic); err != nil {
 			return err
 			return err
 		}
 		}
 	}
 	}

+ 37 - 1
cmd/user.go

@@ -69,7 +69,7 @@ var cmdUser = &cli.Command{
 			Name:    "list",
 			Name:    "list",
 			Aliases: []string{"chr"},
 			Aliases: []string{"chr"},
 			Usage:   "change user role",
 			Usage:   "change user role",
-			Action:  execUserChangeRole,
+			Action:  execUserList,
 		},
 		},
 	},
 	},
 }
 }
@@ -150,6 +150,42 @@ func execUserChangeRole(c *cli.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func execUserList(c *cli.Context) error {
+	manager, err := createAuthManager(c)
+	if err != nil {
+		return err
+	}
+	users, err := manager.Users()
+	if err != nil {
+		return err
+	}
+	return showUsers(c, 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) {
 	authFile := c.String("auth-file")
 	authFile := c.String("auth-file")
 	authDefaultAccess := c.String("auth-default-access")
 	authDefaultAccess := c.String("auth-default-access")

+ 27 - 18
cmd/user_allow.go

@@ -8,6 +8,10 @@ import (
 	"heckel.io/ntfy/util"
 	"heckel.io/ntfy/util"
 )
 )
 
 
+const (
+	userEveryone = "everyone"
+)
+
 var flagsAllow = append(
 var flagsAllow = append(
 	userCommandFlags(),
 	userCommandFlags(),
 	&cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"},
 	&cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"},
@@ -16,7 +20,7 @@ var flagsAllow = append(
 var cmdAllow = &cli.Command{
 var cmdAllow = &cli.Command{
 	Name:      "allow",
 	Name:      "allow",
 	Usage:     "Grant a user access to a topic",
 	Usage:     "Grant a user access to a topic",
-	UsageText: "ntfy allow USERNAME TOPIC [read-write|read-only|write-only]",
+	UsageText: "ntfy allow USERNAME TOPIC [read-write|read-only|write-only|none]",
 	Flags:     flagsAllow,
 	Flags:     flagsAllow,
 	Before:    initConfigFileInputSource("config", flagsAllow),
 	Before:    initConfigFileInputSource("config", flagsAllow),
 	Action:    execUserAllow,
 	Action:    execUserAllow,
@@ -32,14 +36,14 @@ func execUserAllow(c *cli.Context) error {
 		return errors.New("username expected, type 'ntfy allow --help' for help")
 		return errors.New("username expected, type 'ntfy allow --help' for help")
 	} else if !reset && topic == "" {
 	} else if !reset && topic == "" {
 		return errors.New("topic expected, type 'ntfy allow --help' for help")
 		return errors.New("topic expected, type 'ntfy allow --help' for help")
-	} else if !util.InStringList([]string{"", "read-write", "read-only", "read", "ro", "write-only", "write", "wo", "none"}, perms) {
+	} 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)")
 		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 == "everyone" {
+	if username == userEveryone {
 		username = ""
 		username = ""
 	}
 	}
-	read := util.InStringList([]string{"", "read-write", "read-only", "read", "ro"}, perms)
-	write := util.InStringList([]string{"", "read-write", "write-only", "write", "wo"}, perms)
+	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)
 	manager, err := createAuthManager(c)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -56,26 +60,31 @@ func doAccessAllow(c *cli.Context, manager auth.Manager, username string, topic
 	}
 	}
 	if username == "" {
 	if username == "" {
 		if read && write {
 		if read && write {
-			fmt.Fprintf(c.App.ErrWriter, "Anonymous users granted full access to topic %s\n", topic)
+			fmt.Fprintf(c.App.Writer, "Anonymous users granted full access to topic %s\n", topic)
 		} else if read {
 		} else if read {
-			fmt.Fprintf(c.App.ErrWriter, "Anonymous users granted read-only access to topic %s\n", topic)
+			fmt.Fprintf(c.App.Writer, "Anonymous users granted read-only access to topic %s\n", topic)
 		} else if write {
 		} else if write {
-			fmt.Fprintf(c.App.ErrWriter, "Anonymous users granted write-only access to topic %s\n", topic)
+			fmt.Fprintf(c.App.Writer, "Anonymous users granted write-only access to topic %s\n", topic)
 		} else {
 		} else {
-			fmt.Fprintf(c.App.ErrWriter, "Revoked all access to topic %s for all anonymous users\n", topic)
+			fmt.Fprintf(c.App.Writer, "Revoked all access to topic %s for all anonymous users\n", topic)
 		}
 		}
 	} else {
 	} else {
 		if read && write {
 		if read && write {
-			fmt.Fprintf(c.App.ErrWriter, "User %s now has read-write access to topic %s\n", username, topic)
+			fmt.Fprintf(c.App.Writer, "User %s now has read-write access to topic %s\n", username, topic)
 		} else if read {
 		} else if read {
-			fmt.Fprintf(c.App.ErrWriter, "User %s now has read-only access to topic %s\n", username, topic)
+			fmt.Fprintf(c.App.Writer, "User %s now has read-only access to topic %s\n", username, topic)
 		} else if write {
 		} else if write {
-			fmt.Fprintf(c.App.ErrWriter, "User %s now has write-only access to topic %s\n", username, topic)
+			fmt.Fprintf(c.App.Writer, "User %s now has write-only access to topic %s\n", username, topic)
 		} else {
 		} else {
-			fmt.Fprintf(c.App.ErrWriter, "Revoked all access to topic %s for user %s\n", topic, username)
+			fmt.Fprintf(c.App.Writer, "Revoked all access to topic %s for user %s\n", topic, username)
 		}
 		}
 	}
 	}
-	return nil
+	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 {
 func doAccessReset(c *cli.Context, manager auth.Manager, username, topic string) error {
@@ -84,15 +93,15 @@ func doAccessReset(c *cli.Context, manager auth.Manager, username, topic string)
 	}
 	}
 	if username == "" {
 	if username == "" {
 		if topic == "" {
 		if topic == "" {
-			fmt.Fprintln(c.App.ErrWriter, "Reset access for all anonymous users and all topics")
+			fmt.Fprintln(c.App.Writer, "Reset access for all anonymous users and all topics")
 		} else {
 		} else {
-			fmt.Fprintf(c.App.ErrWriter, "Reset access to topic %s for all anonymous users\n", topic)
+			fmt.Fprintf(c.App.Writer, "Reset access to topic %s for all anonymous users\n", topic)
 		}
 		}
 	} else {
 	} else {
 		if topic == "" {
 		if topic == "" {
-			fmt.Fprintf(c.App.ErrWriter, "Reset access for user %s to all topics\n", username)
+			fmt.Fprintf(c.App.Writer, "Reset access for user %s to all topics\n", username)
 		} else {
 		} else {
-			fmt.Fprintf(c.App.ErrWriter, "Reset access for user %s and topic %s\n", username, topic)
+			fmt.Fprintf(c.App.Writer, "Reset access for user %s and topic %s\n", username, topic)
 		}
 		}
 	}
 	}
 	return nil
 	return nil

+ 3 - 3
cmd/user_deny.go

@@ -20,11 +20,11 @@ func execUserDeny(c *cli.Context) error {
 	username := c.Args().Get(0)
 	username := c.Args().Get(0)
 	topic := c.Args().Get(1)
 	topic := c.Args().Get(1)
 	if username == "" {
 	if username == "" {
-		return errors.New("username expected, type 'ntfy allow --help' for help")
+		return errors.New("username expected, type 'ntfy deny --help' for help")
 	} else if topic == "" {
 	} else if topic == "" {
-		return errors.New("topic expected, type 'ntfy allow --help' for help")
+		return errors.New("topic expected, type 'ntfy deny --help' for help")
 	}
 	}
-	if username == "everyone" {
+	if username == userEveryone {
 		username = ""
 		username = ""
 	}
 	}
 	manager, err := createAuthManager(c)
 	manager, err := createAuthManager(c)

+ 1 - 1
server/server.go

@@ -1134,7 +1134,7 @@ func (s *Server) withAuth(next handleFunc, perm auth.Permission) handleFunc {
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		user := auth.Everyone
+		var user *auth.User // may stay nil if no auth header!
 		username, password, ok := r.BasicAuth()
 		username, password, ok := r.BasicAuth()
 		if ok {
 		if ok {
 			if user, err = s.auth.Authenticate(username, password); err != nil {
 			if user, err = s.auth.Authenticate(username, password); err != nil {