How to get validate a #PathVariable before authorization in Spring MVC - spring

I have a REST handler with an endpoint for the GET verb. Where from an identifier (ObjectID of MongoDB) I get the information of that entity.
To validate that the ObjectID is valid and avoid errors when using Spring Data Mongo. I have developed a simple validator following the guidelines of the JPA bean validation standard.
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Constraint(validatedBy = ValidObjectIdValidator.class)
#NotNull
#Documented
public #interface ValidObjectId {
String message() default "{constraints.valid.objectid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class ValidObjectIdValidator implements ConstraintValidator<ValidObjectId, String> {
#Override
public void initialize(ValidObjectId constraintAnnotation) {}
#Override
public boolean isValid(String id, ConstraintValidatorContext context) {
return ObjectId.isValid(id);
}
}
Then I apply the variable-level validation of the controller using the following configuration:
#Api
#RestController("RestUserController")
#Validated
#RequestMapping("/api/v1/children/")
public class ChildrenController implements ISonHAL, ICommentHAL, ISocialMediaHAL
Using #Validated annotation at controller level.
#GetMapping(path = "/{id}")
#ApiOperation(value = "GET_SON_BY_ID", nickname = "GET_SON_BY_ID", notes = "Get Son By Id",
response = SonDTO.class)
#PreAuthorize("#authorizationService.hasParentRole() && #authorizationService.isYourSon(#id)")
public ResponseEntity<APIResponse<SonDTO>> getSonById(
#Valid #ValidObjectId(message = "{son.id.notvalid}")
#ApiParam(value = "id", required = true) #PathVariable String id) throws Throwable {
logger.debug("Get User with id: " + id);
return Optional.ofNullable(sonService.getSonById(id))
.map(sonResource -> addLinksToSon(sonResource))
.map(sonResource -> ApiHelper.<SonDTO>createAndSendResponse(ChildrenResponseCode.SINGLE_USER, HttpStatus.OK, sonResource))
.orElseThrow(() -> { throw new SonNotFoundException(); });
}
Using the #Valid annotation on the #PathVariable.
The problem is that I must verify that the user is currently authenticated is the parent of the child for whom he wants to see his information. This is verified by the execution of:
#PreAuthorize("#authorizationService.hasParentRole() && #authorizationService.isYourSon(#id)")
And here the error occurs. Because I must convert the received id to an ObjectID mediate new ObjectId (id). And this may not be valid.
Is there any way to configure validation to occur before authorization?.
This is my configuration to enable security at the method level:
#EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
Thanks in advance.

Related

Spring custom validator with dependencies on other fields

We are using spring custom validator for our request object used in our controller endpoint. We implemented it the same way as how its done in the link below:
https://www.baeldung.com/spring-mvc-custom-validator
The problem we are facing is, it can't work if the particular field has dependencies on other input fields as well. For example, we have the code below as the request object for our controller endpoint:
public class FundTransferRequest {
private String accountTo;
private String accountFrom;
private String amount;
#CustomValidator
private String reason;
private Metadata metadata;
}
public class Metadata {
private String channel; //e.g. mobile, web, etc.
}
Basically #CustomValidator is our custom validator class and the logic we want is, if the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required. Else, it will be required.
Is there a way to do this? I've done additional research and can't see any that handles this type of scenario.
Obviously if you need access to multiple fields in your custom validator, you have to use a class-level annotation.
The same very article you mentioned has an example of that: https://www.baeldung.com/spring-mvc-custom-validator#custom-class-level-validation
In your case it might look something like this:
#Constraint(validatedBy = CustomValidator.class)
#Target({ ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomValidation {
String message() default "Reason required";
String checkedField() default "metadata.channel";
String checkedValue() default "WEB";
String requiredField() default "reason";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.example.demo;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/*
If the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required.
Else, it will be required.
*/
#Component
public class CustomValidator implements ConstraintValidator<CustomValidation, Object> {
private String checkedField;
private String checkedValue;
private String requiredField;
#Override
public void initialize(CustomValidation constraintAnnotation) {
this.checkedField = constraintAnnotation.checkedField();
this.checkedValue = constraintAnnotation.checkedValue();
this.requiredField = constraintAnnotation.requiredField();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
Object checkedFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(checkedField);
Object requiredFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(requiredField);
return checkedFieldValue != null && checkedFieldValue.equals(checkedValue) || requiredFieldValue != null;
}
}
And the usage will be:
#CustomValidation
public class FundTransferRequest {
...
or with parameters specified:
#CustomValidation(checkedField = "metadata.channel",
checkedValue = "WEB",
requiredField = "reason",
message = "Reason required")
public class FundTransferRequest {
...

Springboot #Valid annotation with String object

So, I have the following controller method:
#RequestMapping(path = "/{application}/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public MyObject getUsers(#RequestParam("itemId") String itemId, #PathVariable("application") String application) {
return userService.get(itemId, application);
}
I would like to check if the request parameter itemId exists in the related application (in the path).
My first idea was to create a validator :
#RequestMapping(path = "/{application}/users", method = RequestMethod.GET, produces =
MediaType.APPLICATION_JSON_VALUE)
#CheckItemId
public MyObject getUsers(#RequestParam("itemId") String itemId, #PathVariable("application") String application) {
return userService.get(itemId, application);
}
CheckItemId.java :
#Target({METHOD})
#Retention(RUNTIME)
#Constraint(validatedBy = CheckItemIdValidator.class)
#Documented
public #interface CheckItemId {
String message() default "error";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
CheckItemIdValidator.java :
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class CheckItemIdValidator implements ConstraintValidator<CheckItemId, Object[]>{
#Override
public boolean isValid(Object[] arg0, ConstraintValidatorContext arg1) {
String itemId= (String) arg0[0];
String application = (String) arg0[1];
// Logic business ...
return true;
}
}
This implementation works well, I managed to get the values itemId and application in the validator. I can now do my verification.
I was wondering if there is a better way to do something like that? Since I handle an array of Object, I need to cast it to String and If I change the parameters order, I will not get the same values since I need to use arg0[0] and arg0[1].
Thank you !
You can use spring validation library. Add #Valid on controller level. Then add #NotBlank on method level as below.
getUsers(#RequestParam("itemId") #NotBlank String itemId)

Springboot show error message for invalid date (YearMonth) formats: eg 2020-15

I have a project with Spring Boot and I want to show an error response if the given date format is incorrect.
The correct format is yyyy-MM (java.time.YearMonth) but I want to want to show a message if someone sends 2020-13, 2020-111 or 2020-1.
When I've added a custom validator the debugger goes in there with a valid request but not with an incorrect request. I also tried to use the message.properties with the typeMismatch.project.startdate=Please enter a valid date. but I also don't see that message in my response body.
It seems like the application does not understand my incorrect request and then always throws a BAD REQUEST with empty body, which is not strange because it is not a valid date.
Can someone explain me how I can show an errormessage in the response for these incorrect values?
Or is there no other way then use a String and convert that to the YearMonth object so I can show catch and show an error message?
Request object:
#Getter
#Setter
public class Project {
#NotNull(message = "mandatory")
#DateTimeFormat(pattern = "yyyy-MM")
private YearMonth startdate;
}
Controller:
#RestController
#RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class ProjectController {
#PostMapping(value = "/project", consumes = MediaType.APPLICATION_JSON_VALUE)
public Project newProject(#Valid #RequestBody Project newProject) {
return projectService.newProject(newProject);
}
}
ExceptionHandler:
#RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#SneakyThrows
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
headers.add("Content-Type", "application/json");
ObjectMapper mapper = new ObjectMapper();
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String name;
if (error instanceof FieldError)
name = ((FieldError) error).getField();
else
name = error.getObjectName();
String errorMessage = error.getDefaultMessage();
errors.put(name, errorMessage);
});
return new ResponseEntity<>(mapper.writeValueAsString(errors), headers, status);
}
}
Okay, I made a solution which is workable for me.
I've added the solution below for people who find this thread in the future and has the same problem I had.
Create a custom validator with a simple regex pattern:
#Target({ FIELD })
#Retention(RUNTIME)
#Constraint(validatedBy = YearMonthValidator.class)
#Documented
public #interface YearMonthPattern {
String message() default "{YearMonth.invalid}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
public class YearMonthValidator implements ConstraintValidator<YearMonthPattern, String> {
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern pattern = Pattern.compile("^([0-9]{4})-([0-9]{2})$");
Matcher matcher = pattern.matcher(value);
try {
return matcher.matches();
} catch (Exception e) {
return false;
}
}
}
Update the request object:
#Getter
#Setter
public class Project {
#NotNull(message = "mandatory")
#YearMonthPattern
private String startdate;
public YearMonth toYearMonth(){
return YearMonth.parse(startdate);
}
}
The DateTimeFormat annotation is replaced with our new custom validator and instead of a YearMonth, make it a String. Now the validator annotation can be executed because the mapping to the YearMonth won't fail anymore.
We also add a new method to convert the String startdate to a YearMonth after Spring has validated the request body, so we can use it in the service as a YearMonth instead of having to translate it each time.
Now when we send a requestbody with:
{
"startdate": "2020-1"
}
we get a nice 400 bad request with the following response:
{
"endDate": "{YearMonth.invalid}"
}

Spring Data Rest Validation Confusion

Looking for some help with Spring data rest validation regarding proper handling of validation errors:
I'm so confused with the docs regarding spring-data-rest validation here: http://docs.spring.io/spring-data/rest/docs/current/reference/html/#validation
I am trying to properly deal with validation for a POST call that tries to save a new Company entity
I got this entity:
#Entity
public class Company implements Serializable {
#Id
#GeneratedValue
private Long id;
#NotNull
private String name;
private String address;
private String city;
private String country;
private String email;
private String phoneNumber;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "company")
private Set<Owner> owners = new HashSet<>();
public Company() {
super();
}
...
and this RestResource dao
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RestResource;
import com.domain.Company;
#RestResource
public interface CompanyDao extends PagingAndSortingRepository<Company, Long> {
}
POST Request to api/Companies:
{
"address" : "One Microsoft Way",
"city" : "Redmond",
"country" : "USA",
"email" : "info#microsoft.com",
"phoneNumber" : "(425) 703-6214"
}
When I issue a POST with a null name , I get the following rest response with httpcode 500
{"timestamp":1455131008472,"status":500,"error":"Internal Server Error","exception":"javax.validation.ConstraintViolationException","message":"Validation failed for classes [com.domain.Company] during persist time for groups [javax.validation.groups.Default, ]\nList of constraint violations:[\n\tConstraintViolationImpl{interpolatedMessage='may not be null', propertyPath=name, rootBeanClass=class com.domain.Company, messageTemplate='{javax.validation.constraints.NotNull.message}'}\n]","path":"/api/companies/"}
I tried creating the following bean, but it never seems to do anything:
#Component(value="beforeCreateCompanyValidator")
public class BeforeCreateCompanyValidator implements Validator{
#Override
public boolean supports(Class<?> clazz) {
return Company.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object arg0, Errors arg1) {
System.out.println("xxxxxxxx");
}
}
and even if it did work, how would it help me in developing a better error response with a proper http code and understandable json response ?
so confused
using 1.3.2.RELEASE
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
#Mathias
it seems the following is enough for jsr 303 annotations to be checked and for it to auto return a http code of 400 with nice messages (I dont even need BeforeCreateCompanyValidator or BeforeSaveCompanyValidator classes):
#Configuration
public class RestValidationConfiguration extends RepositoryRestConfigurerAdapter{
#Bean
#Primary
/**
* Create a validator to use in bean validation - primary to be able to autowire without qualifier
*/
Validator validator() {
return new LocalValidatorFactoryBean();
}
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
Validator validator = validator();
//bean validation always before save and create
validatingListener.addValidator("beforeCreate", validator);
validatingListener.addValidator("beforeSave", validator);
}
}
400 response:
{
"errors": [{
"entity": "Company",
"message": "may not be null",
"invalidValue": "null",
"property": "name"
}, {
"entity": "Company",
"message": "may not be null",
"invalidValue": "null",
"property": "address"
}]
}
I think your problem is that the bean validation is happening too late - it is done on the JPA level before persist. I found that - unlike spring mvc - spring-data-rest is not doing bean validation when a controller method is invoked. You will need some extra configuration for this.
You want spring-data-rest to validate your bean - this will give you nice error messages responses and a proper http return code.
I configured my validation in spring-data-rest like this:
#Configuration
public class MySpringDataRestValidationConfiguration extends RepositoryRestConfigurerAdapter {
#Bean
#Primary
/**
* Create a validator to use in bean validation - primary to be able to autowire without qualifier
*/
Validator validator() {
return new LocalValidatorFactoryBean();
}
#Bean
//the bean name starting with beforeCreate will result into registering the validator before insert
public BeforeCreateCompanyValidator beforeCreateCompanyValidator() {
return new BeforeCreateCompanyValidator();
}
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
Validator validator = validator();
//bean validation always before save and create
validatingListener.addValidator("beforeCreate", validator);
validatingListener.addValidator("beforeSave", validator);
}
}
When bean validation and/or my custom validator find errors I receive a 400 - bad request with a payload like this:
Status = 400
Error message = null
Headers = {Content-Type=[application/hal+json]}
Content type = application/hal+json
Body = {
"errors" : [ {
"entity" : "siteWithAdminUser",
"message" : "may not be null",
"invalidValue" : "null",
"property" : "adminUser"
} ]
}
The answers by #Mathias and #1977 is enough for regular Spring Data REST calls. However in cases when you need to write custom #RepositoryRestControllers using #RequestBody and #Valid JSR-303 annotations didn't work for me.
So, as an addition to the answer, in case of custom #RepositoryRestControllers with #RequestBody and #Valid annotation I've added the following #ControllerAdvice:
/**
* Workaround class for making JSR-303 annotation validation work for controller method parameters.
* Check the issue DATAREST-593
*/
#ControllerAdvice
public class RequestBodyValidationProcessor extends RequestBodyAdviceAdapter {
private final Validator validator;
public RequestBodyValidationProcessor(#Autowired #Qualifier("mvcValidator") final Validator validator) {
this.validator = validator;
}
#Override
public boolean supports(final MethodParameter methodParameter, final Type targetType, final Class<? extends
HttpMessageConverter<?>> converterType) {
final Annotation[] parameterAnnotations = methodParameter.getParameterAnnotations();
for (final Annotation annotation : parameterAnnotations) {
if (annotation.annotationType().equals(Valid.class)) {
return true;
}
}
return false;
}
#Override
public Object afterBodyRead(final Object body, final HttpInputMessage inputMessage, final MethodParameter
parameter, final Type targetType, final Class<? extends HttpMessageConverter<?>> converterType) {
final Object obj = super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
final BindingResult bindingResult = new BeanPropertyBindingResult(obj, obj.getClass().getCanonicalName());
validator.validate(obj, bindingResult);
if (bindingResult.hasErrors()) {
throw new RuntimeBindException(bindingResult);
}
return obj;
}
}

Hibernate validation with custom messages in property file

Hi am using hibernate validator in jersey rest service.
Here how can we pass value to the property file message as follows
empty.check= Please enter {0}
here in {0} i need to pass the value from annotation
#EmptyCheck(message = "{empty.check}") private String userName
here in the {0} i need to pass "user name", similarly i need to reuse message
please help me out to solve this.
You can do this by altering your annotation to provide a field description and then exposing this in the validator.
First, add a description field to your annotation:
#Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EmptyCheckValidator.class)
#Documented
public #interface EmptyCheck {
String description() default "";
String message() default "{empty.check}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Next, change your message so that it uses a named parameter; this is more readable.
empty.check= Please enter ${description}
Since you're using hibernate-validator, you can get the hibernate validator context within your validation class and add a context variable.
public class EmptyCheckValidator
implements ConstraintValidator<EmptyCheck, String> {
String description;
public final void initialize(final EmptyCheck annotation) {
this.description = annotation.description();
}
public final boolean isValid(final String value,
final ConstraintValidatorContext context) {
if(null != value && !value.isEmpty) {
return true;
}
HibernateConstraintValidatorContext ctx =
context.unwrap(HibernateConstraintValidatorContext.class);
ctx.addExpressionVariable("description", this.description);
return false;
}
}
Finally, add the description to the field:
#EmptyCheck(description = "a user name") private String userName
This should produce the following error when userName is null or empty:
Please enter a user name

Resources