amir20/dozzle · warning
Forbidden
Error message
Forbidden
What it means
The notifications API is wrapped by requireNotificationsRole middleware. When an authentication provider other than NONE is configured, the middleware requires the authenticated user to hold the auth.Notifications role. If there is no user in the request context, or the user lacks the role, the middleware returns 'Forbidden' with HTTP 403 and logs a warning.
Solutions
- Grant the user the notifications role in users.yml, e.g. add 'notifications' to the roles list: roles: [viewer, notifications].
- If using forward-proxy auth, ensure the proxy passes the group/header that maps to the notifications role.
- Re-login to obtain a fresh JWT after changing roles.
- If you do not need auth, set the authorization provider to none so the middleware is bypassed (only for trusted networks).
Example fix
// before: data/users.yml admin: email: admin@example.com password: "$2a$..." roles: [admin] // after: keep admin role, add notifications admin: email: admin@example.com password: "$2a$..." roles: [admin, notifications]
Defensive patterns
Strategy: type-guard
Validate before calling
// check the current user's roles before showing notification management UI
const canManage = config.user?.roles?.includes('notifications');
if (!canManage) hideNotificationsAdmin(); Type guard
function canManageNotifications(user) {
return !!user && Array.isArray(user.roles) && user.roles.includes('notifications');
} Prevention
- Assign the notifications role to every user who needs alert configuration
- Re-login after role changes so the JWT reflects new roles
- Forward the correct auth headers/groups in forward-proxy setups
When it happens
Trigger: Any POST/PUT/DELETE to notification endpoints (rules, destinations) while authentication is enabled (simple file-based users.yml or forward-proxy auth) and the current user has not been granted the notifications role, e.g. a user entry without 'notifications' in its roles list.
Common situations: Admin added a users.yml entry with only [viewer] roles and then tries to configure alert destinations in the UI; forward-proxy (Authelia) auth maps the user but roles are derived from groups that do not include notifications; a stale JWT from before roles were changed.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of amir20/dozzle@d9463cbe21 (2026-09-07).
Data as JSON: /api/errors/31e6b382cf961e35.
Report an issue: GitHub.
Appendix: source
Thrown at internal/web/notifications.go:548
writeError(w, http.StatusBadRequest, "invalid id")
return
}
h.hostService.RemoveDispatcher(id)
w.WriteHeader(http.StatusNoContent)
}
// requireNotificationsRole gates the notification rule and dispatcher APIs.
// Rules stream log lines from whatever containers their expression matches and
// dispatchers hold the destinations (and their secrets), neither of which is
// scoped per user, so this is a role rather than a label check.
func (h *handler) requireNotificationsRole(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context())
if user == nil || !user.Roles.Has(auth.Notifications) {
log.Warn().Msg("user is not permitted to manage notifications")
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
}
next.ServeHTTP(w, r)
})
}
const (
// previewLogWindow is how far back the log preview reads. Kept in the response so the
// form can describe the window it is showing without hardcoding a duplicate value.
previewLogWindow = 2 * time.Hour
// previewMaxLogs is how many matching log lines are returned as examples.
previewMaxLogs = 10
// previewMaxLogContainers caps how many matched containers are read for logs. A filter
// like `state == "running"` can match hundreds of containers and reading all of them
// would block the drawer for the full request timeout.
previewMaxLogContainers = 10
// previewMaxMetricSamples caps how many per-container metric rows are returned.View on GitHub (pinned to d9463cbe21)