From 78c8b040c22f1f601015937bb632d94fa0eda38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Z=C3=B6ller?= Date: Wed, 8 Apr 2026 13:41:50 +0200 Subject: [PATCH] Fix too restricted visibility for followers --- .../fitpub/controller/ActivityController.java | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/javahippie/fitpub/controller/ActivityController.java b/src/main/java/net/javahippie/fitpub/controller/ActivityController.java index ca5cab1..f6181f7 100644 --- a/src/main/java/net/javahippie/fitpub/controller/ActivityController.java +++ b/src/main/java/net/javahippie/fitpub/controller/ActivityController.java @@ -11,6 +11,7 @@ import net.javahippie.fitpub.model.dto.ActivityUploadRequest; import net.javahippie.fitpub.model.entity.Activity; import net.javahippie.fitpub.model.entity.PrivacyZone; import net.javahippie.fitpub.model.entity.User; +import net.javahippie.fitpub.repository.FollowRepository; import net.javahippie.fitpub.repository.UserRepository; import net.javahippie.fitpub.service.ActivityFileService; import net.javahippie.fitpub.service.ActivityImageService; @@ -50,6 +51,7 @@ public class ActivityController { private final ActivityFileService activityFileService; private final FitFileService fitFileService; private final UserRepository userRepository; + private final FollowRepository followRepository; private final ActivityPostProcessingService activityPostProcessingService; private final FederationService federationService; private final ActivityImageService activityImageService; @@ -62,6 +64,40 @@ public class ActivityController { @Value("${fitpub.base-url}") private String baseUrl; + /** + * Checks whether a viewer is allowed to see a non-public activity. Caller + * has already established that visibility != PUBLIC and that the viewer is + * authenticated. + * + * Rules: + * - PRIVATE: only the owner. + * - FOLLOWERS: the owner, or any local user with an ACCEPTED follow row + * pointing at the owner's actor URI ({@code baseUrl + "/users/" + username}). + * + * The follow row uses the actor URI rather than a user-id FK because the + * same table also stores follows of remote actors. + */ + private boolean canViewNonPublicActivity(Activity activity, UUID viewerId) { + if (viewerId == null) { + return false; + } + if (viewerId.equals(activity.getUserId())) { + return true; + } + if (activity.getVisibility() != Activity.Visibility.FOLLOWERS) { + return false; + } + User owner = userRepository.findById(activity.getUserId()).orElse(null); + if (owner == null) { + return false; + } + String ownerActorUri = baseUrl + "/users/" + owner.getUsername(); + return followRepository + .findByFollowerIdAndFollowingActorUri(viewerId, ownerActorUri) + .filter(f -> f.getStatus() == net.javahippie.fitpub.model.entity.Follow.FollowStatus.ACCEPTED) + .isPresent(); + } + private void populatePeaks(net.javahippie.fitpub.model.dto.ActivityDTO dto, UUID activityId) { var activityPeaks = activityPeakRepository.findByActivityId(activityId); if (!activityPeaks.isEmpty()) { @@ -202,14 +238,13 @@ public class ActivityController { UUID userId = getUserId(userDetails); - // Check if user has access (owner or follower) - Activity checkedActivity = fitFileService.getActivity(id, userId); - if (checkedActivity == null) { + // Check if user has access (owner, or follower for FOLLOWERS visibility) + if (!canViewNonPublicActivity(activity, userId)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } // Apply privacy filtering (owner sees full track, others see filtered) - ActivityDTO dto = ActivityDTO.fromEntityWithFiltering(checkedActivity, requestingUserId, privacyZones, trackPrivacyFilter); + ActivityDTO dto = ActivityDTO.fromEntityWithFiltering(activity, requestingUserId, privacyZones, trackPrivacyFilter); populatePeaks(dto, id); reactionEnricher.enrichSingle(dto, requestingUserId); return ResponseEntity.ok(dto); @@ -437,9 +472,8 @@ public class ActivityController { UUID userId = getUserId(userDetails); - // Check if user owns the activity - if (!activity.getUserId().equals(userId)) { - // TODO: Check if user is following the activity owner (for FOLLOWERS visibility) + // Owner, or accepted follower for FOLLOWERS visibility + if (!canViewNonPublicActivity(activity, userId)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } }