diff --git a/cmd/serve.go b/cmd/serve.go index 62c88b96..313ec835 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -325,8 +325,8 @@ func execServe(c *cli.Context) error { return errors.New("if upstream-base-url is set, base-url must also be set") } else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL { return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications") - } else if authFile == "" && (enableSignup || enableLogin || requireLogin || enableReservations || stripeSecretKey != "") { - return errors.New("cannot set enable-signup, enable-login, require-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set") + } else if authFile == "" && databaseURL == "" && (enableSignup || enableLogin || requireLogin || enableReservations || stripeSecretKey != "") { + return errors.New("cannot set enable-signup, enable-login, require-login, enable-reserve-topics, or stripe-secret-key if auth-file or database-url is not set") } else if enableSignup && !enableLogin { return errors.New("cannot set enable-signup without also setting enable-login") } else if requireLogin && !enableLogin { @@ -335,8 +335,8 @@ func execServe(c *cli.Context) error { return errors.New("cannot set stripe-secret-key or stripe-webhook-key, support for payments is not available in this build (nopayments)") } else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") { return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set") - } else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") { - return errors.New("if twilio-account is set, twilio-auth-token, twilio-phone-number, twilio-verify-service, base-url, and auth-file must also be set") + } else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || (authFile == "" && databaseURL == "")) { + return errors.New("if twilio-account is set, twilio-auth-token, twilio-phone-number, twilio-verify-service, base-url, and auth-file (or database-url) must also be set") } else if messageSizeLimit > server.DefaultMessageSizeLimit { log.Warn("message-size-limit is greater than 4K, this is not recommended and largely untested, and may lead to issues with some clients") if messageSizeLimit > 5*1024*1024 { diff --git a/db/db.go b/db/db.go index 60807b4a..4d402c41 100644 --- a/db/db.go +++ b/db/db.go @@ -10,7 +10,14 @@ import ( _ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver ) -const defaultMaxOpenConns = 10 +const ( + paramMaxOpenConns = "pool_max_conns" + paramMaxIdleConns = "pool_max_idle_conns" + paramConnMaxLifetime = "pool_conn_max_lifetime" + paramConnMaxIdleTime = "pool_conn_max_idle_time" + + defaultMaxOpenConns = 10 +) // Open opens a PostgreSQL database connection pool from a DSN string. It supports custom // query parameters for pool configuration: pool_max_conns (default 10), pool_max_idle_conns, @@ -22,19 +29,19 @@ func Open(dsn string) (*sql.DB, error) { return nil, fmt.Errorf("invalid database URL: %w", err) } q := u.Query() - maxOpenConns, err := extractIntParam(q, "pool_max_conns", defaultMaxOpenConns) + maxOpenConns, err := extractIntParam(q, paramMaxOpenConns, defaultMaxOpenConns) if err != nil { return nil, err } - maxIdleConns, err := extractIntParam(q, "pool_max_idle_conns", 0) + maxIdleConns, err := extractIntParam(q, paramMaxIdleConns, 0) if err != nil { return nil, err } - connMaxLifetime, err := extractDurationParam(q, "pool_conn_max_lifetime", 0) + connMaxLifetime, err := extractDurationParam(q, paramConnMaxLifetime, 0) if err != nil { return nil, err } - connMaxIdleTime, err := extractDurationParam(q, "pool_conn_max_idle_time", 0) + connMaxIdleTime, err := extractDurationParam(q, paramConnMaxIdleTime, 0) if err != nil { return nil, err } diff --git a/docs/config.md b/docs/config.md index ff47a7fb..e9cd7add 100644 --- a/docs/config.md +++ b/docs/config.md @@ -153,8 +153,12 @@ database-url: "postgres://user:pass@host:5432/ntfy" ``` When `database-url` is set, ntfy will use PostgreSQL for all database-backed stores (message cache, user manager, -and web push subscriptions) instead of SQLite. The `cache-file`, `auth-file`, and `web-push-file` options are not -required in this case. +and web push subscriptions) instead of SQLite. The `cache-file`, `auth-file`, and `web-push-file` options must not +be set in this case. + +Note that setting `database-url` implicitly enables authentication and access control (equivalent to setting +`auth-file` with SQLite). The default access is `read-write`, so anonymous users can still read and write to all +topics. To restrict access, set `auth-default-access` to `deny-all` (see [access control](#access-control)). You can also set this via the environment variable `NTFY_DATABASE_URL` or the command line flag `--database-url`. @@ -215,14 +219,15 @@ and `visitor-attachment-daily-bandwidth-limit`. Setting these conservatively is By default, the ntfy server is open for everyone, meaning **everyone can read and write to any topic** (this is how ntfy.sh is configured). To restrict access to your own server, you can optionally configure authentication and authorization. -ntfy's auth is implemented with a simple [SQLite](https://www.sqlite.org/)-based backend. It implements two roles -(`user` and `admin`) and per-topic `read` and `write` permissions using an [access control list (ACL)](https://en.wikipedia.org/wiki/Access-control_list). -Access control entries can be applied to users as well as the special everyone user (`*`), which represents anonymous API access. +ntfy's auth implements two roles (`user` and `admin`) and per-topic `read` and `write` permissions using an +[access control list (ACL)](https://en.wikipedia.org/wiki/Access-control_list). Access control entries can be applied +to users as well as the special everyone user (`*`), which represents anonymous API access. To set up auth, **configure the following options**: -* `auth-file` is the user/access database; it is created automatically if it doesn't already exist; suggested - location `/var/lib/ntfy/user.db` (easiest if deb/rpm package is used) +* `auth-file` is the user/access database (SQLite); it is created automatically if it doesn't already exist; suggested + location `/var/lib/ntfy/user.db` (easiest if deb/rpm package is used). Alternatively, if `database-url` is set, + auth is automatically enabled using PostgreSQL (see [PostgreSQL database](#postgresql-database)). * `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`. **If you are setting up a private instance, you'll want to set this to `deny-all`** (see [private instance example](#example-private-instance)). @@ -1795,7 +1800,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#message-cache) | | `cache-batch-size` | `NTFY_CACHE_BATCH_SIZE` | *int* | 0 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous) | | `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) | -| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | +| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control (SQLite). If set, enables authentication and access control. Not required if `database-url` is set. See [access control](#access-control). | | `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. | | `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) | | `proxy-forwarded-header` | `NTFY_PROXY_FORWARDED_HEADER` | *string* | `X-Forwarded-For` | Use specified header to determine visitor IP address (for rate limiting) | diff --git a/docs/releases.md b/docs/releases.md index fd71cc0d..aff0483e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1720,4 +1720,4 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** -* Add PostgreSQL as an alternative database backend for the web push subscription store via `database-url` config option +* Add PostgreSQL as an alternative database backend for all stores (message cache, user manager, web push subscriptions) via `database-url` config option, using a single shared connection pool with configurable pool settings (`pool_max_conns`, `pool_max_idle_conns`, `pool_conn_max_lifetime`, `pool_conn_max_idle_time`) diff --git a/server/config.go b/server/config.go index 85a2c2b2..786f0d78 100644 --- a/server/config.go +++ b/server/config.go @@ -88,7 +88,6 @@ var ( // Config is the main config struct for the application. Use New to instantiate a default config struct. type Config struct { File string // Config file, only used for testing - DatabaseURL string // PostgreSQL connection string (e.g. "postgres://user:pass@host:5432/ntfy") BaseURL string ListenHTTP string ListenHTTPS string @@ -96,6 +95,7 @@ type Config struct { ListenUnixMode fs.FileMode KeyFile string CertFile string + DatabaseURL string // PostgreSQL connection string (e.g. "postgres://user:pass@host:5432/ntfy") FirebaseKeyFile string CacheFile string CacheDuration time.Duration @@ -193,7 +193,6 @@ type Config struct { func NewConfig() *Config { return &Config{ File: DefaultConfigFile, // Only used for testing - DatabaseURL: "", BaseURL: "", ListenHTTP: DefaultListenHTTP, ListenHTTPS: "", @@ -201,6 +200,7 @@ func NewConfig() *Config { ListenUnixMode: 0, KeyFile: "", CertFile: "", + DatabaseURL: "", FirebaseKeyFile: "", CacheFile: "", CacheDuration: DefaultCacheDuration, diff --git a/server/server.yml b/server/server.yml index b3f5e11e..389fae80 100644 --- a/server/server.yml +++ b/server/server.yml @@ -40,7 +40,10 @@ # If "database-url" is set, ntfy will use PostgreSQL for all database-backed stores (message cache, # user manager, and web push subscriptions) instead of SQLite. When set, the "cache-file", -# "auth-file", and "web-push-file" options are not required. +# "auth-file", and "web-push-file" options must not be set. +# +# Note: Setting "database-url" implicitly enables authentication and access control. +# The default access is "read-write" (see "auth-default-access"). # # You can append connection pool parameters as query parameters: # pool_max_conns=10 - Maximum number of open connections (default: 10) @@ -90,6 +93,8 @@ # 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. # +# Note: If "database-url" is set, auth is implicitly enabled and "auth-file" must not be set. +# # - 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".