Auth CLI, continued

This commit is contained in:
Philipp Heckel 2022-01-23 23:02:39 -05:00
parent 03a4e3e8e9
commit 393f95aeac
6 changed files with 183 additions and 43 deletions

View file

@ -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 Name string
Role Role 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")
)

View file

@ -58,17 +58,19 @@ const (
// Manager-related queries // Manager-related queries
const ( const (
insertUser = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)` insertUserQuery = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
updateUserPass = `UPDATE user SET pass = ? WHERE user = ?` selectUsernamesQuery = `SELECT user FROM user ORDER BY role, user`
updateUserRole = `UPDATE user SET role = ? WHERE user = ?` selectUserTopicPermsQuery = `SELECT topic, read, write FROM access WHERE user = ?`
upsertAccess = ` 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 = ?` deleteUserQuery = `DELETE FROM user WHERE user = ?`
deleteAllAccess = `DELETE FROM access WHERE user = ?` deleteAllAccessQuery = `DELETE FROM access WHERE user = ?`
deleteAccess = `DELETE FROM access WHERE user = ? AND topic = ?` 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
} }
} }

View file

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

View file

@ -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) read := util.InStringList([]string{"", "read-write", "rw", "read-only", "read", "ro"}, perms)
write := util.InStringList([]string{"", "read-write", "write-only", "write", "wo"}, 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

View file

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

View file

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