diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeDraftsControllerIntegrationTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeDraftsControllerIntegrationTests.java index bb9ca62..2485b5d 100644 --- a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeDraftsControllerIntegrationTests.java +++ b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeDraftsControllerIntegrationTests.java @@ -86,8 +86,7 @@ public class RecipeDraftsControllerIntegrationTests { this.mockMvc.perform( get("/recipe-drafts/{fakeId}", UUID.randomUUID().toString()) ) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.message", is(notNullValue()))); + .andExpect(status().isUnauthorized()); } @Test diff --git a/src/main/java/app/mealsmadeeasy/api/GreetingController.java b/src/main/java/app/mealsmadeeasy/api/GreetingController.java index 961fd58..60be6d3 100644 --- a/src/main/java/app/mealsmadeeasy/api/GreetingController.java +++ b/src/main/java/app/mealsmadeeasy/api/GreetingController.java @@ -1,5 +1,9 @@ package app.mealsmadeeasy.api; +import app.mealsmadeeasy.api.security.EndpointAuthConfigurator; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; @@ -7,6 +11,16 @@ import org.springframework.web.bind.annotation.ResponseBody; @Controller public final class GreetingController { + @Component + public static class GreetingEndpointAuthConfigurator implements EndpointAuthConfigurator { + + @Override + public void configure(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + registry.requestMatchers("/greeting").permitAll(); + } + + } + @GetMapping("/greeting") @ResponseBody public String get() { diff --git a/src/main/java/app/mealsmadeeasy/api/auth/AuthEndpointAuthConfigurator.java b/src/main/java/app/mealsmadeeasy/api/auth/AuthEndpointAuthConfigurator.java new file mode 100644 index 0000000..937148f --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/AuthEndpointAuthConfigurator.java @@ -0,0 +1,16 @@ +package app.mealsmadeeasy.api.auth; + +import app.mealsmadeeasy.api.security.EndpointAuthConfigurator; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.stereotype.Component; + +@Component +public class AuthEndpointAuthConfigurator implements EndpointAuthConfigurator { + + @Override + public void configure(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + registry.requestMatchers("/auth/**").permitAll(); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageController.java b/src/main/java/app/mealsmadeeasy/api/image/ImageController.java index 40a7bb4..c426402 100644 --- a/src/main/java/app/mealsmadeeasy/api/image/ImageController.java +++ b/src/main/java/app/mealsmadeeasy/api/image/ImageController.java @@ -91,9 +91,6 @@ public class ImageController { @RequestParam(required = false) Set viewers, @AuthenticationPrincipal User principal ) throws IOException, ImageException { - if (principal == null) { - throw new AccessDeniedException("Must be logged in."); - } final var specBuilder = ImageCreateSpec.builder() .alt(alt) .caption(caption) @@ -119,9 +116,6 @@ public class ImageController { @PathVariable String filename, @RequestBody ImageUpdateBody body ) { - if (principal == null) { - throw new AccessDeniedException("Must be logged in."); - } final User owner = this.userService.getUser(username); final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal); final Image updated = this.imageService.update(image, principal, this.getImageUpdateSpec(body)); @@ -134,9 +128,6 @@ public class ImageController { @PathVariable String username, @PathVariable String filename ) throws IOException { - if (principal == null) { - throw new AccessDeniedException("Must be logged in."); - } final User owner = this.userService.getUser(username); final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal); this.imageService.deleteImage(image, principal); diff --git a/src/main/java/app/mealsmadeeasy/api/image/ImageEndpointAuthConfigurator.java b/src/main/java/app/mealsmadeeasy/api/image/ImageEndpointAuthConfigurator.java new file mode 100644 index 0000000..620895f --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/image/ImageEndpointAuthConfigurator.java @@ -0,0 +1,20 @@ +package app.mealsmadeeasy.api.image; + +import app.mealsmadeeasy.api.security.EndpointAuthConfigurator; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.stereotype.Component; + +@Component +public class ImageEndpointAuthConfigurator implements EndpointAuthConfigurator { + + @Override + public void configure(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + registry.requestMatchers(HttpMethod.GET, "/images/**").permitAll(); + registry.requestMatchers(HttpMethod.POST, "/images/**").authenticated(); + registry.requestMatchers(HttpMethod.PUT, "/images/**").authenticated(); + registry.requestMatchers(HttpMethod.DELETE, "/images/**").authenticated(); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeDraftsController.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeDraftsController.java index 87f2cc7..b4518ea 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeDraftsController.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeDraftsController.java @@ -10,7 +10,6 @@ import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec; import app.mealsmadeeasy.api.recipe.view.FullRecipeView; import app.mealsmadeeasy.api.recipe.view.RecipeDraftView; import app.mealsmadeeasy.api.user.User; -import app.mealsmadeeasy.api.util.MustBeLoggedInException; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -35,9 +34,6 @@ public class RecipeDraftsController { @GetMapping public ResponseEntity> getAllDraftsForUser(@AuthenticationPrincipal User user) { - if (user == null) { - throw new MustBeLoggedInException(); - } final List recipeDrafts = this.recipeService.getDrafts(user); return ResponseEntity.ok(recipeDrafts.stream() .map(recipeDraft -> this.draftToViewConverter.convert(recipeDraft, user)) @@ -50,18 +46,12 @@ public class RecipeDraftsController { @PathVariable UUID id, @AuthenticationPrincipal User viewer ) { - if (viewer == null) { - throw new MustBeLoggedInException(); - } final RecipeDraft recipeDraft = this.recipeService.getDraftByIdWithViewer(id, viewer); return ResponseEntity.ok(this.draftToViewConverter.convert(recipeDraft, viewer)); } @PostMapping("/manual") public ResponseEntity createManualDraft(@AuthenticationPrincipal User owner) { - if (owner == null) { - throw new MustBeLoggedInException(); - } final RecipeDraft recipeDraft = this.recipeService.createDraft(owner); return ResponseEntity.status(HttpStatus.CREATED).body(this.draftToViewConverter.convert(recipeDraft, owner)); } @@ -72,9 +62,6 @@ public class RecipeDraftsController { @RequestParam MultipartFile sourceFile, @RequestParam String sourceFileName ) throws IOException { - if (owner == null) { - throw new MustBeLoggedInException(); - } final File file = this.fileService.create( sourceFile.getInputStream(), sourceFileName, @@ -91,9 +78,6 @@ public class RecipeDraftsController { @PathVariable UUID id, @RequestBody RecipeDraftUpdateBody updateBody ) { - if (modifier == null) { - throw new MustBeLoggedInException(); - } final RecipeDraftUpdateSpec spec = this.updateBodyToSpecConverter.convert(updateBody, modifier); final RecipeDraft updated = this.recipeService.updateDraft(id, spec, modifier); return ResponseEntity.ok(this.draftToViewConverter.convert(updated, modifier)); @@ -104,9 +88,6 @@ public class RecipeDraftsController { @AuthenticationPrincipal User modifier, @PathVariable UUID id ) { - if (modifier == null) { - throw new MustBeLoggedInException(); - } this.recipeService.deleteDraft(id, modifier); return ResponseEntity.noContent().build(); } @@ -116,9 +97,6 @@ public class RecipeDraftsController { @AuthenticationPrincipal User modifier, @PathVariable UUID id ) { - if (modifier == null) { - throw new MustBeLoggedInException(); - } final Recipe recipe = this.recipeService.publishDraft(id, modifier); final FullRecipeView view = this.recipeToFullViewConverter.convert(recipe, false, modifier); return ResponseEntity.status(HttpStatus.CREATED).body(view); diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeDraftsEndpointAuthConfigurator.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeDraftsEndpointAuthConfigurator.java new file mode 100644 index 0000000..370c289 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeDraftsEndpointAuthConfigurator.java @@ -0,0 +1,16 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.security.EndpointAuthConfigurator; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.stereotype.Component; + +@Component +public class RecipeDraftsEndpointAuthConfigurator implements EndpointAuthConfigurator { + + @Override + public void configure(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + registry.requestMatchers("/recipe-drafts/**").authenticated(); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java index 6618801..eb5b490 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java @@ -23,7 +23,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -124,9 +123,6 @@ public class RecipesController { @PathVariable String slug, @Nullable @AuthenticationPrincipal User principal ) { - if (principal == null) { - throw new AccessDeniedException("Must be logged in to star a recipe."); - } return ResponseEntity.status(HttpStatus.CREATED).body(this.recipeStarService.create(username, slug, principal)); } @@ -136,9 +132,6 @@ public class RecipesController { @PathVariable String slug, @Nullable @AuthenticationPrincipal User principal ) { - if (principal == null) { - throw new AccessDeniedException("Must be logged in to get a recipe star."); - } final @Nullable RecipeStar star = this.recipeStarService.find(username, slug, principal).orElse(null); if (star != null) { return ResponseEntity.ok(Map.of("isStarred", true, "star", star)); @@ -153,9 +146,6 @@ public class RecipesController { @PathVariable String slug, @Nullable @AuthenticationPrincipal User principal ) { - if (principal == null) { - throw new AccessDeniedException("Must be logged in to delete a recipe star."); - } this.recipeStarService.delete(username, slug, principal); return ResponseEntity.noContent().build(); } @@ -183,9 +173,6 @@ public class RecipesController { @RequestBody RecipeCommentCreateBody body, @Nullable @AuthenticationPrincipal User principal ) { - if (principal == null) { - throw new AccessDeniedException("Must be logged in to comment on a recipe."); - } final RecipeComment comment = this.recipeCommentService.create(username, slug, principal, body); return ResponseEntity.ok(RecipeCommentView.from(comment, false)); } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipesEndpointAuthConfigurator.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipesEndpointAuthConfigurator.java new file mode 100644 index 0000000..3e5eb3d --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipesEndpointAuthConfigurator.java @@ -0,0 +1,20 @@ +package app.mealsmadeeasy.api.recipe; + +import app.mealsmadeeasy.api.security.EndpointAuthConfigurator; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.stereotype.Component; + +@Component +public class RecipesEndpointAuthConfigurator implements EndpointAuthConfigurator { + + @Override + public void configure(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + registry.requestMatchers(HttpMethod.GET, "/recipes/**").permitAll(); + registry.requestMatchers(HttpMethod.POST, "/recipes/**").authenticated(); + registry.requestMatchers(HttpMethod.PUT, "/recipes/**").authenticated(); + registry.requestMatchers(HttpMethod.DELETE, "/recipes/**").authenticated(); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/EndpointAuthConfigurator.java b/src/main/java/app/mealsmadeeasy/api/security/EndpointAuthConfigurator.java new file mode 100644 index 0000000..cb837dd --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/security/EndpointAuthConfigurator.java @@ -0,0 +1,8 @@ +package app.mealsmadeeasy.api.security; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +public interface EndpointAuthConfigurator { + void configure(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry); +} diff --git a/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java b/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java index 2f12c89..d454800 100644 --- a/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java +++ b/src/main/java/app/mealsmadeeasy/api/security/SecurityConfiguration.java @@ -19,6 +19,8 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import java.util.List; + @Configuration @EnableWebSecurity @EnableMethodSecurity @@ -26,15 +28,25 @@ public class SecurityConfiguration { private final JpaUserDetailsService jpaUserDetailsService; private final BeanFactory beanFactory; + private final List endpointAuthConfigurators; - public SecurityConfiguration(JpaUserDetailsService jpaUserDetailsService, BeanFactory beanFactory) { + public SecurityConfiguration( + JpaUserDetailsService jpaUserDetailsService, + BeanFactory beanFactory, + List endpointAuthConfigurators + ) { this.jpaUserDetailsService = jpaUserDetailsService; this.beanFactory = beanFactory; + this.endpointAuthConfigurators = endpointAuthConfigurators; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { - httpSecurity.authorizeHttpRequests(requests -> requests.anyRequest().permitAll()); + httpSecurity.authorizeHttpRequests(requests -> { + this.endpointAuthConfigurators.forEach(endpointAuthConfigurator -> { + endpointAuthConfigurator.configure(requests); + }); + }); httpSecurity.csrf(AbstractHttpConfigurer::disable); httpSecurity.cors(Customizer.withDefaults()); httpSecurity.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy( diff --git a/src/main/java/app/mealsmadeeasy/api/signup/SignUpEndpointAuthConfigurator.java b/src/main/java/app/mealsmadeeasy/api/signup/SignUpEndpointAuthConfigurator.java new file mode 100644 index 0000000..b708eee --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/signup/SignUpEndpointAuthConfigurator.java @@ -0,0 +1,16 @@ +package app.mealsmadeeasy.api.signup; + +import app.mealsmadeeasy.api.security.EndpointAuthConfigurator; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.stereotype.Component; + +@Component +public class SignUpEndpointAuthConfigurator implements EndpointAuthConfigurator { + + @Override + public void configure(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + registry.requestMatchers("/sign-up/**").permitAll(); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/util/ExceptionHandlers.java b/src/main/java/app/mealsmadeeasy/api/util/ExceptionHandlers.java index 91b07eb..2b1851c 100644 --- a/src/main/java/app/mealsmadeeasy/api/util/ExceptionHandlers.java +++ b/src/main/java/app/mealsmadeeasy/api/util/ExceptionHandlers.java @@ -22,16 +22,6 @@ public class ExceptionHandlers { )); } - public record MustBeLoggedInExceptionView(String message) {} - - @ExceptionHandler(MustBeLoggedInException.class) - public ResponseEntity handleMustBeLoggedInException( - MustBeLoggedInException e - ) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(new MustBeLoggedInExceptionView(e.getMessage())); - } - public record NoSuchEntityWithUsernameAndSlugExceptionView( String entityName, String username, @@ -56,4 +46,28 @@ public class ExceptionHandlers { )); } + public record NoSuchEntityWithUsernameAndFilenameExceptionView( + String entityName, + String username, + String filename, + String message + ) {} + + @ExceptionHandler(NoSuchEntityWithUsernameAndFilenameException.class) + public ResponseEntity handleNoSuchEntityWithUsernameAndFilename( + NoSuchEntityWithUsernameAndFilenameException e + ) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new NoSuchEntityWithUsernameAndFilenameExceptionView( + e.getEntityType().getSimpleName(), + e.getUsername(), + e.getFilename(), + String.format( + "No such entity %s for username %s and filename %s", + e.getEntityType().getSimpleName(), + e.getUsername(), + e.getFilename() + ) + )); + } + } diff --git a/src/main/java/app/mealsmadeeasy/api/util/MustBeLoggedInException.java b/src/main/java/app/mealsmadeeasy/api/util/MustBeLoggedInException.java deleted file mode 100644 index c5ec7f8..0000000 --- a/src/main/java/app/mealsmadeeasy/api/util/MustBeLoggedInException.java +++ /dev/null @@ -1,13 +0,0 @@ -package app.mealsmadeeasy.api.util; - -public class MustBeLoggedInException extends RuntimeException { - - public MustBeLoggedInException() { - super("Must be logged in to perform the requested operation."); - } - - public MustBeLoggedInException(String message) { - super(message); - } - -}