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

  1. Grant the user the notifications role in users.yml, e.g. add 'notifications' to the roles list: roles: [viewer, notifications].
  2. If using forward-proxy auth, ensure the proxy passes the group/header that maps to the notifications role.
  3. Re-login to obtain a fresh JWT after changing roles.
  4. 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

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.

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)