From ae27172f8d751cd550243cd9e3072dfb63941f27 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 1 Jun 2026 20:30:05 -0400 Subject: [PATCH] Add trace logs --- cmd/user_test.go | 10 +++++----- docs/config.md | 4 ++-- user/access_cache.go | 24 +++++++++++++++++++++++- user/manager.go | 9 ++++----- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/cmd/user_test.go b/cmd/user_test.go index 91373694..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,8 +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 - // Cache is off by default (matches self-hoster setup), so the server reads - // authorizations directly from the DB and sees CLI mutations immediately. + conf.AuthAccessCacheEnabled = false s, port = test.StartServerWithConfig(t, conf) return } diff --git a/docs/config.md b/docs/config.md index af6b9ec9..bc241a4f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2284,7 +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 snapshot of the access control list so authorization checks no longer hit the database. Off by default; only worth enabling on high-volume servers. ACL changes from a separate `ntfy access` CLI invocation against the same database become visible within ~60s; the server's own changes are immediate. | +| `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 | @@ -2296,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-` | diff --git a/user/access_cache.go b/user/access_cache.go index b4a1641e..dc1d96f8 100644 --- a/user/access_cache.go +++ b/user/access_cache.go @@ -53,18 +53,40 @@ func newAccessCache() *accessCache { func (c *accessCache) Lookup(usernameOrEveryone, topic string) (read, write, found bool) { escapedTopic := escapeUnderscore(topic) c.mu.RLock() - defer c.mu.RUnlock() if usernameOrEveryone != Everyone { if entry, ok := c.pickBestNoLock(usernameOrEveryone, topic, escapedTopic); ok { + c.mu.RUnlock() + traceACLDecision(usernameOrEveryone, usernameOrEveryone, topic, entry.read, entry.write) return entry.read, entry.write, true } } if entry, ok := c.pickBestNoLock(Everyone, topic, escapedTopic); ok { + c.mu.RUnlock() + traceACLDecision(usernameOrEveryone, Everyone, topic, entry.read, entry.write) return entry.read, entry.write, true } + c.mu.RUnlock() + traceACLDecision(usernameOrEveryone, "", topic, false, false) return false, false, false } +// traceACLDecision logs an ACL lookup result +func traceACLDecision(requestUser, matchedUser, topic string, read, write bool) { + ev := log.Tag(tag). + Field("user_name", requestUser). + Field("topic", topic). + Field("read", read). + Field("write", write) + if !ev.IsTrace() { + return + } + if matchedUser == "" { + ev.Trace("ACL no match") + return + } + ev.Field("matched_user", matchedUser).Trace("ACL match") +} + // Reload scans (user_name, topic, read, write) rows and merges them into the // cache. With no usernames the cache is replaced wholesale; otherwise the // query is invoked with those usernames as positional args and only the diff --git a/user/manager.go b/user/manager.go index e1a25a04..bae32593 100644 --- a/user/manager.go +++ b/user/manager.go @@ -748,10 +748,10 @@ func (a *Manager) AllowReservation(username string, topic string) error { // The found return value indicates whether an ACL entry was found at all. // // Priority: -// - specific user beats Everyone -// - longer pattern beats shorter (a more specific rule beats a more general one, +// - Specific user beats Everyone +// - Longer pattern beats shorter (a more specific rule beats a more general one, // e.g. "test*" > "*") -// - write beats read at equal length +// - Write beats read at equal length // // When AccessCacheEnabled is true (config), the lookup is served entirely from // the in-memory snapshot maintained by accessCache. Otherwise the original SQL @@ -1634,11 +1634,10 @@ func (a *Manager) maybeProvisionTokens(tx *sql.Tx, provisionUsernames []string, } // Close stops background goroutines and closes the underlying database. -// Safe to call multiple times. func (a *Manager) Close() error { select { case <-a.quit: - // already closed + // Already closed default: close(a.quit) }