More auth

This commit is contained in:
Philipp Heckel 2022-01-24 00:54:28 -05:00
parent 393f95aeac
commit 460162737a
9 changed files with 264 additions and 230 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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