Philipp Heckel 4 лет назад
Родитель
Сommit
03a4e3e8e9
6 измененных файлов с 243 добавлено и 28 удалено
  1. 13 0
      auth/auth.go
  2. 45 11
      auth/auth_sqlite.go
  3. 4 2
      cmd/app.go
  4. 47 15
      cmd/user.go
  5. 99 0
      cmd/user_allow.go
  6. 35 0
      cmd/user_deny.go

+ 13 - 0
auth/auth.go

@@ -12,6 +12,9 @@ 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
 	ChangePassword(username, password string) error
 	ChangePassword(username, password string) error
+	ChangeRole(username string, role Role) error
+	AllowAccess(username string, topic string, read bool, write bool) error
+	ResetAccess(username string, topic string) error
 }
 }
 
 
 type User struct {
 type User struct {
@@ -39,4 +42,14 @@ var Everyone = &User{
 	Role: RoleNone,
 	Role: RoleNone,
 }
 }
 
 
+var Roles = []Role{
+	RoleAdmin,
+	RoleUser,
+	RoleNone,
+}
+
+func AllowedRole(role Role) bool {
+	return role == RoleUser || role == RoleAdmin
+}
+
 var ErrUnauthorized = errors.New("unauthorized")
 var ErrUnauthorized = errors.New("unauthorized")

+ 45 - 11
auth/auth_sqlite.go

@@ -8,16 +8,16 @@ import (
 /*
 /*
 
 
 SELECT * FROM user;
 SELECT * FROM user;
-SELECT * FROM user_topic;
+SELECT * FROM access;
 
 
 INSERT INTO user VALUES ('phil','$2a$06$.4W0LI5mcxzxhpjUvpTaNeu0MhRO0T7B.CYnmAkRnlztIy7PrSODu', 'admin');
 INSERT INTO user VALUES ('phil','$2a$06$.4W0LI5mcxzxhpjUvpTaNeu0MhRO0T7B.CYnmAkRnlztIy7PrSODu', 'admin');
 INSERT INTO user VALUES ('ben','$2a$06$skJK/AecWCUmiCjr69ke.Ow/hFA616RdvJJPxnI221zyohsRlyXL.', 'user');
 INSERT INTO user VALUES ('ben','$2a$06$skJK/AecWCUmiCjr69ke.Ow/hFA616RdvJJPxnI221zyohsRlyXL.', 'user');
 INSERT INTO user VALUES ('marian','$2a$10$8U90swQIatvHHI4sw0Wo7.OUy6dUwzMcoOABi6BsS4uF0x3zcSXRW', 'user');
 INSERT INTO user VALUES ('marian','$2a$10$8U90swQIatvHHI4sw0Wo7.OUy6dUwzMcoOABi6BsS4uF0x3zcSXRW', 'user');
 
 
-INSERT INTO user_topic VALUES ('ben','alerts',1,1);
-INSERT INTO user_topic VALUES ('marian','alerts',1,0);
-INSERT INTO user_topic VALUES ('','announcements',1,0);
-INSERT INTO user_topic VALUES ('','write-all',1,1);
+INSERT INTO access VALUES ('ben','alerts',1,1);
+INSERT INTO access VALUES ('marian','alerts',1,0);
+INSERT INTO access VALUES ('','announcements',1,0);
+INSERT INTO access VALUES ('','write-all',1,1);
 
 
 */
 */
 
 
@@ -34,7 +34,7 @@ const (
 			pass TEXT NOT NULL,
 			pass TEXT NOT NULL,
 			role TEXT NOT NULL
 			role TEXT NOT NULL
 		);
 		);
-		CREATE TABLE IF NOT EXISTS user_topic (
+		CREATE TABLE IF NOT EXISTS access (
 			user TEXT NOT NULL,		
 			user TEXT NOT NULL,		
 			topic TEXT NOT NULL,
 			topic TEXT NOT NULL,
 			read INT NOT NULL,
 			read INT NOT NULL,
@@ -50,7 +50,7 @@ const (
 	selectUserQuery       = `SELECT pass, role FROM user WHERE user = ?`
 	selectUserQuery       = `SELECT pass, role FROM user WHERE user = ?`
 	selectTopicPermsQuery = `
 	selectTopicPermsQuery = `
 		SELECT read, write 
 		SELECT read, write 
-		FROM user_topic 
+		FROM access 
 		WHERE user IN ('', ?) AND topic = ?
 		WHERE user IN ('', ?) AND topic = ?
 		ORDER BY user DESC
 		ORDER BY user DESC
 	`
 	`
@@ -58,10 +58,17 @@ const (
 
 
 // Manager-related queries
 // Manager-related queries
 const (
 const (
-	insertUser      = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
-	updateUserPass  = `UPDATE user SET pass = ? WHERE user = ?`
+	insertUser     = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
+	updateUserPass = `UPDATE user SET pass = ? WHERE user = ?`
+	updateUserRole = `UPDATE user SET role = ? WHERE user = ?`
+	upsertAccess   = `
+		INSERT INTO access (user, topic, read, write) 
+		VALUES (?, ?, ?, ?)
+		ON CONFLICT (user, topic) DO UPDATE SET read=excluded.read, write=excluded.write
+	`
 	deleteUser      = `DELETE FROM user WHERE user = ?`
 	deleteUser      = `DELETE FROM user WHERE user = ?`
-	deleteUserTopic = `DELETE FROM user_topic WHERE user = ?`
+	deleteAllAccess = `DELETE FROM access WHERE user = ?`
+	deleteAccess    = `DELETE FROM access WHERE user = ? AND topic = ?`
 )
 )
 
 
 type SQLiteAuth struct {
 type SQLiteAuth struct {
@@ -167,7 +174,7 @@ func (a *SQLiteAuth) RemoveUser(username string) error {
 	if _, err := a.db.Exec(deleteUser, username); err != nil {
 	if _, err := a.db.Exec(deleteUser, username); err != nil {
 		return err
 		return err
 	}
 	}
-	if _, err := a.db.Exec(deleteUserTopic, username); err != nil {
+	if _, err := a.db.Exec(deleteAllAccess, username); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
@@ -183,3 +190,30 @@ func (a *SQLiteAuth) ChangePassword(username, password string) error {
 	}
 	}
 	return nil
 	return nil
 }
 }
+
+func (a *SQLiteAuth) ChangeRole(username string, role Role) error {
+	if _, err := a.db.Exec(updateUserRole, string(role), username); err != nil {
+		return err
+	}
+	return nil
+}
+
+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 {
+		return err
+	}
+	return nil
+}
+
+func (a *SQLiteAuth) ResetAccess(username string, topic string) error {
+	if topic == "" {
+		if _, err := a.db.Exec(deleteAllAccess, username); err != nil {
+			return err
+		}
+	} else {
+		if _, err := a.db.Exec(deleteAccess, username, topic); err != nil {
+			return err
+		}
+	}
+	return nil
+}

+ 4 - 2
cmd/app.go

@@ -15,8 +15,8 @@ var (
 )
 )
 
 
 const (
 const (
-	categoryClient = "Client-side commands"
-	categoryServer = "Server-side commands"
+	categoryClient = "Client commands"
+	categoryServer = "Server commands"
 )
 )
 
 
 // New creates a new CLI application
 // New creates a new CLI application
@@ -37,6 +37,8 @@ func New() *cli.App {
 			// Server commands
 			// Server commands
 			cmdServe,
 			cmdServe,
 			cmdUser,
 			cmdUser,
+			cmdAllow,
+			cmdDeny,
 
 
 			// Client commands
 			// Client commands
 			cmdPublish,
 			cmdPublish,

+ 47 - 15
cmd/user.go

@@ -29,12 +29,7 @@ dabbling for CLI
 
 
 */
 */
 
 
-var flagsUser = []cli.Flag{
-	&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
-	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
-	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
-}
-
+var flagsUser = userCommandFlags()
 var cmdUser = &cli.Command{
 var cmdUser = &cli.Command{
 	Name:      "user",
 	Name:      "user",
 	Usage:     "Manage users and access to topics",
 	Usage:     "Manage users and access to topics",
@@ -60,21 +55,33 @@ var cmdUser = &cli.Command{
 		},
 		},
 		{
 		{
 			Name:    "change-pass",
 			Name:    "change-pass",
-			Aliases: []string{"ch"},
+			Aliases: []string{"chp"},
 			Usage:   "change user password",
 			Usage:   "change user password",
 			Action:  execUserChangePass,
 			Action:  execUserChangePass,
 		},
 		},
+		{
+			Name:    "change-role",
+			Aliases: []string{"chr"},
+			Usage:   "change user role",
+			Action:  execUserChangeRole,
+		},
+		{
+			Name:    "list",
+			Aliases: []string{"chr"},
+			Usage:   "change user role",
+			Action:  execUserChangeRole,
+		},
 	},
 	},
 }
 }
 
 
 func execUserAdd(c *cli.Context) error {
 func execUserAdd(c *cli.Context) error {
-	role := c.String("role")
-	if c.NArg() == 0 {
+	username := c.Args().Get(0)
+	role := auth.Role(c.String("role"))
+	if username == "" {
 		return errors.New("username expected, type 'ntfy user add --help' for help")
 		return errors.New("username expected, type 'ntfy user add --help' for help")
-	} else if role != string(auth.RoleUser) && role != string(auth.RoleAdmin) {
+	} else if !auth.AllowedRole(role) {
 		return errors.New("role must be either 'user' or 'admin'")
 		return errors.New("role must be either 'user' or 'admin'")
 	}
 	}
-	username := c.Args().Get(0)
 	password, err := readPassword(c)
 	password, err := readPassword(c)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -91,10 +98,10 @@ func execUserAdd(c *cli.Context) error {
 }
 }
 
 
 func execUserDel(c *cli.Context) error {
 func execUserDel(c *cli.Context) error {
-	if c.NArg() == 0 {
+	username := c.Args().Get(0)
+	if username == "" {
 		return errors.New("username expected, type 'ntfy user del --help' for help")
 		return errors.New("username expected, type 'ntfy user del --help' for help")
 	}
 	}
-	username := c.Args().Get(0)
 	manager, err := createAuthManager(c)
 	manager, err := createAuthManager(c)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -107,10 +114,10 @@ func execUserDel(c *cli.Context) error {
 }
 }
 
 
 func execUserChangePass(c *cli.Context) error {
 func execUserChangePass(c *cli.Context) error {
-	if c.NArg() == 0 {
+	username := c.Args().Get(0)
+	if username == "" {
 		return errors.New("username expected, type 'ntfy user change-pass --help' for help")
 		return errors.New("username expected, type 'ntfy user change-pass --help' for help")
 	}
 	}
-	username := c.Args().Get(0)
 	password, err := readPassword(c)
 	password, err := readPassword(c)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -126,6 +133,23 @@ func execUserChangePass(c *cli.Context) error {
 	return nil
 	return nil
 }
 }
 
 
+func execUserChangeRole(c *cli.Context) error {
+	username := c.Args().Get(0)
+	role := auth.Role(c.Args().Get(1))
+	if username == "" || !auth.AllowedRole(role) {
+		return errors.New("username and new role expected, type 'ntfy user change-role --help' for help")
+	}
+	manager, err := createAuthManager(c)
+	if err != nil {
+		return err
+	}
+	if err := manager.ChangeRole(username, role); err != nil {
+		return err
+	}
+	fmt.Fprintf(c.App.ErrWriter, "Changed role for user %s to %s\n", username, role)
+	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")
@@ -158,3 +182,11 @@ func readPassword(c *cli.Context) (string, error) {
 	}
 	}
 	return string(password), nil
 	return string(password), nil
 }
 }
+
+func userCommandFlags() []cli.Flag {
+	return []cli.Flag{
+		&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
+		altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
+		altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
+	}
+}

+ 99 - 0
cmd/user_allow.go

@@ -0,0 +1,99 @@
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"github.com/urfave/cli/v2"
+	"heckel.io/ntfy/auth"
+	"heckel.io/ntfy/util"
+)
+
+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]",
+	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", "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 == "everyone" {
+		username = ""
+	}
+	read := util.InStringList([]string{"", "read-write", "read-only", "read", "ro"}, perms)
+	write := util.InStringList([]string{"", "read-write", "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.ErrWriter, "Anonymous users granted full access to topic %s\n", topic)
+		} else if read {
+			fmt.Fprintf(c.App.ErrWriter, "Anonymous users granted read-only access to topic %s\n", topic)
+		} else if write {
+			fmt.Fprintf(c.App.ErrWriter, "Anonymous users granted write-only access to topic %s\n", topic)
+		} else {
+			fmt.Fprintf(c.App.ErrWriter, "Revoked all access to topic %s for all anonymous users\n", topic)
+		}
+	} else {
+		if read && write {
+			fmt.Fprintf(c.App.ErrWriter, "User %s now has read-write access to topic %s\n", username, topic)
+		} else if read {
+			fmt.Fprintf(c.App.ErrWriter, "User %s now has read-only access to topic %s\n", username, topic)
+		} else if write {
+			fmt.Fprintf(c.App.ErrWriter, "User %s now has write-only access to topic %s\n", username, topic)
+		} else {
+			fmt.Fprintf(c.App.ErrWriter, "Revoked all access to topic %s for user %s\n", topic, username)
+		}
+	}
+	return nil
+}
+
+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.ErrWriter, "Reset access for all anonymous users and all topics")
+		} else {
+			fmt.Fprintf(c.App.ErrWriter, "Reset access to topic %s for all anonymous users\n", topic)
+		}
+	} else {
+		if topic == "" {
+			fmt.Fprintf(c.App.ErrWriter, "Reset access for user %s to all topics\n", username)
+		} else {
+			fmt.Fprintf(c.App.ErrWriter, "Reset access for user %s and topic %s\n", username, topic)
+		}
+	}
+	return nil
+}

+ 35 - 0
cmd/user_deny.go

@@ -0,0 +1,35 @@
+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 allow --help' for help")
+	} else if topic == "" {
+		return errors.New("topic expected, type 'ntfy allow --help' for help")
+	}
+	if username == "everyone" {
+		username = ""
+	}
+	manager, err := createAuthManager(c)
+	if err != nil {
+		return err
+	}
+	return doAccessAllow(c, manager, username, topic, false, false)
+}