fix: allow any user permission to read/update own notification preferences

@RequirePermission now accepts Permission[] so a single annotation can
express "any of these" rather than a single required permission.

PermissionAspect updated accordingly — all existing single-value usages
compile unchanged (Java auto-wraps scalars in arrays for annotation attrs).

NotificationController: preference endpoints (GET/PUT /api/users/me/
notification-preferences) override the class-level READ_ALL gate with
{READ_ALL, WRITE_ALL, ANNOTATE_ALL} so users without READ_ALL can still
manage their own settings. Notification list endpoints retain READ_ALL.

UserSearchController: same broadened set so ANNOTATE_ALL users can search
for users to @mention when writing comments.

Tests: added WRITE_ALL and ANNOTATE_ALL passing cases for preferences and
user search; added 403 case for preferences with no permission; confirmed
WRITE_ALL cannot reach notification list endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-28 08:05:15 +01:00
parent dc6ea080c4
commit 23d0005514
6 changed files with 80 additions and 9 deletions

View File

@@ -51,12 +51,14 @@ public class NotificationController {
}
@GetMapping("/api/users/me/notification-preferences")
@RequirePermission({Permission.READ_ALL, Permission.WRITE_ALL, Permission.ANNOTATE_ALL})
public NotificationPreferenceDTO getPreferences(Authentication authentication) {
AppUser user = resolveUser(authentication);
return new NotificationPreferenceDTO(user.isNotifyOnReply(), user.isNotifyOnMention());
}
@PutMapping("/api/users/me/notification-preferences")
@RequirePermission({Permission.READ_ALL, Permission.WRITE_ALL, Permission.ANNOTATE_ALL})
public NotificationPreferenceDTO updatePreferences(
@RequestBody NotificationPreferenceDTO dto,
Authentication authentication) {

View File

@@ -14,7 +14,7 @@ import java.util.List;
@RestController
@RequiredArgsConstructor
@RequirePermission(Permission.READ_ALL)
@RequirePermission({Permission.READ_ALL, Permission.WRITE_ALL, Permission.ANNOTATE_ALL})
public class UserSearchController {
private final UserSearchService userSearchService;

View File

@@ -23,7 +23,7 @@ public class PermissionAspect {
RequirePermission permission = getAnnotation(joinPoint);
if (permission != null) {
validateUserAccess(permission.value());
validateUserAccess(permission.value()); // value() is now Permission[]
}
return joinPoint.proceed();
@@ -43,18 +43,23 @@ public class PermissionAspect {
return joinPoint.getTarget().getClass().getAnnotation(RequirePermission.class);
}
private void validateUserAccess(Permission requiredPerm) {
private void validateUserAccess(Permission[] requiredPerms) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
throw DomainException.unauthorized("Not authenticated");
}
boolean hasPermission = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals(requiredPerm.name()));
boolean hasAny = auth.getAuthorities().stream()
.anyMatch(a -> {
for (Permission p : requiredPerms) {
if (a.getAuthority().equals(p.name())) return true;
}
return false;
});
if (!hasPermission) {
throw DomainException.forbidden("Missing required permission: " + requiredPerm.name());
if (!hasAny) {
throw DomainException.forbidden("Missing required permission");
}
}
}

View File

@@ -8,5 +8,5 @@ import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
Permission value(); // e.g. "ADMIN" or "WRITE_ALL"
Permission[] value(); // one or more — user needs any of the listed permissions
}