From 5cf92c55c647107067271211a54eb7426752a46e Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 1 Feb 2022 16:40:33 -0500 Subject: [PATCH] Docs and minor improvements to "ntfy access" --- .goreleaser.yml | 2 ++ auth/auth.go | 1 + auth/auth_sqlite.go | 61 ++++++++++++++++++++++++++++++++++++--------- cmd/access.go | 50 ++++++++++++++++++++++++++++++------- cmd/serve.go | 6 ++--- scripts/postinst.sh | 4 +-- server/server.yml | 30 ++++++++++++++++------ 7 files changed, 120 insertions(+), 34 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 74863ea5..69785225 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -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: diff --git a/auth/auth.go b/auth/auth.go index 669ca2e5..35b910c5 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,3 +1,4 @@ +// Package auth deals with authentication and authorization against topics package auth import ( diff --git a/auth/auth_sqlite.go b/auth/auth_sqlite.go index 4b7eb38c..b7dc2680 100644 --- a/auth/auth_sqlite.go +++ b/auth/auth_sqlite.go @@ -2,13 +2,16 @@ package auth import ( "database/sql" + "errors" + "fmt" _ "github.com/mattn/go-sqlite3" // SQLite driver "golang.org/x/crypto/bcrypt" "strings" ) const ( - bcryptCost = 11 + 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 +} diff --git a/cmd/access.go b/cmd/access.go index b67463b7..d8a61912 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -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 ( userEveryone = "everyone" @@ -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) diff --git a/cmd/serve.go b/cmd/serve.go index 67c56eb4..7435189d 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -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 == "-" { diff --git a/scripts/postinst.sh b/scripts/postinst.sh index 8e19d659..e4ee165a 100755 --- a/scripts/postinst.sh +++ b/scripts/postinst.sh @@ -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" diff --git a/server/server.yml b/server/server.yml index 45d86327..f8921c84 100644 --- a/server/server.yml +++ b/server/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. # -# key-file: -# cert-file: +# key-file: +# cert-file: # 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 . # # cache-file: - -# 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 . +# +# auth-file: +# 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. #