More RecipeService security and tests.

This commit is contained in:
JesseBrault0709 2024-07-09 09:24:36 +02:00
parent d3a532fb12
commit 35ef2aa039
5 changed files with 77 additions and 23 deletions

View File

@ -64,7 +64,7 @@ public class RecipeServiceTests {
public void getByIdPublic() throws RecipeException { public void getByIdPublic() throws RecipeException {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.createTestUser("recipeOwner");
Recipe recipe = this.createTestRecipe(owner); Recipe recipe = this.createTestRecipe(owner);
recipe = this.recipeService.setPublic(recipe, true); recipe = this.recipeService.setPublic(recipe, owner, true);
final Recipe byId = this.recipeService.getById(recipe.getId()); final Recipe byId = this.recipeService.getById(recipe.getId());
assertThat(byId.getId(), is(recipe.getId())); assertThat(byId.getId(), is(recipe.getId()));
assertThat(byId.getTitle(), is("My Recipe")); assertThat(byId.getTitle(), is("My Recipe"));
@ -93,7 +93,7 @@ public class RecipeServiceTests {
public void getByIdOkayWhenPublic() { public void getByIdOkayWhenPublic() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.createTestUser("recipeOwner");
final Recipe notYetPublicRecipe = this.createTestRecipe(owner); final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, true); final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(publicRecipe.getId())); assertDoesNotThrow(() -> this.recipeService.getById(publicRecipe.getId()));
} }
@ -103,7 +103,7 @@ public class RecipeServiceTests {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.createTestUser("recipeOwner");
final User viewer = this.createTestUser("viewer"); final User viewer = this.createTestUser("viewer");
final Recipe notYetPublicRecipe = this.createTestRecipe(owner); final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, true); final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(publicRecipe.getId(), viewer)); assertDoesNotThrow(() -> this.recipeService.getById(publicRecipe.getId(), viewer));
} }
@ -112,7 +112,7 @@ public class RecipeServiceTests {
public void getByIdWithStarsPublic() throws RecipeException { public void getByIdWithStarsPublic() throws RecipeException {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.createTestUser("recipeOwner");
Recipe recipe = this.createTestRecipe(owner); Recipe recipe = this.createTestRecipe(owner);
recipe = this.recipeService.setPublic(recipe, true); recipe = this.recipeService.setPublic(recipe, owner, true);
final RecipeStar star = this.recipeService.addStar(recipe, owner); final RecipeStar star = this.recipeService.addStar(recipe, owner);
final Recipe byIdWithStars = this.recipeService.getByIdWithStars(recipe.getId()); final Recipe byIdWithStars = this.recipeService.getByIdWithStars(recipe.getId());
assertThat(byIdWithStars.getStars(), containsStars(star)); assertThat(byIdWithStars.getStars(), containsStars(star));
@ -132,7 +132,7 @@ public class RecipeServiceTests {
public void getByIdWithStarsOkayWhenPublic() { public void getByIdWithStarsOkayWhenPublic() {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.createTestUser("recipeOwner");
final Recipe notYetPublicRecipe = this.createTestRecipe(owner); final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, true); final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, owner, true);
assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId())); assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId()));
} }
@ -142,7 +142,7 @@ public class RecipeServiceTests {
final User owner = this.createTestUser("recipeOwner"); final User owner = this.createTestUser("recipeOwner");
final User viewer = this.createTestUser("viewer"); final User viewer = this.createTestUser("viewer");
final Recipe notYetPublicRecipe = this.createTestRecipe(owner); final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, true); final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, owner, true);
assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId(), viewer)); assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId(), viewer));
} }
@ -157,9 +157,9 @@ public class RecipeServiceTests {
Recipe r1 = this.createTestRecipe(owner); Recipe r1 = this.createTestRecipe(owner);
Recipe r2 = this.createTestRecipe(owner); Recipe r2 = this.createTestRecipe(owner);
r0 = this.recipeService.setPublic(r0, true); r0 = this.recipeService.setPublic(r0, owner, true);
r1 = this.recipeService.setPublic(r1, true); r1 = this.recipeService.setPublic(r1, owner, true);
r2 = this.recipeService.setPublic(r2, true); r2 = this.recipeService.setPublic(r2, owner, true);
// r0.stars = 0, r1.stars = 1, r2.stars = 2 // r0.stars = 0, r1.stars = 1, r2.stars = 2
this.recipeService.addStar(r1, u0); this.recipeService.addStar(r1, u0);
@ -236,8 +236,8 @@ public class RecipeServiceTests {
Recipe r0 = this.createTestRecipe(owner); Recipe r0 = this.createTestRecipe(owner);
Recipe r1 = this.createTestRecipe(owner); Recipe r1 = this.createTestRecipe(owner);
r0 = this.recipeService.setPublic(r0, true); r0 = this.recipeService.setPublic(r0, owner, true);
r1 = this.recipeService.setPublic(r1, true); r1 = this.recipeService.setPublic(r1, owner, true);
final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes(); final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes();
assertThat(publicRecipes.size(), is(2)); assertThat(publicRecipes.size(), is(2));
@ -274,10 +274,19 @@ public class RecipeServiceTests {
final Recipe recipe = this.recipeService.create( final Recipe recipe = this.recipeService.create(
owner, "My Recipe", "# A Heading" owner, "My Recipe", "# A Heading"
); );
final String rendered = this.recipeService.getRenderedMarkdown(recipe); final String rendered = this.recipeService.getRenderedMarkdown(recipe, owner);
assertThat(rendered, is("<h1>A Heading</h1>")); assertThat(rendered, is("<h1>A Heading</h1>"));
} }
@Test
@DirtiesContext
public void getRenderedMarkThrowsIfNotViewable() {
final User owner = this.createTestUser("recipeOwner");
final User notViewer = this.createTestUser("notViewer");
final Recipe recipe = this.createTestRecipe(owner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.getRenderedMarkdown(recipe, notViewer));
}
@Test @Test
@DirtiesContext @DirtiesContext
public void updateRawText() { public void updateRawText() {
@ -286,10 +295,22 @@ public class RecipeServiceTests {
owner, "My Recipe", "# A Heading" owner, "My Recipe", "# A Heading"
); );
final String newRawText = "# A Heading\n## A Subheading"; final String newRawText = "# A Heading\n## A Subheading";
recipe = this.recipeService.updateRawText(recipe, newRawText); recipe = this.recipeService.updateRawText(recipe, owner, newRawText);
assertThat(recipe.getRawText(), is(newRawText)); assertThat(recipe.getRawText(), is(newRawText));
} }
@Test
@DirtiesContext
public void updateRawTextThrowsIfNotOwner() {
final User owner = this.createTestUser("recipeOwner");
final User notOwner = this.createTestUser("notOwner");
final Recipe recipe = this.createTestRecipe(owner);
assertThrows(
AccessDeniedException.class,
() -> this.recipeService.updateRawText(recipe, notOwner, "should fail")
);
}
@Test @Test
@DirtiesContext @DirtiesContext
public void updateOwnerViaUser() throws RecipeException { public void updateOwnerViaUser() throws RecipeException {
@ -343,4 +364,22 @@ public class RecipeServiceTests {
assertThat(recipe.getStars(), is(empty())); assertThat(recipe.getStars(), is(empty()));
} }
@Test
@DirtiesContext
public void deleteRecipe() {
final User owner = this.createTestUser("recipeOwner");
final Recipe toDelete = this.createTestRecipe(owner);
this.recipeService.deleteRecipe(toDelete, owner);
assertThrows(RecipeException.class, () -> this.recipeService.getById(toDelete.getId(), owner));
}
@Test
@DirtiesContext
public void deleteRecipeThrowsIfNotOwner() {
final User owner = this.createTestUser("recipeOwner");
final User notOwner = this.createTestUser("notOwner");
final Recipe toDelete = this.createTestRecipe(owner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.deleteRecipe(toDelete, notOwner));
}
} }

View File

@ -4,5 +4,6 @@ import app.mealsmadeeasy.api.user.User;
public interface RecipeSecurity { public interface RecipeSecurity {
boolean isOwner(Recipe recipe, User user); boolean isOwner(Recipe recipe, User user);
boolean isOwner(long recipeId, User user) throws RecipeException;
boolean isViewableBy(Recipe recipe, User user); boolean isViewableBy(Recipe recipe, User user);
} }

View File

@ -19,6 +19,15 @@ public class RecipeSecurityImpl implements RecipeSecurity {
return recipe.getOwner() != null && recipe.getOwner().getId().equals(user.getId()); return recipe.getOwner() != null && recipe.getOwner().getId().equals(user.getId());
} }
@Override
public boolean isOwner(long recipeId, User user) throws RecipeException {
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID,
"No such Recipe with id " + recipeId
));
return this.isOwner(recipe, user);
}
@Override @Override
public boolean isViewableBy(Recipe recipe, User user) { public boolean isViewableBy(Recipe recipe, User user) {
if (Objects.equals(recipe.getOwner().getId(), user.getId())) { if (Objects.equals(recipe.getOwner().getId(), user.getId())) {

View File

@ -24,9 +24,9 @@ public interface RecipeService {
List<Recipe> getRecipesViewableBy(User user); List<Recipe> getRecipesViewableBy(User user);
List<Recipe> getRecipesOwnedBy(User user); List<Recipe> getRecipesOwnedBy(User user);
String getRenderedMarkdown(Recipe recipe); String getRenderedMarkdown(Recipe recipe, User viewer);
Recipe updateRawText(Recipe recipe, String newRawText); Recipe updateRawText(Recipe recipe, User owner, String newRawText);
Recipe updateOwner(Recipe recipe, User oldOwner, User newOwner) throws RecipeException; Recipe updateOwner(Recipe recipe, User oldOwner, User newOwner) throws RecipeException;
@ -34,7 +34,7 @@ public interface RecipeService {
void deleteStarByUser(Recipe recipe, User giver) throws RecipeException; void deleteStarByUser(Recipe recipe, User giver) throws RecipeException;
void deleteStar(RecipeStar recipeStar); void deleteStar(RecipeStar recipeStar);
Recipe setPublic(Recipe recipe, boolean isPublic); Recipe setPublic(Recipe recipe, User owner, boolean isPublic);
Recipe addViewer(Recipe recipe, User user); Recipe addViewer(Recipe recipe, User user);
Recipe removeViewer(Recipe recipe, User user); Recipe removeViewer(Recipe recipe, User user);
@ -47,7 +47,7 @@ public interface RecipeService {
void deleteComment(RecipeComment comment); void deleteComment(RecipeComment comment);
Recipe clearComments(Recipe recipe); Recipe clearComments(Recipe recipe);
void deleteRecipe(Recipe recipe); void deleteRecipe(Recipe recipe, User owner);
void deleteById(long id); void deleteById(long id, User owner);
} }

View File

@ -136,7 +136,8 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
public String getRenderedMarkdown(Recipe recipe) { @PreAuthorize("#recipe.isPublic || @recipeSecurity.isViewableBy(#recipe, #viewer)")
public String getRenderedMarkdown(Recipe recipe, User viewer) {
RecipeEntity entity = (RecipeEntity) recipe; RecipeEntity entity = (RecipeEntity) recipe;
if (entity.getCachedRenderedText() == null) { if (entity.getCachedRenderedText() == null) {
entity.setCachedRenderedText(renderAndCleanMarkdown(entity.getRawText())); entity.setCachedRenderedText(renderAndCleanMarkdown(entity.getRawText()));
@ -146,7 +147,8 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
public Recipe updateRawText(Recipe recipe, String newRawText) { @PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)")
public Recipe updateRawText(Recipe recipe, User owner, String newRawText) {
final RecipeEntity entity = (RecipeEntity) recipe; final RecipeEntity entity = (RecipeEntity) recipe;
entity.setCachedRenderedText(null); entity.setCachedRenderedText(null);
entity.setRawText(newRawText); entity.setRawText(newRawText);
@ -188,7 +190,8 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
public Recipe setPublic(Recipe recipe, boolean isPublic) { @PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)")
public Recipe setPublic(Recipe recipe, User owner, boolean isPublic) {
final RecipeEntity entity = (RecipeEntity) recipe; final RecipeEntity entity = (RecipeEntity) recipe;
entity.setPublic(isPublic); entity.setPublic(isPublic);
return this.recipeRepository.save(entity); return this.recipeRepository.save(entity);
@ -266,12 +269,14 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
public void deleteRecipe(Recipe recipe) { @PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)")
public void deleteRecipe(Recipe recipe, User owner) {
this.recipeRepository.delete((RecipeEntity) recipe); this.recipeRepository.delete((RecipeEntity) recipe);
} }
@Override @Override
public void deleteById(long id) { @PreAuthorize("@recipeSecurity.isOwner(#id, #owner)")
public void deleteById(long id, User owner) {
this.recipeRepository.deleteById(id); this.recipeRepository.deleteById(id);
} }