Docs and minor improvements to "ntfy access"
parent
e56eb0c178
commit
5cf92c55c6
|
@ -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:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Package auth deals with authentication and authorization against topics
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -2,6 +2,8 @@ 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"
|
||||||
|
@ -9,6 +11,7 @@ import (
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
@ -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),
|
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)
|
||||||
|
|
|
@ -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 == "-" {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
#
|
#
|
||||||
|
|
Loading…
Reference in New Issue