halo-dev/halo · error · AccessDeniedException
You have no permission to delete this notification.
Error message
You have no permission to delete this notification.
What it means
Thrown as AccessDeniedException by DefaultNotificationService.deleteByName when the fetched Notification's spec.recipient does not equal the requesting username. Notifications are user-scoped; only the recipient may delete their own notification.
Source
Thrown at application/src/main/java/run/halo/app/notification/DefaultNotificationService.java:56
notification.getSpec().setLastReadAt(Instant.now());
return client.update(notification);
});
}
@Override
public Flux<String> markSpecifiedAsRead(String username, List<String> names) {
return Flux.fromIterable(names)
.flatMap(name -> markAsRead(username, name))
.map(notification -> notification.getMetadata().getName());
}
@Override
public Mono<Notification> deleteByName(String username, String name) {
return client.get(Notification.class, name)
.doOnNext(notification -> {
var recipient = notification.getSpec().getRecipient();
if (!username.equals(recipient)) {
throw new AccessDeniedException("You have no permission to delete this notification.");
}
})
.flatMap(client::delete);
}
static boolean isRecipient(Notification notification, String username) {
Assert.notNull(notification, "Notification must not be null");
Assert.notNull(username, "Username must not be null");
return username.equals(notification.getSpec().getRecipient());
}
}
View on GitHub (pinned to d2f5165f9c)
Solutions
- Ensure the client only issues deletes for notifications owned by the current user.
- Verify the notification name corresponds to the recipient before calling delete.
- If legitimate admin deletion is needed, use an admin-scoped API rather than the user endpoint.
- Check that the authenticated principal matches the expected username.
Example fix
// before
notificationService.deleteByName(currentUser, someName).block();
// after
Notification n = client.get(Notification.class, someName).block();
if (!currentUser.equals(n.getSpec().getRecipient())) {
return Mono.error(new AccessDeniedException("Not owner"));
}
notificationService.deleteByName(currentUser, someName).block(); Defensive patterns
Strategy: validation
Validate before calling
Notification n = client.get(Notification.class, name).block();
if (n == null || !currentUser.equals(n.getSpec().getRecipient())) {
return Mono.error(new AccessDeniedException("Not the recipient"));
} Type guard
static boolean isOwner(Notification n, String user) {
return n != null && n.getSpec() != null && user.equals(n.getSpec().getRecipient());
} Try / catch
notificationService.deleteByName(currentUser, name)
.onErrorResume(AccessDeniedException.class, e -> ServerResponse.status(HttpStatus.FORBIDDEN).build()); Prevention
- Drive deletes from lists scoped to the current user only.
- Do not expose another user's notification names in the UI.
- Treat AccessDeniedException as 403 and never leak which name was attempted.
When it happens
Trigger: An authenticated request to delete a notification whose spec.recipient is a different user than the current principal; e.g. a user guesses/iterates another user's notification name and calls delete.
Common situations: Client passing the wrong notification name; a shared/migrated account; frontend caching a stale notification name; a permission boundary bug where a non-owner action is exposed in the UI.
Related errors
- unable to determine kind and namespace from url, %s
- ESM UI provider output must expose a default PluginModule ex
- ESM UI provider output must contain at most one entry styles
- Cannot resolve shared dependency ${root} from ${resolutionBa
- Shared dependency ${root} resolved to ${packageJson.name ||
AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14).
Data as JSON: /api/errors/b8ea0ba536a202b7.
Report an issue: GitHub.