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
- dst: /var/cache/ntfy/attachments
type: dir
- dst: /var/lib/ntfy
type: dir
- dst: /usr/share/ntfy/logo.png
src: server/static/img/ntfy.png
scripts:

View file

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

View file

@ -2,6 +2,8 @@ package auth
import (
"database/sql"
"errors"
"fmt"
_ "github.com/mattn/go-sqlite3" // SQLite driver
"golang.org/x/crypto/bcrypt"
"strings"
@ -9,6 +11,7 @@ import (
const (
bcryptCost = 11
intentionalSlowDownHash = "$2a$11$eX15DeF27FwAgXt9wqJF0uAUMz74XywJcGBH3kP93pzKYv6ATk2ka" // Cost should match bcryptCost
)
// Auther-related queries
@ -27,7 +30,7 @@ const (
write INT NOT NULL,
PRIMARY KEY (topic, user)
);
CREATE TABLE IF NOT EXISTS schema_version (
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
version INT NOT NULL
);
@ -61,6 +64,13 @@ const (
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
// in a SQLite database.
type SQLiteAuth struct {
@ -78,7 +88,7 @@ func NewSQLiteAuth(filename string, defaultRead, defaultWrite bool) (*SQLiteAuth
if err != nil {
return nil, err
}
if err := setupNewAuthDB(db); err != nil {
if err := setupAuthDB(db); err != nil {
return nil, err
}
return &SQLiteAuth{
@ -88,14 +98,6 @@ func NewSQLiteAuth(filename string, defaultRead, defaultWrite bool) (*SQLiteAuth
}, 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
// returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect.
@ -105,7 +107,7 @@ func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) {
}
user, err := a.User(username)
if err != nil {
bcrypt.CompareHashAndPassword([]byte("$2a$11$eX15DeF27FwAgXt9wqJF0uAUMz74XywJcGBH3kP93pzKYv6ATk2ka"),
bcrypt.CompareHashAndPassword([]byte(intentionalSlowDownHash),
[]byte("intentional slow-down to avoid timing attacks"))
return nil, ErrUnauthenticated
}
@ -360,3 +362,38 @@ func toSQLWildcard(s string) string {
func fromSQLWildcard(s string) string {
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,14 +10,7 @@ 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
*/
@ -38,9 +31,45 @@ var cmdAccess = &cli.Command{
Before: initConfigFileInputSource("config", flagsAccess),
Action: execUserAccess,
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 {
if c.NArg() > 3 {
return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
}
manager, err := createAuthManager(c)
if err != nil {
return err
@ -53,6 +82,9 @@ func execUserAccess(c *cli.Context) error {
perms := c.Args().Get(2)
reset := c.Bool("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)
} else if perms == "" {
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")
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "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) {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'")
} 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', 'write-only' or 'deny-all'")
}
// Default auth permissions
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
authDefaultWrite := authDefaultAccess == "read-write"
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
// Special case: Unset default
if listenHTTP == "-" {

View file

@ -8,8 +8,8 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
if [ -d /run/systemd/system ]; then
# Create ntfy user/group
id ntfy >/dev/null 2>&1 || useradd --system --no-create-home ntfy
chown ntfy.ntfy /var/cache/ntfy /var/cache/ntfy/attachments
chmod 700 /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 /var/lib/ntfy
# Hack to change permissions on cache file
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.
#
# key-file:
# cert-file:
# key-file: <filename>
# cert-file: <filename>
# 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.
@ -32,6 +32,8 @@
# 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.
#
# 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.
# 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>.
#
# 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"
# 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
# instead of the remote address of the connection.
#