WebTestClient used multiple times returns empty body sometimes - spring-webflux

not sure, why this could be an issue, but I can't stabilize my unit-tests.
Here some snippets from my testclass:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.main.web-application-type=reactive" })
#RunWith(SpringRunner.class)
#TestPropertySource(locations = "classpath:application-test.properties")
public class SolrControllerV1Test {
#Inject
ApplicationContext context;
#LocalServerPort
int port;
private WebTestClient client;
#TestConfiguration
static class TestConfig {
#Bean
public TestingAuthenticationProvider testAuthentiationManager() {
return new TestingAuthenticationProvider();
}
#Bean
public SecurityWebFilterChain securityConfig(ServerHttpSecurity http, ReactiveAuthenticationManager authenticationManager) {
AuthenticationWebFilter webFilter = new AuthenticationWebFilter(authenticationManager);
return http.addFilterAt(webFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.build();
}
}
#Before
public void setUp() {
this.client = WebTestClient.bindToApplicationContext(context).configureClient().responseTimeout(Duration.ofDays(1L)).baseUrl("http://localhost:" + port).build();
}
private void defaultCheck(ResponseSpec spec) {
spec.expectStatus().isOk().expectBody().jsonPath("$.response.numFound").hasJsonPath();
}
#Test
#WithMockUser(roles = { "ADMIN" })
public void simpleUsrSelect() throws Exception {
ResponseSpec spec = this.client.get().uri("/" + serviceVersion + "/usr/select?q=*:*&fq=*:*&fl=USRTYP,USRKEY,USRCID&rows=1&start=10&sort=last_update desc").exchange();
defaultCheck(spec);
}
#Test
#WithMockUser(roles = { "ADMIN" })
public void simpleCvdSelect() throws Exception {
ResponseSpec spec = this.client.get().uri("/" + serviceVersion + "/cvd/select?q=*:*&rows=10000").exchange();
defaultCheck(spec);
}
.
.
.
}
There are some more unit-tests there, some of which are long running (>1sec). If I have enough unit-tests in the class (~5-8), of which 1 or 2 are taking a bit longer, the unit-tests start to break. This looks like a thread safety issue, but I don't know, what I'm doing wrong. Any ideas?
EDIT
Here the Server Part that made trouble:
#PreAuthorize("hasAnyRole('ADMIN','TENANT')")
public Mono<ServerResponse> select(ServerRequest request) {
return request.principal().flatMap((principal) -> {
return client.get().uri(f -> {
URI u = f.path(request.pathVariable("collection")).path("/select/").queryParams(
queryModifier.modify(principal, request.pathVariable("collection"), request.queryParams())
.onErrorMap(NoSuchFieldException.class, t -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Collection not found"))
.block()).build();
return u;
})
.exchange()
.flatMap((ClientResponse mapper) -> {
return ServerResponse.status(mapper.statusCode())
.headers(c -> mapper.headers().asHttpHeaders().forEach((name, value) -> c.put(name, value)))
.body(mapper.bodyToFlux(DataBuffer.class), DataBuffer.class);
})
.doOnError(t -> handleAuthxErrors(t, principal, request.uri()));
});
}
If I add a publishOn(Schedulers.elastic) right after the .exchange() part, it seems to be working. Since this is trial&error, and I don't really understand why the publishOn fixes the problem, does anybody else know? I'm not even sure, whether using springs reactive Webclient is blocking in this case, or not...
Thanks, Henning

Related

Spring Cloud Sleuth: Initialise baggage item

I already have this Java Configuration:
#Configuration
public class FAPIAutoConfiguration {
private static final String INTERACTION_ID = "x-fapi-interaction-id";
private final BaggageField fapiBaggageField = BaggageField.create(INTERACTION_ID);
#Bean
BaggagePropagationCustomizer baggagePropagationCustomizer() {
return builder -> builder.add(SingleBaggageField.
remote(fapiBaggageField));
}
#Bean
CorrelationScopeCustomizer correlationScopeCustomizer() {
return builder -> builder.add(SingleCorrelationField.create(fapiBaggageField));
}
}
And the propagation in a Webflux application works, but I would like to know what is the best way to initialize the baggage if it is not present in the request headers. I mean, if the header is missing, generate a value and propagate this one.
I ended up adding a TracingCustomizer to the above configuration to fill the value when is missing in that context.
#Bean
TracingCustomizer tracingCustomizer(UniqueIdGenerator generator) {
return builder -> builder.addSpanHandler(new SpanHandler() {
#Override
public boolean begin(TraceContext context, MutableSpan span, TraceContext parent) {
var value = fapiBaggageField.getValue(context);
if (value == null) {
fapiBaggageField.updateValue(context, generator.next());
}
return super.begin(context, span, parent);
}
});
}
I do not know if this is the best option yet

Spring R2dbc: Is there are way to get constant stream from postgresql database and process them?

I want to fetch records for newly created records in a table in postgresql as a live/continuous stream. Is it possible to use using spring r2dbc? If so what options do I have?
Thanks
You need to use pg_notify and start to listing on it. Any change that you want to see should be wrapped in simple trigger that will send message to pg_notify.
I have an example of this on my github, but long story short:
prepare function and trigger:
CREATE OR REPLACE FUNCTION notify_member_saved()
RETURNS TRIGGER
AS $$
BEGIN
PERFORM pg_notify('MEMBER_SAVED', row_to_json(NEW)::text);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER member_saved_trigger
AFTER INSERT OR UPDATE
ON members
FOR EACH ROW
EXECUTE PROCEDURE notify_member_saved();
In java code prepare listener
#Service
#RequiredArgsConstructor
#Slf4j
class NotificationService {
private final ConnectionFactory connectionFactory;
private final Set<NotificationTopic> watchedTopics = Collections.synchronizedSet(new HashSet<>());
#Qualifier("postgres-event-mapper")
private final ObjectMapper objectMapper;
private PostgresqlConnection connection;
#PreDestroy
private void preDestroy() {
this.getConnection().close().subscribe();
}
private PostgresqlConnection getConnection() {
if(connection == null) {
synchronized(NotificationService.class) {
if(connection == null) {
try {
connection = Mono.from(connectionFactory.create())
.cast(Wrapped.class)
.map(Wrapped::unwrap)
.cast(PostgresqlConnection.class)
.toFuture().get();
} catch(InterruptedException e) {
throw new RuntimeException(e);
} catch(ExecutionException e) {
throw new RuntimeException(e);
}
}
}
}
return this.connection;
}
public <T> Flux<T> listen(final NotificationTopic topic, final Class<T> clazz) {
if(!watchedTopics.contains(topic)) {
executeListenStatement(topic);
}
return getConnection().getNotifications()
.log("notifications")
.filter(notification -> topic.name().equals(notification.getName()) && notification.getParameter() != null)
.handle((notification, sink) -> {
final String json = notification.getParameter();
if(!StringUtils.isBlank(json)) {
try {
sink.next(objectMapper.readValue(json, clazz));
} catch(JsonProcessingException e) {
log.error(String.format("Problem deserializing an instance of [%s] " +
"with the following json: %s ", clazz.getSimpleName(), json), e);
Mono.error(new DeserializationException(topic, e));
}
}
});
}
private void executeListenStatement(final NotificationTopic topic) {
getConnection().createStatement(String.format("LISTEN \"%s\"", topic)).execute()
.doOnComplete(() -> watchedTopics.add(topic))
.subscribe();
}
public void unlisten(final NotificationTopic topic) {
if(watchedTopics.contains(topic)) {
executeUnlistenStatement(topic);
}
}
private void executeUnlistenStatement(final NotificationTopic topic) {
getConnection().createStatement(String.format("UNLISTEN \"%s\"", topic)).execute()
.doOnComplete(() -> watchedTopics.remove(topic))
.subscribe();
}
}
start listiong from controller
#GetMapping("/events")
public Flux<ServerSentEvent<Object>> listenToEvents() {
return Flux.merge(listenToDeletedItems(), listenToSavedItems())
.map(o -> ServerSentEvent.builder()
.retry(Duration.ofSeconds(4L))
.event(o.getClass().getName())
.data(o).build()
);
}
#GetMapping("/unevents")
public Mono<ResponseEntity<Void>> unlistenToEvents() {
unlistenToDeletedItems();
unlistenToSavedItems();
return Mono.just(
ResponseEntity
.status(HttpStatus.I_AM_A_TEAPOT)
.body(null)
);
}
private Flux<Member> listenToSavedItems() {
return this.notificationService.listen(MEMBER_SAVED, Member.class);
}
private void unlistenToSavedItems() {
this.notificationService.unlisten(MEMBER_SAVED);
}
but remember that if something broke then you lost pg_notify events for some time so it is for non-mission-citical solutions.

SupervisorStrategy strategy not respecting maxNrOfRetries

I'm learning Akka.net and am stuck trying to get an actor to restart on failure.
With the following, I would have expected the "child has error" string to display 5 times (maxNrOfRetries), however I only see it once.
Can someone please point out what I'm missing? Is my code wrong, my understanding? Both?
Here is the LINQPad script http://share.linqpad.net/t8x5wl.linq
Thank you
async Task Main()
{
var system = ActorSystem.Create("MySystem");
var parent = system.ActorOf(Props.Create<ParentActor>(), "parent");
var child = await parent.Ask<IActorRef>(new GetChild());
child.Tell(new HelloChild());
child.Tell(new CauseChildToThrow());
child.Tell(new HelloChild());
child.Tell(new StopChild());
await parent.Ask<object>(new EndProcess());
"fin".Dump();
}
public class ParentActor : ReceiveActor
{
public ParentActor()
{
var child = Context.ActorOf(Props.Create<ChildActor>(), "child");
Receive<GetChild>(msg => Sender.Tell(child));
Receive<EndProcess>(msg => Sender.Tell(new {}));
}
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
maxNrOfRetries: 5,
withinTimeRange: Timeout.InfiniteTimeSpan,
localOnlyDecider: x =>
{
//return Directive.Resume; // skips the bad messsage?
return Directive.Restart; // retries with the bad message?
});
}
}
public class ChildActor : ReceiveActor
{
protected override void PreStart()
{
"child prestart".Dump();
}
protected override void PreRestart(Exception reason, object message)
{
"child pre restart".Dump(reason.Message + " " + message.ToString());
}
public ChildActor()
{
"child constructor".Dump();
Receive<HelloChild>(msg => "Child says hi".Dump());
Receive<StopChild>(msg =>
{
"child stopping gracefully".Dump();
Context.Stop(Self);
});
Receive<CauseChildToThrow>(_ =>
{
"child has error".Dump();
throw new MyException();
});
}
}
public class GetChild { }
public class HelloChild { }
public class StopChild { }
public class CauseChildToThrow { }
public class EndProcess { }
public class MyException : Exception{ }
I found this issue. The dying actor needed to resend the failing message back to itself.
protected override void PreRestart(Exception reason, object message)
{
"child pre restart".Dump(reason.Message + " " + message.ToString());
Self.Tell(message);
}
Also, i had to remove the call to
child.Tell(new StopChild());
Working LINQPad script here http://share.linqpad.net/hv6w4g.linq

Issue with reactive nested flatMap calls

I need to perform an operation on two Monos. The difficulty is that one depend on the result of the other.
Let me explain:
I have a Mono<User> (I get that from a ServerRequest; User is a POJO).
I need to be able to extract the user email from the above Mono and pass it to the UserRepository in order to check whether the email already exists in DB.
If the user already exists I will throw a 400 error; otherwise, I will save the user contained in the ServerRequest.
Here is what I have tried in my handler:
public Mono<ServerResponse> saveUser(ServerRequest serverRequest) {
return serverRequest.bodyToMono(User.class)
.flatMap(user -> userRepository
.findByEmail(user.getEmail())
.flatMap(foundUser -> {
if (foundUser != null) {
System.out.println("found:" + foundUser);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email already exists");
} else {
System.out.println("creating" + user);
return status(CREATED).contentType(APPLICATION_JSON).body(userRepository.save(user), User.class);
}
}));
}
User:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class User {
#Id
private Integer id;
#Size(min = 2)
private String firstName;
#Size(min = 2)
private String lastName;
#Email
private String email;
}
UserRepository:
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
#Query("select id, first_name, last_name, email from user u where u.email = :email")
Mono<User> findByEmail(String email);
}
However, It seems there's an issue with my subscription: none of my System.out.println are called when the endpoint is called. Can someone please help?
edit 1: Here is the router calling the above handler method:
#Configuration
public class UserRouter {
#Bean
public RouterFunction<ServerResponse> route(UserHandler userHandler) {
return RouterFunctions.route()
.GET("/api/user", accept(APPLICATION_JSON), userHandler::getUsers)
.POST("/api/sign-up", accept(APPLICATION_JSON), userHandler::saveUser)
.build();
}
}
The issue here is that you expect a null when the user is not found, however, in reactive streams null is invalid. Instead, reactive streams have a dedicated empty state and dedicated operators to handle the empty case.
In your example you could do the following:
public Mono<ServerResponse> saveUser(ServerRequest serverRequest)
{
return serverRequest.bodyToMono(User.class)
.flatMap(this::createUserIfNotExists);
}
private Mono<ServerResponse> createUserIfNotExists(User user)
{
return userRepository.findByEmail(user.getEmail())
.hasElement()
.flatMap(foundUser ->
{
if (foundUser)
{
System.out.println("found user");
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Email already exists");
} else
{
System.out.println("creating user");
return status(CREATED).contentType(APPLICATION_JSON)
.body(userRepository.save(user), User.class);
}
});
}
or another alternative:
public Mono<ServerResponse> saveUser(ServerRequest serverRequest)
{
return serverRequest.bodyToMono(User.class)
.flatMap(this::createUserIfNotExists);
}
private Mono<ServerResponse> createUserIfNotExists(User user)
{
return userRepository.findByEmail(user.getEmail())
.flatMap(foundUser ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject("User already exists."))
)
.switchIfEmpty(
userRepository.save(user)
.flatMap(newUser -> status(CREATED).contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser)))
);
}

Spring security custom FilterInvocationSecurityMetadataSource implementation 403 forbidden issue

To make things short I'm trying to implement a custom FilterInvocationSecurityMetadataSource in order to secure/authorize certain parts/URL endpoints dynamically in my web app using spring security 5.0.6 and Spring Boot 2.0.3.
The issue is that no matter what Role I use it always gives me the forbidden page.
I have tried several things with different role names and (believe me) I have searched the whole internet even on spring security 5.0.6 books but nothing seems to work.
This issue may be similar to this: Spring Security issue with securing URLs dynamically
Below the relevant parts of the custom FilterInvocationSecurityMetadataSource
public class DbFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
public Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException {
FilterInvocation fi=(FilterInvocation)object;
String url=fi.getRequestUrl();
System.out.println("URL requested: " + url);
String[] stockArr = new String[]{"ROLE_ADMIN"};
return SecurityConfig.createList(stockArr);
}
Below the relevant parts of the custom implementation of securitywebconfigAdapter
#Configuration
public class Security extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
FilterInvocationSecurityMetadataSource newSource = new DbFilterInvocationSecurityMetadataSource();
fsi.setSecurityMetadataSource(newSource);
return fsi;
}
})
.and()
.formLogin()
.permitAll();
}
Below the relevant parts for custom userDetails authorities.
The user has the role: ROLE_ADMIN in database.
public class CustomUserDetails extends User implements UserDetails {
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<String> dbRoles=new ArrayList<String>();
for (Role userRole : super.getRoles()) {
dbRoles.add(userRole.getType());
}
List<SimpleGrantedAuthority> authorities=new ArrayList<SimpleGrantedAuthority>();
for (String role : dbRoles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
What am I doing wrong??
If more code is needed just comment below.
If you have even good books where I can learn this dynamic part of Spring security authorization comment below.
Thanks!
I managed to get into the security flow by debugging and it seems that by creating ConfigAttributes of this SecurityConfig class is the 'culprit'
return SecurityConfig.createList(stockArr);
public static List<ConfigAttribute> createList(String... attributeNames) {
Assert.notNull(attributeNames, "You must supply an array of attribute names");
List<ConfigAttribute> attributes = new ArrayList(attributeNames.length);
String[] var2 = attributeNames;
int var3 = attributeNames.length;
for(int var4 = 0; var4 < var3; ++var4) {
String attribute = var2[var4];
attributes.add(new SecurityConfig(attribute.trim()));
}
return attributes;
}
Above is the actual implementation of the method where you can see
attributes.add(new SecurityConfig(attribute.trim()));
And this always creates an instance of SecurityConfig type.
And below you can actually see where and how the decision is being made.
private WebExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) {
Iterator var2 = attributes.iterator();
ConfigAttribute attribute;
do {
if (!var2.hasNext()) {
return null;
}
attribute = (ConfigAttribute)var2.next();
} while(!(attribute instanceof WebExpressionConfigAttribute));
return (WebExpressionConfigAttribute)attribute;
}
So in order for it to actually return a configattribute for checking it must be of type WebExpressionConfigAttribute which is never going to be the case because of this
attributes.add(new SecurityConfig(attribute.trim()));
So the way I fixed it is to create my own accessDecisionManager the following way
public class MyAccessDecisionManager implements AccessDecisionManager {
#Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
if(configAttributes == null){
return ;
}
Iterator<ConfigAttribute> ite = configAttributes.iterator();
while(ite.hasNext()){
ConfigAttribute ca = ite.next();
String needRole = ((SecurityConfig)ca).getAttribute();
for(GrantedAuthority grantedAuthority : authentication.getAuthorities()){
if(needRole.trim().equals(grantedAuthority.getAuthority().trim())){
return;
}
}
}
throw new AccessDeniedException("Access is denied");
}
And registering as above now setting the accessdecisionManager with my custom one
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
FilterInvocationSecurityMetadataSource newSource = new DbFilterInvocationSecurityMetadataSource();
fsi.setSecurityMetadataSource(newSource);
fsi.setAccessDecisionManager(new MyAccessDecisionManager());
return fsi;
}