diff --git a/cmd/serve.go b/cmd/serve.go index 0cfc1cbc..9712f94f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -52,6 +52,7 @@ var flagsServe = append( altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-access", Aliases: []string{"auth_access"}, EnvVars: []string{"NTFY_AUTH_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-tokens", Aliases: []string{"auth_tokens"}, EnvVars: []string{"NTFY_AUTH_TOKENS"}, Usage: "pre-provisioned declarative access tokens"}), + altsrc.NewBoolFlag(&cli.BoolFlag{Name: "auth-access-cache", Aliases: []string{"auth_access_cache"}, EnvVars: []string{"NTFY_AUTH_ACCESS_CACHE"}, Value: user.DefaultAccessCacheEnabled, Usage: "enables the in-memory ACL cache (high-volume servers only)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files, or S3 URL (s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT])"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), @@ -168,6 +169,7 @@ func execServe(c *cli.Context) error { authUsersRaw := c.StringSlice("auth-users") authAccessRaw := c.StringSlice("auth-access") authTokensRaw := c.StringSlice("auth-tokens") + authAccessCacheEnabled := c.Bool("auth-access-cache") attachmentCacheDir := c.String("attachment-cache-dir") attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") @@ -468,6 +470,7 @@ func execServe(c *cli.Context) error { conf.AuthUsers = authUsers conf.AuthAccess = authAccess conf.AuthTokens = authTokens + conf.AuthAccessCacheEnabled = authAccessCacheEnabled conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit diff --git a/cmd/user.go b/cmd/user.go index cd6cf795..2e5af5f4 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -378,6 +378,7 @@ func createUserManager(c *cli.Context) (*user.Manager, error) { ProvisionEnabled: false, // Hack: Do not re-provision users on manager initialization BcryptCost: user.DefaultUserPasswordBcryptCost, QueueWriterInterval: user.DefaultUserStatsQueueWriterInterval, + AccessCacheEnabled: false, // Do not cache for CLI commands } if databaseURL != "" { host, dbErr := pg.Open(databaseURL) diff --git a/cmd/user_test.go b/cmd/user_test.go index ed6f5de4..a6250b72 100644 --- a/cmd/user_test.go +++ b/cmd/user_test.go @@ -1,14 +1,15 @@ package cmd import ( + "os" + "path/filepath" + "testing" + "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" "heckel.io/ntfy/v2/server" "heckel.io/ntfy/v2/test" "heckel.io/ntfy/v2/user" - "os" - "path/filepath" - "testing" ) func TestCLI_User_Add(t *testing.T) { @@ -128,6 +129,7 @@ func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config, conf.File = configFile conf.AuthFile = filepath.Join(t.TempDir(), "user.db") conf.AuthDefault = user.PermissionDenyAll + conf.AuthAccessCacheEnabled = false s, port = test.StartServerWithConfig(t, conf) return } diff --git a/docs/config.md b/docs/config.md index c934143a..bc241a4f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2284,6 +2284,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `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 (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`. | +| `auth-access-cache` | `NTFY_AUTH_ACCESS_CACHE` | *bool* | false | Enables an in-memory ACL cache so authorization checks no longer hit the database. Only worth enabling on high-volume servers. | | `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) | | `proxy-trusted-hosts` | `NTFY_PROXY_TRUSTED_HOSTS` | *comma-separated host/IP/CIDR list* | - | Comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header | @@ -2295,7 +2296,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled | | `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled | | `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled | -| `smtp-sender-verify` | `NTFY_SMTP_SENDER_VERIFY` | *bool* | `false` | If true, require verified email addresses for email notifications; anonymous email sending is disabled | +| `smtp-sender-verify` | `NTFY_SMTP_SENDER_VERIFY` | *bool* | `false` | If true, require verified email addresses for email notifications; anonymous email sending is disabled | | `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` | | `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` | | `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | *string* | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` | @@ -2392,6 +2393,7 @@ OPTIONS: --auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE] --auth-startup-queries value, --auth_startup_queries value queries run when the auth database is initialized [$NTFY_AUTH_STARTUP_QUERIES] --auth-default-access value, --auth_default_access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS] + --auth-access-cache, --auth_access_cache enables the in-memory ACL cache (high-volume servers only) (default: false) [$NTFY_AUTH_ACCESS_CACHE] --attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files, or S3 URL (s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT][&disable_http2=true]) [$NTFY_ATTACHMENT_CACHE_DIR] --attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: "5G") [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT] --attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: "15M") [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT] diff --git a/docs/releases.md b/docs/releases.md index df18add5..2853f25d 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1926,6 +1926,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ### ntfy server v2.24.0 (UNRELEASED) +**Features:** + +* Add opt-in in-memory ACL cache (`auth-access-cache`) that serves topic authorization without a database round-trip; off by default, intended for high-volume servers + **Bug fixes + maintenance:** * Extend account token automatically from the PWA service worker, so installed PWAs don't get logged out ([#1669](https://github.com/binwiederhier/ntfy/pull/1669), [#1203](https://github.com/binwiederhier/ntfy/issues/1203), [#1533](https://github.com/binwiederhier/ntfy/issues/1533), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves) for the contribution) diff --git a/server/config.go b/server/config.go index 1cfed3fc..b7dadddf 100644 --- a/server/config.go +++ b/server/config.go @@ -116,6 +116,8 @@ type Config struct { AuthTokens map[string][]*user.Token AuthBcryptCost int AuthStatsQueueWriterInterval time.Duration + AuthAccessCacheEnabled bool // Enables the in-memory ACL cache (high volume servers only) + AuthAccessCacheReloadInterval time.Duration // Reload interval for access cache, relevant for ACL writes from CLI AttachmentCacheDir string AttachmentTotalSizeLimit int64 AttachmentFileSizeLimit int64 @@ -223,6 +225,8 @@ func NewConfig() *Config { AuthDefault: user.PermissionReadWrite, AuthBcryptCost: user.DefaultUserPasswordBcryptCost, AuthStatsQueueWriterInterval: user.DefaultUserStatsQueueWriterInterval, + AuthAccessCacheEnabled: user.DefaultAccessCacheEnabled, + AuthAccessCacheReloadInterval: user.DefaultAccessCacheReloadInterval, AttachmentCacheDir: "", AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit, AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit, diff --git a/server/server.go b/server/server.go index 7ca0b4e7..c380bd26 100644 --- a/server/server.go +++ b/server/server.go @@ -247,16 +247,18 @@ func New(conf *Config) (*Server, error) { var userManager *user.Manager if conf.AuthFile != "" || pool != nil { authConfig := &user.Config{ - Filename: conf.AuthFile, - DatabaseURL: conf.DatabaseURL, - StartupQueries: conf.AuthStartupQueries, - DefaultAccess: conf.AuthDefault, - ProvisionEnabled: true, // Enable provisioning of users and access - Users: conf.AuthUsers, - Access: conf.AuthAccess, - Tokens: conf.AuthTokens, - BcryptCost: conf.AuthBcryptCost, - QueueWriterInterval: conf.AuthStatsQueueWriterInterval, + Filename: conf.AuthFile, + DatabaseURL: conf.DatabaseURL, + StartupQueries: conf.AuthStartupQueries, + DefaultAccess: conf.AuthDefault, + ProvisionEnabled: true, // Enable provisioning of users and access + Users: conf.AuthUsers, + Access: conf.AuthAccess, + Tokens: conf.AuthTokens, + BcryptCost: conf.AuthBcryptCost, + QueueWriterInterval: conf.AuthStatsQueueWriterInterval, + AccessCacheEnabled: conf.AuthAccessCacheEnabled, + AccessCacheReloadInterval: conf.AuthAccessCacheReloadInterval, } if pool != nil { userManager, err = user.NewPostgresManager(pool, authConfig) diff --git a/server/server.yml b/server/server.yml index 08161dc2..ee23dd8b 100644 --- a/server/server.yml +++ b/server/server.yml @@ -116,6 +116,8 @@ # - auth-tokens is a list of access tokens that are automatically created when the server starts. # Each entry is in the format ":[: