I have a REST API developed in Spring and now I'm learning how to use GraphQL in my Spring project so I'm "translating" my REST endpoints to GraphQL queries.
Just to give some background on what I'm trying to do, I will show how one of my REST controllers look like (code in Kotlin):
Rest Controller
#RestController
#RequestMapping("countries")
class CountryController(private val countryService: CountryService) {
#GetMapping("{code}")
fun findByCode(#PathVariable #Uppercase code: String): Country {
return countryService.findByCode(code)
}
}
It's basically an endpoint that fetches a country based on a code. For example /countries/bra will fetch information about the country Brazil.
In the code above there's also a custom annotation called #Uppercase. This annotation extends ConditionalGenericConverter and the only thing it does is to take the code parameter and convert it to uppercase. This is the code for this annotation:
#Uppercase implementation
#Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Uppercase
#Component
class UppercaseConverter : ConditionalGenericConverter {
override fun matches(sourceType: TypeDescriptor, targetType: TypeDescriptor): Boolean {
return targetType.getAnnotation(Uppercase::class.java) != null
}
override fun getConvertibleTypes(): MutableSet<ConvertiblePair>? {
return mutableSetOf(ConvertiblePair(String::class.java, String::class.java))
}
override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor): String {
return (source as String).uppercase()
}
}
Everything works as expected in the code above and the code parameter is always converted to uppercase if I use the #Uppercase annotation in my REST controller.
Now I created a similar controller for Spring GraphQL that looks like this:
GraphQL Controller
#Controller
class CountryResolver(private val countryService: CountryService) {
#QueryMapping(name = "country")
fun findByCode(#Argument #Uppercase code: String): Country {
return countryService.findByCode(code)
}
}
And my GraphQL is executed properly with the code above, except that the parameter code is not converted to uppercase when I use my custom annotation #Uppercase. It seems that my custom annotation only works with #PathVariable in the REST controller, but it doesn't with #Argument in the GraphQL controller.
Any idea why on how to make it work with GraphQL as well? Thanks in advance!
Related
I have a REST interface implemented in kotlin and springboot with the following controller method:
fun getAllUsers() : ResponseEntity<UserCollectionDto> {
return getDelegate().getAllUsers();
}
The UserCollectionDto is a class that wraps a collection with some additional properties to support pagination.
data class UserCollectionDto(
val offset: Int,
val limit: Int,
val total: Int,
val content: List<UserDto>
) {
}
I would like to use spring-security using method level security such that the content field in the response is filtered based on the role of the subject. The role based filtering needs to be done using a function like this:
fun filterContentByRole(role: String, userCollectionDto: UserCollectionDto) : UserCollectionDto {
// Filter userCollectionDto.content filtering by role
return userCollectionDto
}
Is this use case supported by spring method level security? If so, how can it be specified?
You can use Spring Security's #PostFilter annotation on a method that returns a collection, array, map and stream.
This iterates through the returned collection or map and removes any elements for which the supplied expression is false.
For your use case, you would add the #PostFilter annotation to the method that populates the UserCollectionDto content.
Suppose that method is called getUserDto.
#PostFilter("#myBean.filterContentByRole('admin', filterObject)")
fun getUserDto(): List<UserDto> {
// return UserDtos
}
In the above expression we are referencing a bean called myBean that has the method filterContentByRole.
#Bean
fun myBean(): MyBean {
return MyBean()
}
class MyBean {
fun filterContentByRole(role: String, userDto: UserDto) : Boolean {
// Filter UserDto filtering by role
}
}
Note that using a bean is optional, if you have a simple filter condition you can simply inline it, for example #PostFilter("filterObject.someField == 'admin'")
Don't forget to enable method security #EnableGlobalMethodSecurity(prePostEnabled = true).
Be aware that the method that #PostFilter is applied to must return a mutable collection.
In the above example, this means that getUserDto must return a MutableList.
I want SpringBoot to be able to provide default values for fields that the user must enter. For example, I have something like this:
*Controller class*
#PostMapping("/test")
public ResponseEntity<> myMethod(#RequestBody #Valid MyContract contract) {}
*MyContract class*
#Valid
DataObject dataObject;
*DataObject class*
#Component
public class DataObject {
private #Value("${field1.default}") String field1Default;
private String field1
public String getField1() {
return (field1 == null ? field1Default : field1);
}
}
The DataObject class needs to be created on a per request basis. There are also other places in the code where it needs to be created on demand. So I imagine it needs to be a Prototype object. But I can't figure out how to get Spring to created it properly when it creates it for the request.
Update
I have read more about #RequstBody, e.g., https://www.javadevjournal.com/spring/spring-request-response-body/ and Should spring #RequestBody class be singleton or prototype?, which explains that the object is not a Component, but a simple POJO that gets the values from the Json request. So it seems that there is no way to inject #Values from the Spring application.properties file. Is there any other way around this? Or another suggested implementation?
I'm learning Kotlin, part of my project is to integrate JSON as an object and use the POST method to change or add information.
I'm not able to do this, I need help.
package com.example.blog
import org.springframework.web.bind.annotation.*
data class Relatorio(
val titulo: String,
val autor: String,
val serie: String
)
#RestController
#RequestMapping("/Bradesco")
class BradescoController {
#GetMapping()
public fun relatorio(): Relatorio {
val result = Relatorio(
"Investimentos",
"Luis Felipe",
"Bradesco Analises"
)
return result
}
#PostMapping
#RequestMapping( #RequestBody "/empiricus")
public fun relatorio2() {
"titulo" = "Contra as altas taxas"
return "Atualizado";
}
}
It looks like some annotations are out of place in your relatorio2 method. You want to register a REST-endpoint for the POST-method and the path /empiricus.
This can happen one of two ways:
Annotate the method with #RequestMapping(value = "/empiricus", method = RequestMethod.POST)
Annotate the method with `#PostMapping("/empiricus") (you can omit the method-parameter from the example above, since this a shortcut for exactly that.
The #RequestBody annotation needs to be placed in the parameter of the relatorio2 method since it tells Spring to map the POST request-body to an object.
Therefore the method should look something like this:
#PostMapping("/empiricus")
public fun relatorio2(#RequestBody relatorio: Relatorio) {
"titulo" = "Contra as altas taxas"
return "Atualizado";
}
Since you added a path on class level, the complete path to call the method is /Bradesco/empiricus. When the object is available in the relatorio2 method, you can use it in your business logic.
Using Webflux and Reactive Spring Security, how do you do post processing via annotations to control access to methods?
Trying a very basic sample, I'm not able to get the value from the PostAuthorize annotation. For example
#GetMapping
#PostAuthorize("#email == authentication.principal.email")
public Flux<Project> sampleTest(final String email) {
log.info("email: {}", email);
return Flux.empty();
}
The email will always be null. I have the basic wiring working to the fact if I set something like #PreAuthorize("hasRole('ADMIN')") I'll get back a 403.
I can extract the Authentication out with a helper like:
public Mono<Authentication> getAuthentication() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.flatMap(Mono::just);
}
I may not be understanding your question correctly, but the PostAuthorize uses the return object - the body of the method doesn't have access to anything in the SPEL expression.
Something like this might work -
#GetMapping
#PostAuthorize("returnObject == someCondition")
public Flux<Project> sampleTest(final String email) {
// get some data and return it
}
But maybe you want to filter the items in the Flux?
You might look at the #PostFilter annotation -
// assuming there's an email property on your Project object.
#GetMapping
#PostFilter("filterObject.getEmail() == authentication.principal.email")
public Flux<Project> sampleTest() {
// get some data and return it
}
I am currently implementing SiteMinder for the site, which looks for a key called SM_USER in the request header. I retrieve it using the function below:
public string ReadUser()
{
return HttpContext.Current.Request.Headers["SM_USER"];
}
I wish to test if functionality releated to this function work; I have already tried unit testing using a mock class so I am looking to create the key SM_USER in the request header. How can I do that?
I am implementing the application with MVC3.
As long as you are using HttpContext.Current you will not be able to test it as Unit Test will not have HttpContext.Current.
Try to use an Interface with method returning string, say ReadUser(). Implement this interface in a class in your application. Use the interface variable whichever class you are using this method in. In that class' default constructor set that interface variable value to 'new' implementer class. Add an overload of the constructor which will take a parameter of type interface and set that parameter to interface variable.
Now in your UnitTest project implement same interface in another class. In this implementation you can now pass whatever mock value you want test.
public interface IReadUserInfo
{ string ReadUser(); }
public class ReadUserInfo: IReadUserInfo
{
public string ReadUser()
{
return HttpContext.Current.Request.Headers["SM_USER"];
}
}
public class UserClass
{
IReadUserInfo userinfo;
public UserClass()
{
userinfo = new ReadUserInfo();
}
public USerClass(IReadUserInfo newuserinfo)
{
userinfo = newuserinfo;
}
}
public class TestReadUserInfo : IReadUSerInfo
{
public string ReadUser()
{ return "testvalue"; }
}
If ReadUser is the only value you are using from Request header, then this approach will solve the problem. However, if you using more values from Request object, you might want to mock entire request object in similar way.