{"record":{"id":"31e6b382cf961e35","repo":"amir20/dozzle","slug":"forbidden-31e6b3","errorCode":null,"errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"internal/web/notifications.go","lineNumber":548,"sourceCode":"\t\twriteError(w, http.StatusBadRequest, \"invalid id\")\n\t\treturn\n\t}\n\n\th.hostService.RemoveDispatcher(id)\n\tw.WriteHeader(http.StatusNoContent)\n}\n\n// requireNotificationsRole gates the notification rule and dispatcher APIs.\n// Rules stream log lines from whatever containers their expression matches and\n// dispatchers hold the destinations (and their secrets), neither of which is\n// scoped per user, so this is a role rather than a label check.\nfunc (h *handler) requireNotificationsRole(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif h.config.Authorization.Provider != NONE {\n\t\t\tuser := auth.UserFromContext(r.Context())\n\t\t\tif user == nil || !user.Roles.Has(auth.Notifications) {\n\t\t\t\tlog.Warn().Msg(\"user is not permitted to manage notifications\")\n\t\t\t\thttp.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nconst (\n\t// previewLogWindow is how far back the log preview reads. Kept in the response so the\n\t// form can describe the window it is showing without hardcoding a duplicate value.\n\tpreviewLogWindow = 2 * time.Hour\n\t// previewMaxLogs is how many matching log lines are returned as examples.\n\tpreviewMaxLogs = 10\n\t// previewMaxLogContainers caps how many matched containers are read for logs. A filter\n\t// like `state == \"running\"` can match hundreds of containers and reading all of them\n\t// would block the drawer for the full request timeout.\n\tpreviewMaxLogContainers = 10\n\t// previewMaxMetricSamples caps how many per-container metric rows are returned.","sourceCodeStart":530,"sourceCodeEnd":566,"githubUrl":"https://github.com/amir20/dozzle/blob/d9463cbe21874e44ab79db6fa63e746ca7d22928/internal/web/notifications.go#L530-L566","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before: data/users.yml\nadmin:\n  email: admin@example.com\n  password: \"$2a$...\"\n  roles: [admin]\n// after: keep admin role, add notifications\nadmin:\n  email: admin@example.com\n  password: \"$2a$...\"\n  roles: [admin, notifications]","handlingStrategy":"type-guard","validationCode":"// check the current user's roles before showing notification management UI\nconst canManage = config.user?.roles?.includes('notifications');\nif (!canManage) hideNotificationsAdmin();","typeGuard":"function canManageNotifications(user) {\n  return !!user && Array.isArray(user.roles) && user.roles.includes('notifications');\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["auth","authorization","rbac"],"backgroundTag":"permission-denied","analyzedSha":"d9463cbe21874e44ab79db6fa63e746ca7d22928","analyzedAt":"2026-09-07T10:08:55.855Z","contentChangedAt":"2026-09-07T10:08:55.855Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}