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:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user