瀏覽代碼

More auth

Philipp Heckel 4 年之前
父節點
當前提交
460162737a
共有 9 個文件被更改,包括 258 次插入224 次删除
  1. 7 13
      auth/auth.go
  2. 73 40
      auth/auth_sqlite.go
  3. 174 0
      cmd/access.go
  4. 1 2
      cmd/app.go
  5. 1 25
      cmd/user.go
  6. 0 108
      cmd/user_allow.go
  7. 0 35
      cmd/user_deny.go
  8. 1 0
      server/errors.go
  9. 1 1
      server/server.go

+ 7 - 13
auth/auth.go

@@ -15,6 +15,7 @@ type Manager interface {
 	User(username string) (*User, error)
 	ChangePassword(username, password string) error
 	ChangeRole(username string, role Role) error
+	DefaultAccess() (read bool, write bool)
 	AllowAccess(username string, topic string, read bool, write bool) error
 	ResetAccess(username string, topic string) error
 }
@@ -42,21 +43,14 @@ const (
 type Role string
 
 const (
-	RoleAdmin = Role("admin")
-	RoleUser  = Role("user")
-	RoleNone  = Role("none")
+	RoleAdmin     = Role("admin")
+	RoleUser      = Role("user")
+	RoleAnonymous = Role("anonymous")
 )
 
-var Everyone = &User{
-	Name: "",
-	Role: RoleNone,
-}
-
-var Roles = []Role{
-	RoleAdmin,
-	RoleUser,
-	RoleNone,
-}
+const (
+	Everyone = "*"
+)
 
 func AllowedRole(role Role) bool {
 	return role == RoleUser || role == RoleAdmin

+ 73 - 40
auth/auth_sqlite.go

@@ -21,10 +21,6 @@ INSERT INTO access VALUES ('','write-all',1,1);
 
 */
 
-const (
-	bcryptCost = 11
-)
-
 // Auther-related queries
 const (
 	createAuthTablesQueries = `
@@ -51,26 +47,28 @@ const (
 	selectTopicPermsQuery = `
 		SELECT read, write 
 		FROM access 
-		WHERE user IN ('', ?) AND topic = ?
+		WHERE user IN ('*', ?) AND topic = ?
 		ORDER BY user DESC
 	`
 )
 
 // Manager-related queries
 const (
-	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         = `
+	insertUserQuery      = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
+	selectUsernamesQuery = `SELECT user FROM user ORDER BY role, user`
+	updateUserPassQuery  = `UPDATE user SET pass = ? WHERE user = ?`
+	updateUserRoleQuery  = `UPDATE user SET role = ? WHERE user = ?`
+	deleteUserQuery      = `DELETE FROM user WHERE user = ?`
+
+	upsertUserAccessQuery = `
 		INSERT INTO access (user, topic, read, write) 
 		VALUES (?, ?, ?, ?)
 		ON CONFLICT (user, topic) DO UPDATE SET read=excluded.read, write=excluded.write
 	`
-	deleteUserQuery      = `DELETE FROM user WHERE user = ?`
-	deleteAllAccessQuery = `DELETE FROM access WHERE user = ?`
-	deleteAccessQuery    = `DELETE FROM access WHERE user = ? AND topic = ?`
+	selectUserAccessQuery  = `SELECT topic, read, write FROM access WHERE user = ?`
+	deleteAllAccessQuery   = `DELETE FROM access`
+	deleteUserAccessQuery  = `DELETE FROM access WHERE user = ?`
+	deleteTopicAccessQuery = `DELETE FROM access WHERE user = ? AND topic = ?`
 )
 
 type SQLiteAuth struct {
@@ -106,6 +104,9 @@ func setupNewAuthDB(db *sql.DB) error {
 }
 
 func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) {
+	if username == Everyone {
+		return nil, ErrUnauthorized
+	}
 	rows, err := a.db.Query(selectUserQuery, username)
 	if err != nil {
 		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
 	// rows (one for everyone, and one for the user), but prioritizes the user. The value for
 	// user.Name may be empty (= everyone).
-	var username string
+	username := Everyone
 	if user != nil {
 		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 {
-	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
+	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 	if err != nil {
 		return err
 	}
@@ -180,7 +181,7 @@ func (a *SQLiteAuth) RemoveUser(username string) error {
 	if _, err := a.db.Exec(deleteUserQuery, username); err != nil {
 		return err
 	}
-	if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil {
+	if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil {
 		return err
 	}
 	return nil
@@ -211,10 +212,18 @@ func (a *SQLiteAuth) Users() ([]*User, error) {
 		}
 		users = append(users, user)
 	}
+	everyone, err := a.everyoneUser()
+	if err != nil {
+		return nil, err
+	}
+	users = append(users, everyone)
 	return users, nil
 }
 
 func (a *SQLiteAuth) User(username string) (*User, error) {
+	if username == Everyone {
+		return a.everyoneUser()
+	}
 	urows, err := a.db.Query(selectUserQuery, username)
 	if err != nil {
 		return nil, err
@@ -229,18 +238,44 @@ func (a *SQLiteAuth) User(username string) (*User, error) {
 	} else if err := urows.Err(); err != nil {
 		return nil, err
 	}
-	arows, err := a.db.Query(selectUserTopicPermsQuery, username)
+	grants, err := a.readGrants(username)
+	if err != nil {
+		return nil, err
+	}
+	return &User{
+		Name:   username,
+		Pass:   hash,
+		Role:   Role(role),
+		Grants: grants,
+	}, 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 arows.Close()
+	defer rows.Close()
 	grants := make([]Grant, 0)
-	for arows.Next() {
+	for rows.Next() {
 		var topic string
 		var read, write bool
-		if err := arows.Scan(&topic, &read, &write); err != nil {
+		if err := rows.Scan(&topic, &read, &write); err != nil {
 			return nil, err
-		} else if err := arows.Err(); err != nil {
+		} else if err := rows.Err(); err != nil {
 			return nil, err
 		}
 		grants = append(grants, Grant{
@@ -249,16 +284,11 @@ func (a *SQLiteAuth) User(username string) (*User, error) {
 			Write: write,
 		})
 	}
-	return &User{
-		Name:   username,
-		Pass:   hash,
-		Role:   Role(role),
-		Grants: grants,
-	}, nil
+	return grants, nil
 }
 
 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 {
 		return err
 	}
@@ -273,29 +303,32 @@ func (a *SQLiteAuth) ChangeRole(username string, role Role) error {
 		return err
 	}
 	if role == RoleAdmin {
-		if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil {
+		if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil {
 			return err
 		}
 	}
 	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 {
-	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 nil
 }
 
 func (a *SQLiteAuth) ResetAccess(username string, topic string) error {
-	if topic == "" {
-		if _, err := a.db.Exec(deleteAllAccessQuery, username); err != nil {
-			return err
-		}
-	} else {
-		if _, err := a.db.Exec(deleteAccessQuery, username, topic); err != nil {
-			return err
-		}
+	if username == "" && topic == "" {
+		_, err := a.db.Exec(deleteAllAccessQuery, username)
+		return err
+	} else if topic == "" {
+		_, err := a.db.Exec(deleteUserAccessQuery, username)
+		return err
 	}
-	return nil
+	_, err := a.db.Exec(deleteTopicAccessQuery, username, topic)
+	return err
 }

+ 174 - 0
cmd/access.go

@@ -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
+}

+ 1 - 2
cmd/app.go

@@ -37,8 +37,7 @@ func New() *cli.App {
 			// Server commands
 			cmdServe,
 			cmdUser,
-			cmdAllow,
-			cmdDeny,
+			cmdAccess,
 
 			// Client commands
 			cmdPublish,

+ 1 - 25
cmd/user.go

@@ -159,31 +159,7 @@ func execUserList(c *cli.Context) error {
 	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
+	return showUsers(c, manager, users)
 }
 
 func createAuthManager(c *cli.Context) (auth.Manager, error) {

+ 0 - 108
cmd/user_allow.go

@@ -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
-}

+ 0 - 35
cmd/user_deny.go

@@ -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)
-}

+ 1 - 0
server/errors.go

@@ -41,6 +41,7 @@ var (
 	errHTTPBadRequestWebSocketsUpgradeHeaderMissing  = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", ""}
 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
 	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"}
 	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"}

+ 1 - 1
server/server.go

@@ -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 {
 			log.Printf("unauthorized: %s", err.Error())
-			return errHTTPUnauthorized
+			return errHTTPForbidden
 		}
 		return next(w, r, v)
 	}