Custom Error message with #Preauthorize and ##ControllerAdvice - spring-mvc

We are using spring and spring-security-3.2. Recently We are adding annotations #PreAuthorize to RestAPIs(earlier it was URL based).
#PreAuthorize("hasPermission('salesorder','ViewSalesOrder')")
#RequestMapping(value = "/restapi/salesorders/", method = RequestMethod.GET)
public ModelAndView getSalesOrders(){}
We already have Global exception handler which annotated with - #ControllerAdvice and custom PermissionEvaluator in place, everything works fine except the error message.
Lets say some user is accessing API At moment without having 'ViewSalesOrder' permission then spring by default throws the exception 'Access is denied',but didn't tell which permission is missing (Its our requirement to mention which permission is missing).
Is it possible to throw an exception which also include the permission name, so final error message should be look like "Access is denied, you need ViewSalesOrder permission"(here permission name should be from #PreAuthorize annotation)?
Please note that we have 100 such restAPI in place so generic solution will be highly appreciated.

There is no pretty way of achieving what you expect since PermissionEvaluator interface doesn't let you pass the missing permission along with the result of the evaluation.
In addition, AccessDecisionManager decides on the final authorization with respect to the votes of the AccessDecisionVoter instances, one of which is PreInvocationAuthorizationAdviceVoter which votes with respect to the evaluation of #PreAuthorize value.
Long story short, PreInvocationAuthorizationAdviceVoter votes against the request (giving the request –1 point) when your custom PermissionEvaluator returns false to hasPermission call. As you see there is no way to propagate the cause of the failure in this flow.
On the other hand, you may try some workarounds to achieve what you want. One way can be to throw an exception within your custom PermissionEvaluator when permission check fails. You can use this exception to propagate the missing permission to your global exception handler. There, you can pass the missing permission to your message descriptors as a parameter. Beware that this will halt execution process of AccessDecisionManager which means successive voters will not be executed (defaults are RoleVoter and AuthenticatedVoter). You should be careful if you choose to go down this path.
Another safer but clumsier way can be to implement a custom AccessDeniedHandler and customize the error message before responding with 403. AccessDeniedHandler provides you current HttpServletRequest which can be used to retrieve the request URI. However, bad news in this case is, you need a URI to permission mapping in order to locate the missing permission.

I have implemented the second possible solution mentioned by Mert Z. My solution works only for #PreAuthorize annotations used in the API layer (e.g. with #RequestMapping). I have registered a custom AccessDeniedHandler bean in which I get the value of the #PreAuthorize annotation of the forbidden API method and fills it into error message.
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private DispatcherServlet dispatcherServlet;
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
List<HandlerMapping> handlerMappings = dispatcherServlet.getHandlerMappings();
if (handlerMappings != null) {
HandlerExecutionChain handler = null;
for (HandlerMapping handlerMapping : handlerMappings) {
try {
handler = handlerMapping.getHandler(request);
} catch (Exception e) {}
if (handler != null)
break;
}
if (handler != null && handler.getHandler() instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler.getHandler();
PreAuthorize methodAnnotation = method.getMethodAnnotation(PreAuthorize.class);
if (methodAnnotation != null) {
response.sendError(HttpStatus.FORBIDDEN.value(),
"Authorization condition not met: " + methodAnnotation.value());
return;
}
}
}
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
#Inject
public void setDispatcherServlet(DispatcherServlet dispatcherServlet) {
this.dispatcherServlet = dispatcherServlet;
}
}
The handler is registered in WebSecurityConfigurerAdapter:
#EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
#EnableWebSecurity
public abstract class BaseSecurityInitializer extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
...
}
#Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
}
Beware that if there is also a global resource exception handler with #ControllerAdvice the CustomAccessDeniedHandler won't be executed. I solved this by rethrowing the exception in the global handler (as advised here https://github.com/spring-projects/spring-security/issues/6908):
#ControllerAdvice
public class ResourceExceptionHandler {
#ExceptionHandler(AccessDeniedException.class)
public ResponseEntity accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
log.info(e.toString());
throw e;
}
}

You can throw an org.springframework.security.access.AccessDeniedException from a method that was called inside an EL-Expression:
#PreAuthorize("#myBean.myMethod(#myRequestParameter)")

Ideally, the #PreAuthorize annotation should be supporting String message(); in addition to the SpEl value. But, for whatever reason, it does not. Most of the suggestions here seem unnecessarily cumbersome and elaborate. As #lathspell has suggested, the simplest way to provide your own error message - along with any custom access validation logic - would be to add a simple method that performs the check and throws the AccessDeniedException in case the check fails, and then reference that method in the SpEl expression. Here's an example:
#RestController
#RequiredArgsConstructor // if you use lombok
public class OrderController {
private final OrderService orderService;
...
#GetMapping(value = "/salesorders", produces = MediaType.APPLICATION_JSON_VALUE)
#PreAuthorize("#orderController.hasPermissionToSeeOrders(#someArgOfThisMethod)")
public Page<OrderDto> getSalesOrders(
// someArgOfThisMethod here, perhaps HttpRequest, #PathVariable, #RequestParam, etc.
int pageIndex, int pageSize, String sortBy, String sortOrder) {
Pageable pageRequest = PageRequest.of(pageIndex, pageSize, Sort.Direction.fromString(sortOrder), sortBy);
return ordersService.retrieveSalesOrders(..., pageRequest);
}
public static Boolean hasPermissionToSeeOrders(SomeArgOfTheTargetMethod argToEvaluate) {
//check eligibility to perform the operation based on some data from the incoming objects (argToEvaluate)
if (condition fails) {
throw new AccessDeniedException("Your message");
}
return true;
}

Related

Error managment in ControllerAdvice lead to duplication in URI and 404

I have a simple rest controller :
#RestController
#RequestMapping("/api/v1/")
public class OrderController {
#RequestMapping(value = "/orders2", method = RequestMethod.POST)
public OrderDto createOrder2(#RequestBody OrderDto order) throws Exception {
throw new Exception("Bouh!");
}
}
And I want to manage exceptions globally. From what I read it can be done with something like :
#ControllerAdvice
public class ErrorController {
#ExceptionHandler(Exception.class)
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ErrorDto handleConflict(HttpServletRequest request, Exception e) throws Exception {
ErrorDto o = new ErrorDto ();
o.setMessage(e.getMessage());
return o;
}
}
But when I make a post on my request, I get the following error :
26/10/2016 17:26:08.187 [http-nio-8080-exec-12] WARN o.s.web.servlet.PageNotFound -
No mapping found for HTTP request with URI [/duorest/api/v1/api/v1/orders2]
in DispatcherServlet with name 'rest'
I don't know why the uri change to /duorest/api/v1/api/v1/orders2
Some facts :
I checked in debug, my code is executed
If I move the method in the rest controller, I get no error and what I expect (my ErrorDto object)
Spring framework version 4.3.3.RELEASE
Spring-data-rest-webmvc version 2.5.4.RELEASE
Anybody already had this problem ? Or any hint ?
Is it resolved? if not please try to execute with #ResponseBody is missing on the handleConflict method.

How to target specific handlers with a #ControllerAdvice #ModelAttribute?

I'd like to display a warning message on specific pages 5 minutes prior to a system shutdown. Rather than add it manually to each these pages I created a #ControllerAdvice class with a #ModelAttribute method that adds the message to the Model parameter, but from what I understand reading the documentation and SO and some initial testing this model attribute will be added to every method with a #RequestMapping.
I realize I could refactor my code so that the targeted methods are all in one controller and limit the #ControllerAdvice to that one controller, but I would end up with a collection of otherwise non-related methods in that controller which muddies up the overall structure of my controllers.
So, is there a way to indicate which specific methods in multiple controllers the #ModelAttribute is applied to? Would a custom annotation be a solution (not sure how that would work)? I'd like to do this via annotations if possible.
Edit:
The #ControllerAdvice code is pretty basic:
#ControllerAdvice
public class GlobalModelController {
private final Logger logger = LoggerFactory.getLogger(getClass());
#Autowired
private MaintenanceInterceptor maintInterceptor;
#ModelAttribute()
public void globalAttributes(Model model, Locale locale) {
if (maintInterceptor.isMaintenanceWindowSet() && !maintInterceptor.isMaintenanceInEffect()) {
String msg = maintInterceptor.getImminentMaint(locale);
model.addAttribute("warningMaint", msg);
logger.debug("maint msg= " + msg);
}
}
}
A controller advice can be limited to certain controllers (not methods) by using one of the values of the #ControllerAdvice annotation, e.g.
#ControllerAdvice(assignableTypes = {MyController1.class, MyController2.class})
If you need to do it on a method level I suggest to take a look at Interceptors.
Thanks to #zeroflagL for pointing me to the interceptor solution. I ditched the #ControllerAdvice approach and ended up with this:
Custom annotation:
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Qualifier
public #interface MaintAware {
String name() default "MaintAware";
}
Interceptor:
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
MaintAware maintAware = method.getAnnotation(MaintAware.class);
if (maintAware != null) {
Locale locale = request.getLocale();
if (isMaintenanceWindowSet() && !isMaintenanceInEffect()) {
String msg = getImminentMaint(locale);
if (!msg.isEmpty())
modelAndView.addObject("warningMaint", msg);
}
}
super.postHandle(request, response, handler, modelAndView);
}
Now I can annotate the specific methods that require the maintenance notification. Easy peasy. :)

Setting exception handled in Web API ExceptionFilterAttribute

Is there any way in ASP.NET Web API to mark an exception as handled in an ExceptionFilterAttribute?
I want to handle the exception at the method level with an exception filter and stop the propagation to a globally registered exception filter.
Filter used on a controller action:
public class MethodExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is NotImplementedException)
{
context.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(context.Exception.Message)
};
// here in MVC you could set context.ExceptionHandled = true;
}
}
}
The globally registered filter:
public class GlobalExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is SomeOtherException)
{
context.Response = new HttpResponseMessage(HttpStatusCode.SomethingElse)
{
Content = new StringContent(context.Exception.Message)
};
}
}
}
Try throwing an HttpResponseException at the end of your local handling. By design, they are not caught by exception filters.
throw new HttpResponseException(context.Response);
Web API 2 is designed with inversion of control in mind. You consider the possibility for the exception to already be handled, rather than interrupting the filter execution after you handle it.
In this sense, attributes deriving from ExceptionFilterAttribute should check if the exception is already handled, which your code already does since is operator returns false for null values. In addition, after you handle the exception, you set context.Exception to null in order to avoid further handling.
To achieve this in your code, you need to replace your comment from MethodExceptionFilterAttribute with context.Exception = null to clear the exception.
It is important to note that it is not a good idea to register more than one global exception filter, due to ordering issues. For information about the execution order of attribute filters in Web API, see the following thread Order of execution with multiple filters in web api.

Why Spring MVC does not allow to expose Model or BindingResult to an #ExceptionHandler?

Situation
I'm trying to group the code that logs the exceptions and render a nice view in a few methods. At the moment the logic is sometime in the #RequestHandler itself (in the a catch block), othertimes is delegated to an utility class (that works but moves the logic away from the place where the exception is thrown).
Spring's #ExceptionHandler seemed the way to group everything in one place (the controller itself or a parent) and get rid of some code (no need to put logic in the try-catch and no need for an utility class)... until I realized that an #ExceptionHandler methods won't have the ModelMap or BindingResult parameters autowired. Currently those objects are used to render the view with a sensible error message and we want to log some information contained in these objects as well.
Question
Why Spring doesn't not support method arguments such ModelMap or BindingResult for the #ExceptionHandler? What is the rationale behind it?
Possible solution
In the Spring source code (3.0.5) the arguments for the method are resolved in the HandlerMethodInvoker.invokeHandlerMethod. A an exception thrown by the request handler is caught there and re-thrown. The #ExceptionHandler and it's parameters are resolved elsewhere. As a workaround I thought to check if the Exception implements an hypothetical "ModelAware" or "BindingResultAware" interface, and in that case set Model and BindingResult attributes before re-throwhing it.
How does it sound?
As stated before you can raise an exception wrapping a binding result object in some method of your controller:
if (bindingResult.hasErrors()) {
logBindingErrors(bindingResult);
//return "users/create";
// Exception handling happens later in this controller
throw new BindingErrorsException("MVC binding errors", userForm, bindingResult);
}
With your exception defined as illustrated here:
public class BindingErrorsException extends RuntimeException {
private static final Logger log = LoggerFactory.getLogger(BindingErrorsException.class);
private static final long serialVersionUID = -7882202987868263849L;
private final UserForm userForm;
private final BindingResult bindingResult;
public BindingErrorsException(
final String message,
final UserForm userForm,
final BindingResult bindingResult
) {
super(message);
this.userForm = userForm;
this.bindingResult = bindingResult;
log.error(getLocalizedMessage());
}
public UserForm getUserForm() {
return userForm;
}
public BindingResult getBindingResult() {
return bindingResult;
}
}
Next you just have to extract the required information from the raised then caught exception. Here assuming you have a suitable exception handler defined on your controller. It might be in a controller advice instead or even elewhere. See the Spring documentation for suitable and appropriate locations.
#ExceptionHandler(BindingErrorsException.class)
public ModelAndView bindingErrors(
final HttpServletResponse resp,
final Exception ex
) {
if(ex instanceof BindingErrorsException) {
final BindingErrorsException bex = (BindingErrorsException) ex;
final ModelAndView mav = new ModelAndView("users/create", bex.getBindingResult().getModel());
mav.addObject("user", bex.getUserForm());
return mav;
} else {
final ModelAndView mav = new ModelAndView("users/create");
return mav;
}
}
I ran into to same problem a while ago. The ModelMap or BindingResult are explicitly not listed as supported argument types in the JavaDocs of #ExceptionHandler, so this must have been intentional.
I reckon the reason behind it being that throwing exceptions in general could leave your ModelMap in an inconsistent state. So depending on your situation you might consider
Explicitly catch the exception to tell Spring MVC that you know what you're doing (you could use the Template pattern to refactor exception handling logic into one single place)
If you're in control of the exception hierarchy you could hand over the BindingResult to the exception and extract it from the exception later for rendering purposes
Not throw an exception in the first place, but use some result code (just like BeanValidation does for example)
HTH
To improve the first answer:
#ExceptionHandler(value = {MethodArgumentNotValidException.class})
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public VndErrors methodArgumentNotValidException(MethodArgumentNotValidException ex, WebRequest request) {
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors();
List<VndError> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size());
VndError error;
for (FieldError fieldError : fieldErrors) {
error = new VndError(ErrorType.FORM_VALIDATION_ERROR.toString(), fieldError.getField() + ", "
+ fieldError.getDefaultMessage());
errors.add(error);
}
for (ObjectError objectError : globalErrors) {
error = new VndError(ErrorType.FORM_VALIDATION_ERROR.toString(), objectError.getDefaultMessage());
errors.add(error);
}
return new VndErrors(errors);
}
There is already MethodArgumentNotValidException has already a BindingResult object, and you can use it, if you don't need to create an specific exception for this purpose.
I had the same problem to "add" FunctinalException to ourthe BindingResult
To resolve it, we use aop, if the controller method throws a runtime exception (or the one you want),
the aop catch it and update the bindingresult or model (if they are args of the method).
The method has to be annoted with a specific annotation containing the error path (configurable for specific exception if necessary).
It is not the best way because developer must not forget to add args that he don't use in its method but Spring does not provide a simple system to do this need.
I've wondered this too.
In order to handle bean validation in a way that allows for a non-global error view to display any ConstraintViolationExceptions that may be thrown, I opted for a solution along the lines of what #Stefan Haberl proposed:
Explicitly catch the exception to tell Spring MVC that you know what you're doing (you could use the Template pattern to refactor exception handling logic into one single place)
I created a simple Action interface:
public interface Action {
String run();
}
And an ActionRunner class which does the work of ensuring ConstraintViolationExceptions are handled nicely (basically the messages from each ConstraintViolationException is simply added to a Set and added to the model):
public class ActionRunner {
public String handleExceptions(Model model, String input, Action action) {
try {
return action.run();
}
catch (RuntimeException rEx) {
Set<String> errors = BeanValidationUtils.getErrorMessagesIfPresent(rEx);
if (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return input;
}
throw rEx;
}
}
}
Java 8 makes this pretty nice to run within the controller action method:
#RequestMapping(value = "/event/save", method = RequestMethod.POST)
public String saveEvent(Event event, Model model, RedirectAttributes redirectAttributes) {
return new ActionRunner().handleExceptions(model, "event/form", () -> {
eventRepository.save(event);
redirectAttributes.addFlashAttribute("messages", "Event saved.");
return "redirect:/events";
});
}
This is to wrap up those action methods for which I'd like to explicitly handle exceptions that could be thrown due to Bean Validation. I still have a global #ExceptionHandler but this deals with only "oh crap" exceptions.
Actually it does, just create an #ExceptionHandler method for MethodArgumentNotValidException.
That class gives you access to a BindingResult object.

Returning an error and message from a Spring controller or service

I'm migrating some servlets over to the Spring framework, using Spring MVC. Currently in each servlet we authenticate the user and if the authentication fails we do this:
if (authfailed)
{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"You are not authorized.");
return;
}
On the front end is a YUI-based application, and when an error status is returned the "failure" callback displays a dialog with the error message given above.
I know in my controller I can get the response object and call sendError, but is that the best way to handle this? sendError also throws an IOException so I'd have to catch that - a bit of annoying code to insert in every method of every controller.
I have the same problem handling exceptions - the servlets have try-catch blocks that call sendError in the catch method. I know I can mark my exception handlers with
#ExceptionHandler
#ResponseStatus(value = HttpStatus.NOT_FOUND)
but doesn't the exception handling class need to be in each controller class?
Finally, if the exception happens in a service called from a controller, does the exception bubble up to the controller or should I handle the exception in the service (thus pushing these exception handling issues into the service layer)?
This seems more difficult than it should be, but as with many things in Spring it's likely I don't understand what's going on. All I want to do is to send an error status and message back in the response!
Thanks,
Paul
It looks like you have the most of the answers in your question itself :)
To reiterate,
Have the controller like this
#RequestMapping("/test")
public String verifyAuth(HttpServletRequest request) throws NotFoundException {
String id = request.getParameter("id");
if (id == null)
throw new NotFoundException("Id not found in the request");
return "success";
}
Declare the exception class in NotFoundException.java,
#ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Id Not Found")
public class NotFoundException extends Exception {
public NotFoundException(String msg) {
super(msg);
}
}
This exception class need not be every controller class. Declare it as public class and import it in every required controller.
This is one way of doing it. If you like the non-spring style, declare HttpServletResponse in every controller arguments and do
#RequestMapping("/test")
public String verifyAuth(HttpServletRequest request, HttpServletResponse response) {
...
try {
response.sendError(..)
catch(..) {}
}
Or you can use views to show error message,
#RequestMapping("/test")
public String verifyAuth(HttpServletRequest request, Map<String, Object> map){
String id = request.getParameter("id");
if (id == null) {
map.put("status", HttpStatus.NOTFOUND);
map.put("reason", "Id Not Found");
return "error"
}
return "success";
}
Make sure your viewResolver is configured correctly and in the error.jsp to get the error string, you could say.
<body>
${status} ${reason}
</body>
Define error.jsp with nice css for all kind of errors you would expect.
These are not the only ways. With spring you have freedom to do anything. I have seen few ppl rendering json object for error message.
To answer your another question of if the error happens in the service called by the controller is depend on your scenario. For example you are trying to read the user store, if the user store not available error happens, I would handle there itself to read from another replica user store if one available and If I found user does not exist I would leave the exception to the controller to throw.

Resources