Docs and minor improvements to "ntfy access"

This commit is contained in:
Philipp Heckel 2022-02-01 16:40:33 -05:00
parent e56eb0c178
commit 5cf92c55c6
7 changed files with 120 additions and 34 deletions

View file

@ -61,6 +61,8 @@ nfpms:
type: dir type: dir
- dst: /var/cache/ntfy/attachments - dst: /var/cache/ntfy/attachments
type: dir type: dir
- dst: /var/lib/ntfy
type: dir
- dst: /usr/share/ntfy/logo.png - dst: /usr/share/ntfy/logo.png
src: server/static/img/ntfy.png src: server/static/img/ntfy.png
scripts: scripts:

View file

@ -1,3 +1,4 @@
// Package auth deals with authentication and authorization against topics
package auth package auth
import ( import (

View file

@ -2,13 +2,16 @@ package auth
import ( import (
"database/sql" "database/sql"
"errors"
"fmt"
_ "github.com/mattn/go-sqlite3" // SQLite driver _ "github.com/mattn/go-sqlite3" // SQLite driver
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"strings" "strings"
) )
const ( const (
bcryptCost = 11 bcryptCost = 11
intentionalSlowDownHash = "$2a$11$eX15DeF27FwAgXt9wqJF0uAUMz74XywJcGBH3kP93pzKYv6ATk2ka" // Cost should match bcryptCost
) )
// Auther-related queries // Auther-related queries
@ -27,7 +30,7 @@ const (
write INT NOT NULL, write INT NOT NULL,
PRIMARY KEY (topic, user) PRIMARY KEY (topic, user)
); );
CREATE TABLE IF NOT EXISTS schema_version ( CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY, id INT PRIMARY KEY,
version INT NOT NULL version INT NOT NULL
); );
@ -61,6 +64,13 @@ const (
deleteTopicAccessQuery = `DELETE FROM access WHERE user = ? AND topic = ?` deleteTopicAccessQuery = `DELETE FROM access WHERE user = ? AND topic = ?`
) )
// Schema management queries
const (
currentSchemaVersion = 1
insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)`
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
)
// SQLiteAuth is an implementation of Auther and Manager. It stores users and access control list // SQLiteAuth is an implementation of Auther and Manager. It stores users and access control list
// in a SQLite database. // in a SQLite database.
type SQLiteAuth struct { type SQLiteAuth struct {
@ -78,7 +88,7 @@ func NewSQLiteAuth(filename string, defaultRead, defaultWrite bool) (*SQLiteAuth
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := setupNewAuthDB(db); err != nil { if err := setupAuthDB(db); err != nil {
return nil, err return nil, err
} }
return &SQLiteAuth{ return &SQLiteAuth{
@ -88,14 +98,6 @@ func NewSQLiteAuth(filename string, defaultRead, defaultWrite bool) (*SQLiteAuth
}, nil }, nil
} }
func setupNewAuthDB(db *sql.DB) error {
if _, err := db.Exec(createAuthTablesQueries); err != nil {
return err
}
// FIXME schema version
return nil
}
// Authenticate checks username and password and returns a user if correct. The method // Authenticate checks username and password and returns a user if correct. The method
// returns in constant-ish time, regardless of whether the user exists or the password is // returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect. // correct or incorrect.
@ -105,7 +107,7 @@ func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) {
} }
user, err := a.User(username) user, err := a.User(username)
if err != nil { if err != nil {
bcrypt.CompareHashAndPassword([]byte("$2a$11$eX15DeF27FwAgXt9wqJF0uAUMz74XywJcGBH3kP93pzKYv6ATk2ka"), bcrypt.CompareHashAndPassword([]byte(intentionalSlowDownHash),
[]byte("intentional slow-down to avoid timing attacks")) []byte("intentional slow-down to avoid timing attacks"))
return nil, ErrUnauthenticated return nil, ErrUnauthenticated
} }
@ -360,3 +362,38 @@ func toSQLWildcard(s string) string {
func fromSQLWildcard(s string) string { func fromSQLWildcard(s string) string {
return strings.ReplaceAll(s, "%", "*") return strings.ReplaceAll(s, "%", "*")
} }
func setupAuthDB(db *sql.DB) error {
// If 'schemaVersion' table does not exist, this must be a new database
rowsSV, err := db.Query(selectSchemaVersionQuery)
if err != nil {
return setupNewAuthDB(db)
}
defer rowsSV.Close()
// If 'schemaVersion' table exists, read version and potentially upgrade
schemaVersion := 0
if !rowsSV.Next() {
return errors.New("cannot determine schema version: database file may be corrupt")
}
if err := rowsSV.Scan(&schemaVersion); err != nil {
return err
}
rowsSV.Close()
// Do migrations
if schemaVersion == currentSchemaVersion {
return nil
}
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
}
func setupNewAuthDB(db *sql.DB) error {
if _, err := db.Exec(createAuthTablesQueries); err != nil {
return err
}
if _, err := db.Exec(insertSchemaVersion, currentSchemaVersion); err != nil {
return err
}
return nil
}

View file

@ -10,16 +10,9 @@ import (
/* /*
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 ( const (
userEveryone = "everyone" userEveryone = "everyone"
@ -38,9 +31,45 @@ var cmdAccess = &cli.Command{
Before: initConfigFileInputSource("config", flagsAccess), Before: initConfigFileInputSource("config", flagsAccess),
Action: execUserAccess, Action: execUserAccess,
Category: categoryServer, Category: categoryServer,
Description: `Manage the access control list for the ntfy server.
This is a server-only command. It directly manages the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
to the related command 'ntfy user'.
The command allows you to show the access control list, as well as change it, depending on how
it is called.
Usage:
ntfy access # Shows the entire access control list
ntfy access USERNAME # Shows access control entries for USERNAME
ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC
Arguments:
USERNAME an existing user, as created with 'ntfy user add'
TOPIC name of a topic with optional wildcards, e.g. "mytopic*"
PERMISSION one of the following:
- read-write (alias: rw)
- read-only (aliases: read, ro)
- write-only (aliases: write, wo)
- deny (alias: none)
Examples:
ntfy access
ntfy access phil # Shows access for user phil
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 everyone "up*" write # Allow anonymous write-only access to topics "up..."
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
`,
} }
func execUserAccess(c *cli.Context) error { func execUserAccess(c *cli.Context) error {
if c.NArg() > 3 {
return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
}
manager, err := createAuthManager(c) manager, err := createAuthManager(c)
if err != nil { if err != nil {
return err return err
@ -53,6 +82,9 @@ func execUserAccess(c *cli.Context) error {
perms := c.Args().Get(2) perms := c.Args().Get(2)
reset := c.Bool("reset") reset := c.Bool("reset")
if reset { if reset {
if perms != "" {
return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
}
return resetAccess(c, manager, username, topic) return resetAccess(c, manager, username, topic)
} else if perms == "" { } else if perms == "" {
return showAccess(c, manager, username) return showAccess(c, manager, username)

View file

@ -131,13 +131,13 @@ func execServe(c *cli.Context) error {
return errors.New("if attachment-cache-dir is set, base-url must also be set") return errors.New("if attachment-cache-dir is set, base-url must also be set")
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { } else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
return errors.New("if set, base-url must start with http:// or https://") return errors.New("if set, base-url must start with http:// or https://")
} else if !util.InStringList([]string{"read-write", "read-only", "deny-all"}, authDefaultAccess) { } else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'") return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
} }
// Default auth permissions // Default auth permissions
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only" authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
authDefaultWrite := authDefaultAccess == "read-write" authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
// Special case: Unset default // Special case: Unset default
if listenHTTP == "-" { if listenHTTP == "-" {

View file

@ -8,8 +8,8 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
if [ -d /run/systemd/system ]; then if [ -d /run/systemd/system ]; then
# Create ntfy user/group # Create ntfy user/group
id ntfy >/dev/null 2>&1 || useradd --system --no-create-home ntfy id ntfy >/dev/null 2>&1 || useradd --system --no-create-home ntfy
chown ntfy.ntfy /var/cache/ntfy /var/cache/ntfy/attachments chown ntfy.ntfy /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
chmod 700 /var/cache/ntfy /var/cache/ntfy/attachments chmod 700 /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
# Hack to change permissions on cache file # Hack to change permissions on cache file
configfile="/etc/ntfy/server.yml" configfile="/etc/ntfy/server.yml"

View file

@ -21,8 +21,8 @@
# Path to the private key & cert file for the HTTPS web server. Not used if "listen-https" is not set. # Path to the private key & cert file for the HTTPS web server. Not used if "listen-https" is not set.
# #
# key-file: # key-file: <filename>
# cert-file: # cert-file: <filename>
# If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. # If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app.
# This is optional and only required to save battery when using the Android app. # This is optional and only required to save battery when using the Android app.
@ -32,6 +32,8 @@
# If set, messages are cached in a local SQLite database instead of only in-memory. This # If set, messages are cached in a local SQLite database instead of only in-memory. This
# allows for service restarts without losing messages in support of the since= parameter. # allows for service restarts without losing messages in support of the since= parameter.
# #
# The "cache-duration" parameter defines the duration for which messages will be buffered
# before they are deleted. This is required to support the "since=..." and "poll=1" parameter.
# To disable the cache entirely (on-disk/in-memory), set "cache-duration" to 0. # To disable the cache entirely (on-disk/in-memory), set "cache-duration" to 0.
# The cache file is created automatically, provided that the correct permissions are set. # The cache file is created automatically, provided that the correct permissions are set.
# #
@ -44,14 +46,26 @@
# ntfy user and group by running: chown ntfy.ntfy <filename>. # ntfy user and group by running: chown ntfy.ntfy <filename>.
# #
# cache-file: <filename> # cache-file: <filename>
# Duration for which messages will be buffered before they are deleted.
# This is required to support the "since=..." and "poll=1" parameter.
#
# You can disable the cache entirely by setting this to 0.
#
# cache-duration: "12h" # cache-duration: "12h"
# If set, access to the ntfy server and API can be controlled on a granular level using
# the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs.
#
# - auth-file is the SQLite user/access database; it is created automatically if it doesn't already exist
# - auth-default-access defines the default/fallback access if no access control entry is found; it can be
# set to "read-write" (default), "read-only", "write-only" or "deny-all".
#
# Debian/RPM package users:
# Use /var/lib/ntfy/user.db as user database to avoid permission issues. The package
# creates this folder for you.
#
# Check your permissions:
# If you are running ntfy with systemd, make sure this user database file is owned by the
# ntfy user and group by running: chown ntfy.ntfy <filename>.
#
# auth-file: <filename>
# auth-default-access: "read-write"
# If set, the X-Forwarded-For header is used to determine the visitor IP address # If set, the X-Forwarded-For header is used to determine the visitor IP address
# instead of the remote address of the connection. # instead of the remote address of the connection.
# #