Skip to content

Commit a550215

Browse files
committed
Add RSocket and WebFlux Observation Tests
Issue gh-11989 Issue gh-11990
1 parent 46fe012 commit a550215

File tree

2 files changed

+281
-0
lines changed

2 files changed

+281
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.rsocket;
18+
19+
import java.util.ArrayList;
20+
import java.util.Iterator;
21+
import java.util.List;
22+
23+
import io.micrometer.observation.Observation;
24+
import io.micrometer.observation.ObservationHandler;
25+
import io.micrometer.observation.ObservationRegistry;
26+
import io.rsocket.core.RSocketServer;
27+
import io.rsocket.frame.decoder.PayloadDecoder;
28+
import io.rsocket.metadata.WellKnownMimeType;
29+
import io.rsocket.transport.netty.server.CloseableChannel;
30+
import io.rsocket.transport.netty.server.TcpServerTransport;
31+
import org.junit.jupiter.api.AfterEach;
32+
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.api.extension.ExtendWith;
35+
import org.mockito.ArgumentCaptor;
36+
37+
import org.springframework.beans.factory.annotation.Autowired;
38+
import org.springframework.context.annotation.Bean;
39+
import org.springframework.context.annotation.Configuration;
40+
import org.springframework.messaging.handler.annotation.MessageMapping;
41+
import org.springframework.messaging.rsocket.RSocketRequester;
42+
import org.springframework.messaging.rsocket.RSocketStrategies;
43+
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
44+
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
45+
import org.springframework.security.core.userdetails.User;
46+
import org.springframework.security.core.userdetails.UserDetails;
47+
import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor;
48+
import org.springframework.security.rsocket.metadata.SimpleAuthenticationEncoder;
49+
import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata;
50+
import org.springframework.stereotype.Controller;
51+
import org.springframework.test.context.ContextConfiguration;
52+
import org.springframework.test.context.junit.jupiter.SpringExtension;
53+
import org.springframework.util.MimeTypeUtils;
54+
55+
import static org.assertj.core.api.Assertions.assertThat;
56+
import static org.mockito.ArgumentMatchers.any;
57+
import static org.mockito.BDDMockito.given;
58+
import static org.mockito.Mockito.mock;
59+
import static org.mockito.Mockito.times;
60+
import static org.mockito.Mockito.verify;
61+
62+
/**
63+
* @author Rob Winch
64+
*/
65+
@ContextConfiguration
66+
@ExtendWith(SpringExtension.class)
67+
public class HelloRSocketObservationITests {
68+
69+
@Autowired
70+
RSocketMessageHandler handler;
71+
72+
@Autowired
73+
SecuritySocketAcceptorInterceptor interceptor;
74+
75+
@Autowired
76+
ServerController controller;
77+
78+
@Autowired
79+
ObservationHandler<Observation.Context> observationHandler;
80+
81+
private CloseableChannel server;
82+
83+
private RSocketRequester requester;
84+
85+
@BeforeEach
86+
public void setup() {
87+
// @formatter:off
88+
this.server = RSocketServer.create()
89+
.payloadDecoder(PayloadDecoder.ZERO_COPY)
90+
.interceptors((registry) ->
91+
registry.forSocketAcceptor(this.interceptor)
92+
)
93+
.acceptor(this.handler.responder())
94+
.bind(TcpServerTransport.create("localhost", 0))
95+
.block();
96+
// @formatter:on
97+
}
98+
99+
@AfterEach
100+
public void dispose() {
101+
this.requester.rsocket().dispose();
102+
this.server.dispose();
103+
this.controller.payloads.clear();
104+
}
105+
106+
@Test
107+
public void getWhenUsingObservationRegistryThenObservesRequest() {
108+
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("rob", "password");
109+
// @formatter:off
110+
this.requester = RSocketRequester.builder()
111+
.setupMetadata(credentials, MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()))
112+
.rsocketStrategies(this.handler.getRSocketStrategies())
113+
.connectTcp("localhost", this.server.address().getPort())
114+
.block();
115+
// @formatter:on
116+
String data = "rob";
117+
// @formatter:off
118+
this.requester.route("secure.retrieve-mono")
119+
.metadata(credentials, MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()))
120+
.data(data)
121+
.retrieveMono(String.class)
122+
.block();
123+
// @formatter:on
124+
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
125+
verify(this.observationHandler, times(2)).onStart(captor.capture());
126+
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
127+
// once for setup
128+
assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications");
129+
// once for request
130+
assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications");
131+
}
132+
133+
@Configuration
134+
@EnableRSocketSecurity
135+
static class Config {
136+
137+
private ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
138+
139+
@Bean
140+
ServerController controller() {
141+
return new ServerController();
142+
}
143+
144+
@Bean
145+
RSocketMessageHandler messageHandler() {
146+
RSocketMessageHandler handler = new RSocketMessageHandler();
147+
handler.setRSocketStrategies(rsocketStrategies());
148+
return handler;
149+
}
150+
151+
@Bean
152+
RSocketStrategies rsocketStrategies() {
153+
return RSocketStrategies.builder().encoder(new SimpleAuthenticationEncoder()).build();
154+
}
155+
156+
@Bean
157+
MapReactiveUserDetailsService uds() {
158+
// @formatter:off
159+
UserDetails rob = User.withDefaultPasswordEncoder()
160+
.username("rob")
161+
.password("password")
162+
.roles("USER", "ADMIN")
163+
.build();
164+
// @formatter:on
165+
return new MapReactiveUserDetailsService(rob);
166+
}
167+
168+
@Bean
169+
ObservationHandler<Observation.Context> observationHandler() {
170+
return this.handler;
171+
}
172+
173+
@Bean
174+
ObservationRegistry observationRegistry() {
175+
given(this.handler.supportsContext(any())).willReturn(true);
176+
ObservationRegistry registry = ObservationRegistry.create();
177+
registry.observationConfig().observationHandler(this.handler);
178+
return registry;
179+
}
180+
181+
}
182+
183+
@Controller
184+
static class ServerController {
185+
186+
private List<String> payloads = new ArrayList<>();
187+
188+
@MessageMapping("**")
189+
String retrieveMono(String payload) {
190+
add(payload);
191+
return "Hi " + payload;
192+
}
193+
194+
private void add(String p) {
195+
this.payloads.add(p);
196+
}
197+
198+
}
199+
200+
}

Diff for: config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java

+81
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,27 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323
import java.net.URI;
24+
import java.util.Iterator;
2425

26+
import io.micrometer.observation.Observation;
27+
import io.micrometer.observation.ObservationHandler;
28+
import io.micrometer.observation.ObservationRegistry;
2529
import org.junit.jupiter.api.Test;
2630
import org.junit.jupiter.api.extension.ExtendWith;
31+
import org.mockito.ArgumentCaptor;
2732
import reactor.core.publisher.Mono;
2833

2934
import org.springframework.beans.factory.annotation.Autowired;
3035
import org.springframework.context.ApplicationContext;
3136
import org.springframework.context.annotation.Bean;
3237
import org.springframework.context.annotation.Configuration;
38+
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
3339
import org.springframework.security.authentication.TestingAuthenticationToken;
3440
import org.springframework.security.authentication.password.CompromisedPasswordDecision;
3541
import org.springframework.security.authentication.password.CompromisedPasswordException;
3642
import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker;
3743
import org.springframework.security.config.Customizer;
44+
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
3845
import org.springframework.security.config.test.SpringTestContext;
3946
import org.springframework.security.config.test.SpringTestContextExtension;
4047
import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration;
@@ -60,6 +67,12 @@
6067
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
6168

6269
import static org.assertj.core.api.Assertions.assertThat;
70+
import static org.mockito.ArgumentMatchers.any;
71+
import static org.mockito.BDDMockito.given;
72+
import static org.mockito.Mockito.mock;
73+
import static org.mockito.Mockito.times;
74+
import static org.mockito.Mockito.verify;
75+
import static org.springframework.security.config.Customizer.withDefaults;
6376
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
6477
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockAuthentication;
6578
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
@@ -205,6 +218,30 @@ public void resoleMetaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression()
205218
.isEqualTo("harold");
206219
}
207220

221+
@Test
222+
public void getWhenUsingObservationRegistryThenObservesRequest() {
223+
this.spring.register(ObservationRegistryConfig.class).autowire();
224+
// @formatter:off
225+
this.webClient
226+
.get()
227+
.uri("/hello")
228+
.headers((headers) -> headers.setBasicAuth("user", "password"))
229+
.exchange()
230+
.expectStatus()
231+
.isNotFound();
232+
// @formatter:on
233+
ObservationHandler<Observation.Context> handler = this.spring.getContext().getBean(ObservationHandler.class);
234+
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
235+
verify(handler, times(6)).onStart(captor.capture());
236+
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
237+
assertThat(contexts.next().getContextualName()).isEqualTo("http get");
238+
assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain before");
239+
assertThat(contexts.next().getName()).isEqualTo("spring.security.authentications");
240+
assertThat(contexts.next().getName()).isEqualTo("spring.security.authorizations");
241+
assertThat(contexts.next().getName()).isEqualTo("spring.security.http.secured.requests");
242+
assertThat(contexts.next().getContextualName()).isEqualTo("security filterchain after");
243+
}
244+
208245
@Configuration
209246
static class SubclassConfig extends ServerHttpSecurityConfiguration {
210247

@@ -368,4 +405,48 @@ AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
368405

369406
}
370407

408+
@Configuration
409+
@EnableWebFlux
410+
@EnableWebFluxSecurity
411+
static class ObservationRegistryConfig {
412+
413+
private ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
414+
415+
@Bean
416+
SecurityWebFilterChain app(ServerHttpSecurity http) throws Exception {
417+
http.httpBasic(withDefaults()).authorizeExchange((authorize) -> authorize.anyExchange().authenticated());
418+
return http.build();
419+
}
420+
421+
@Bean
422+
ReactiveUserDetailsService userDetailsService() {
423+
return new MapReactiveUserDetailsService(
424+
User.withDefaultPasswordEncoder().username("user").password("password").authorities("app").build());
425+
}
426+
427+
@Bean
428+
ObservationHandler<Observation.Context> observationHandler() {
429+
return this.handler;
430+
}
431+
432+
@Bean
433+
ObservationRegistry observationRegistry() {
434+
given(this.handler.supportsContext(any())).willReturn(true);
435+
ObservationRegistry registry = ObservationRegistry.create();
436+
registry.observationConfig().observationHandler(this.handler);
437+
return registry;
438+
}
439+
440+
}
441+
442+
@EnableRSocketSecurity
443+
static class RSocketSecurityConfig {
444+
445+
@Bean
446+
RSocketMessageHandler messageHandler() {
447+
return new RSocketMessageHandler();
448+
}
449+
450+
}
451+
371452
}

0 commit comments

Comments
 (0)