fix(security): restrict DRAFT list to author — prevent cross-user draft leak
GeschichteService.list() now applies hasAuthor(currentUser()) whenever status == DRAFT, so BLOG_WRITE users cannot read other users' unpublished stories. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,8 +77,10 @@ public class GeschichteService {
|
|||||||
GeschichteStatus effective = currentUserHasBlogWrite() ? status : GeschichteStatus.PUBLISHED;
|
GeschichteStatus effective = currentUserHasBlogWrite() ? status : GeschichteStatus.PUBLISHED;
|
||||||
int safeLimit = limit <= 0 ? DEFAULT_LIMIT : Math.min(limit, MAX_LIMIT);
|
int safeLimit = limit <= 0 ? DEFAULT_LIMIT : Math.min(limit, MAX_LIMIT);
|
||||||
|
|
||||||
|
UUID authorId = effective == GeschichteStatus.DRAFT ? currentUser().getId() : null;
|
||||||
Specification<Geschichte> spec = Specification.allOf(
|
Specification<Geschichte> spec = Specification.allOf(
|
||||||
GeschichteSpecifications.hasStatus(effective),
|
GeschichteSpecifications.hasStatus(effective),
|
||||||
|
GeschichteSpecifications.hasAuthor(authorId),
|
||||||
GeschichteSpecifications.hasAllPersons(personIds),
|
GeschichteSpecifications.hasAllPersons(personIds),
|
||||||
GeschichteSpecifications.hasDocument(documentId),
|
GeschichteSpecifications.hasDocument(documentId),
|
||||||
GeschichteSpecifications.orderByDisplayDateDesc()
|
GeschichteSpecifications.orderByDisplayDateDesc()
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ public final class GeschichteSpecifications {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Specification<Geschichte> hasAuthor(UUID authorId) {
|
||||||
|
return (root, query, cb) ->
|
||||||
|
authorId == null ? null : cb.equal(root.get("author").get("id"), authorId);
|
||||||
|
}
|
||||||
|
|
||||||
public static Specification<Geschichte> hasDocument(UUID documentId) {
|
public static Specification<Geschichte> hasDocument(UUID documentId) {
|
||||||
return (root, query, cb) -> {
|
return (root, query, cb) -> {
|
||||||
if (documentId == null) return null;
|
if (documentId == null) return null;
|
||||||
|
|||||||
@@ -159,6 +159,26 @@ class GeschichteServiceIntegrationTest {
|
|||||||
.isEmpty();
|
.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void list_DRAFT_does_not_return_other_users_drafts() {
|
||||||
|
// writer creates a draft; writer2 (also BLOG_WRITE) should not see it
|
||||||
|
AppUser writer2 = appUserRepository.save(AppUser.builder()
|
||||||
|
.email("writer2-int@test")
|
||||||
|
.password("hash")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
authenticateAs(writer, Permission.BLOG_WRITE);
|
||||||
|
GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
|
||||||
|
dto.setTitle("Writer 1 draft");
|
||||||
|
dto.setBody("<p>private</p>");
|
||||||
|
geschichteService.create(dto);
|
||||||
|
|
||||||
|
authenticateAs(writer2, Permission.BLOG_WRITE);
|
||||||
|
List<Geschichte> result = geschichteService.list(GeschichteStatus.DRAFT, List.of(), null, 50);
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private UUID publishedStoryWithPersons(String title, List<UUID> personIds) {
|
private UUID publishedStoryWithPersons(String title, List<UUID> personIds) {
|
||||||
GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
|
GeschichteUpdateDTO dto = new GeschichteUpdateDTO();
|
||||||
dto.setTitle(title);
|
dto.setTitle(title);
|
||||||
|
|||||||
Reference in New Issue
Block a user