33
33
import io .micrometer .observation .ObservationRegistry ;
34
34
import io .micrometer .observation .ObservationTextPublisher ;
35
35
import jakarta .annotation .security .DenyAll ;
36
+ import org .aopalliance .aop .Advice ;
36
37
import org .aopalliance .intercept .MethodInterceptor ;
37
38
import org .aopalliance .intercept .MethodInvocation ;
38
39
import org .junit .jupiter .api .Test ;
42
43
import org .mockito .Mockito ;
43
44
44
45
import org .springframework .aop .Advisor ;
46
+ import org .springframework .aop .Pointcut ;
45
47
import org .springframework .aop .config .AopConfigUtils ;
46
48
import org .springframework .aop .support .DefaultPointcutAdvisor ;
47
49
import org .springframework .aop .support .JdkRegexpMethodPointcut ;
62
64
import org .springframework .core .annotation .AnnotationAwareOrderComparator ;
63
65
import org .springframework .core .annotation .AnnotationConfigurationException ;
64
66
import org .springframework .core .annotation .Order ;
67
+ import org .springframework .http .HttpStatus ;
65
68
import org .springframework .http .HttpStatusCode ;
69
+ import org .springframework .http .MediaType ;
66
70
import org .springframework .http .ResponseEntity ;
71
+ import org .springframework .http .converter .HttpMessageNotWritableException ;
67
72
import org .springframework .security .access .AccessDeniedException ;
68
73
import org .springframework .security .access .PermissionEvaluator ;
69
74
import org .springframework .security .access .annotation .BusinessService ;
95
100
import org .springframework .security .authorization .method .MethodInvocationResult ;
96
101
import org .springframework .security .authorization .method .PrePostTemplateDefaults ;
97
102
import org .springframework .security .config .annotation .SecurityContextChangedListenerConfig ;
103
+ import org .springframework .security .config .annotation .web .configuration .EnableWebSecurity ;
98
104
import org .springframework .security .config .core .GrantedAuthorityDefaults ;
99
105
import org .springframework .security .config .observation .SecurityObservationSettings ;
100
106
import org .springframework .security .config .test .SpringTestContext ;
106
112
import org .springframework .security .test .context .support .WithAnonymousUser ;
107
113
import org .springframework .security .test .context .support .WithMockUser ;
108
114
import org .springframework .security .test .context .support .WithSecurityContextTestExecutionListener ;
115
+ import org .springframework .security .web .util .ThrowableAnalyzer ;
109
116
import org .springframework .stereotype .Component ;
117
+ import org .springframework .stereotype .Service ;
110
118
import org .springframework .test .context .ContextConfiguration ;
111
119
import org .springframework .test .context .TestExecutionListeners ;
112
120
import org .springframework .test .context .junit .jupiter .SpringExtension ;
121
+ import org .springframework .test .web .servlet .MockMvc ;
122
+ import org .springframework .test .web .servlet .MvcResult ;
123
+ import org .springframework .test .web .servlet .request .MockHttpServletRequestBuilder ;
124
+ import org .springframework .web .bind .annotation .ControllerAdvice ;
125
+ import org .springframework .web .bind .annotation .ExceptionHandler ;
126
+ import org .springframework .web .bind .annotation .GetMapping ;
127
+ import org .springframework .web .bind .annotation .RequestParam ;
128
+ import org .springframework .web .bind .annotation .RestController ;
113
129
import org .springframework .web .context .ConfigurableWebApplicationContext ;
114
130
import org .springframework .web .context .support .AnnotationConfigWebApplicationContext ;
115
131
import org .springframework .web .servlet .ModelAndView ;
132
+ import org .springframework .web .servlet .config .annotation .EnableWebMvc ;
116
133
117
134
import static org .assertj .core .api .Assertions .assertThat ;
118
135
import static org .assertj .core .api .Assertions .assertThatExceptionOfType ;
127
144
import static org .mockito .Mockito .times ;
128
145
import static org .mockito .Mockito .verify ;
129
146
import static org .mockito .Mockito .verifyNoInteractions ;
147
+ import static org .springframework .security .test .web .servlet .request .SecurityMockMvcRequestPostProcessors .user ;
148
+ import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .get ;
149
+ import static org .springframework .test .web .servlet .result .MockMvcResultMatchers .status ;
130
150
131
151
/**
132
152
* Tests for {@link PrePostMethodSecurityConfiguration}.
@@ -148,6 +168,9 @@ public class PrePostMethodSecurityConfigurationTests {
148
168
@ Autowired (required = false )
149
169
BusinessService businessService ;
150
170
171
+ @ Autowired (required = false )
172
+ MockMvc mvc ;
173
+
151
174
@ WithMockUser
152
175
@ Test
153
176
public void customMethodSecurityPreAuthorizeAdminWhenRoleUserThenAccessDeniedException () {
@@ -1181,6 +1204,97 @@ void autowireWhenDefaultsThenAdvisorAnnotationsAreSorted() {
1181
1204
}
1182
1205
}
1183
1206
1207
+ @ Test
1208
+ void getWhenPostAuthorizeAuthenticationNameMatchesThenRespondsWithOk () throws Exception {
1209
+ this .spring .register (WebMvcMethodSecurityConfig .class , BasicController .class ).autowire ();
1210
+ // @formatter:off
1211
+ MockHttpServletRequestBuilder requestWithUser = get ("/authorized-person" )
1212
+ .param ("name" , "rob" )
1213
+ .with (user ("rob" ));
1214
+ // @formatter:on
1215
+ this .mvc .perform (requestWithUser ).andExpect (status ().isOk ());
1216
+ }
1217
+
1218
+ @ Test
1219
+ void getWhenPostAuthorizeAuthenticationNameNotMatchThenRespondsWithForbidden () throws Exception {
1220
+ this .spring .register (WebMvcMethodSecurityConfig .class , BasicController .class ).autowire ();
1221
+ // @formatter:off
1222
+ MockHttpServletRequestBuilder requestWithUser = get ("/authorized-person" )
1223
+ .param ("name" , "john" )
1224
+ .with (user ("rob" ));
1225
+ // @formatter:on
1226
+ this .mvc .perform (requestWithUser ).andExpect (status ().isForbidden ());
1227
+ }
1228
+
1229
+ @ Test
1230
+ void getWhenPostAuthorizeWithinServiceAuthenticationNameMatchesThenRespondsWithOk () throws Exception {
1231
+ this .spring .register (WebMvcMethodSecurityConfig .class , BasicController .class , BasicService .class ).autowire ();
1232
+ // @formatter:off
1233
+ MockHttpServletRequestBuilder requestWithUser = get ("/greetings/authorized-person" )
1234
+ .param ("name" , "rob" )
1235
+ .with (user ("rob" ));
1236
+ // @formatter:on
1237
+ MvcResult mvcResult = this .mvc .perform (requestWithUser ).andExpect (status ().isOk ()).andReturn ();
1238
+ assertThat (mvcResult .getResponse ().getContentAsString ()).isEqualTo ("Hello: rob" );
1239
+ }
1240
+
1241
+ @ Test
1242
+ void getWhenPostAuthorizeWithinServiceAuthenticationNameNotMatchThenCustomHandlerRespondsWithForbidden ()
1243
+ throws Exception {
1244
+ this .spring
1245
+ .register (WebMvcMethodSecurityConfig .class , BasicController .class , BasicService .class ,
1246
+ BasicControllerAdvice .class )
1247
+ .autowire ();
1248
+ // @formatter:off
1249
+ MockHttpServletRequestBuilder requestWithUser = get ("/greetings/authorized-person" )
1250
+ .param ("name" , "john" )
1251
+ .with (user ("rob" ));
1252
+ // @formatter:on
1253
+ MvcResult mvcResult = this .mvc .perform (requestWithUser ).andExpect (status ().isForbidden ()).andReturn ();
1254
+ assertThat (mvcResult .getResponse ().getContentAsString ()).isEqualTo ("""
1255
+ {"message":"Access Denied"}\
1256
+ """ );
1257
+ }
1258
+
1259
+ @ Test
1260
+ void getWhenPostAuthorizeAuthenticationNameNotMatchThenCustomHandlerRespondsWithForbidden () throws Exception {
1261
+ this .spring
1262
+ .register (WebMvcMethodSecurityConfig .class , BasicController .class , BasicService .class ,
1263
+ BasicControllerAdvice .class )
1264
+ .autowire ();
1265
+ // @formatter:off
1266
+ MockHttpServletRequestBuilder requestWithUser = get ("/authorized-person" )
1267
+ .param ("name" , "john" )
1268
+ .with (user ("rob" ));
1269
+ // @formatter:on
1270
+ MvcResult mvcResult = this .mvc .perform (requestWithUser ).andExpect (status ().isForbidden ()).andReturn ();
1271
+ assertThat (mvcResult .getResponse ().getContentAsString ()).isEqualTo ("""
1272
+ {"message":"Could not write JSON: Access Denied"}\
1273
+ """ );
1274
+ }
1275
+
1276
+ @ Test
1277
+ void getWhenCustomAdvisorAuthenticationNameMatchesThenRespondsWithOk () throws Exception {
1278
+ this .spring .register (WebMvcMethodSecurityCustomAdvisorConfig .class , BasicController .class ).autowire ();
1279
+ // @formatter:off
1280
+ MockHttpServletRequestBuilder requestWithUser = get ("/authorized-person" )
1281
+ .param ("name" , "rob" )
1282
+ .with (user ("rob" ));
1283
+ // @formatter:on
1284
+ this .mvc .perform (requestWithUser ).andExpect (status ().isOk ());
1285
+ }
1286
+
1287
+ @ Test
1288
+ void getWhenCustomAdvisorAuthenticationNameNotMatchThenRespondsWithForbidden () throws Exception {
1289
+ this .spring .register (WebMvcMethodSecurityCustomAdvisorConfig .class , BasicController .class ).autowire ();
1290
+ // @formatter:off
1291
+ MockHttpServletRequestBuilder requestWithUser = get ("/authorized-person" )
1292
+ .param ("name" , "john" )
1293
+ .with (user ("rob" ));
1294
+ // @formatter:on
1295
+ this .mvc .perform (requestWithUser ).andExpect (status ().isForbidden ());
1296
+ }
1297
+
1184
1298
private static Consumer <ConfigurableWebApplicationContext > disallowBeanOverriding () {
1185
1299
return (context ) -> ((AnnotationConfigWebApplicationContext ) context ).setAllowBeanDefinitionOverriding (false );
1186
1300
}
@@ -1919,4 +2033,118 @@ void onRequestDenied(AuthorizationDeniedEvent<? extends MethodInvocation> denied
1919
2033
1920
2034
}
1921
2035
2036
+ @ EnableWebMvc
2037
+ @ EnableWebSecurity
2038
+ @ EnableMethodSecurity
2039
+ static class WebMvcMethodSecurityConfig {
2040
+
2041
+ }
2042
+
2043
+ @ EnableWebMvc
2044
+ @ EnableWebSecurity
2045
+ @ EnableMethodSecurity
2046
+ static class WebMvcMethodSecurityCustomAdvisorConfig {
2047
+
2048
+ @ Bean
2049
+ AuthorizationAdvisor customAdvisor (SecurityContextHolderStrategy strategy ) {
2050
+ JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut ();
2051
+ pointcut .setPattern (".*AuthorizedPerson.*getName" );
2052
+ return new AuthorizationAdvisor () {
2053
+ @ Override
2054
+ public Object invoke (MethodInvocation mi ) throws Throwable {
2055
+ Authentication auth = strategy .getContext ().getAuthentication ();
2056
+ Object result = mi .proceed ();
2057
+ if (auth .getName ().equals (result )) {
2058
+ return result ;
2059
+ }
2060
+ throw new AccessDeniedException ("Access Denied for User '" + auth .getName () + "'" );
2061
+ }
2062
+
2063
+ @ Override
2064
+ public Pointcut getPointcut () {
2065
+ return pointcut ;
2066
+ }
2067
+
2068
+ @ Override
2069
+ public Advice getAdvice () {
2070
+ return this ;
2071
+ }
2072
+
2073
+ @ Override
2074
+ public int getOrder () {
2075
+ return AuthorizationInterceptorsOrder .POST_FILTER .getOrder () + 1 ;
2076
+ }
2077
+ };
2078
+ }
2079
+
2080
+ }
2081
+
2082
+ @ RestController
2083
+ static class BasicController {
2084
+
2085
+ @ Autowired (required = false )
2086
+ BasicService service ;
2087
+
2088
+ @ GetMapping ("/greetings/authorized-person" )
2089
+ String getAuthorizedPersonGreeting (@ RequestParam String name ) {
2090
+ AuthorizedPerson authorizedPerson = this .service .getAuthorizedPerson (name );
2091
+ return "Hello: " + authorizedPerson .getName ();
2092
+ }
2093
+
2094
+ @ AuthorizeReturnObject
2095
+ @ GetMapping (value = "/authorized-person" , produces = MediaType .APPLICATION_JSON_VALUE )
2096
+ AuthorizedPerson getAuthorizedPerson (@ RequestParam String name ) {
2097
+ return new AuthorizedPerson (name );
2098
+ }
2099
+
2100
+ }
2101
+
2102
+ @ ControllerAdvice
2103
+ static class BasicControllerAdvice {
2104
+
2105
+ @ ExceptionHandler (AccessDeniedException .class )
2106
+ ResponseEntity <Map <String , String >> handleAccessDenied (AccessDeniedException ex ) {
2107
+ Map <String , String > responseBody = Map .of ("message" , ex .getMessage ());
2108
+ return ResponseEntity .status (HttpStatus .FORBIDDEN ).body (responseBody );
2109
+ }
2110
+
2111
+ @ ExceptionHandler (HttpMessageNotWritableException .class )
2112
+ ResponseEntity <Map <String , String >> handleHttpMessageNotWritable (HttpMessageNotWritableException ex ) {
2113
+ ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer ();
2114
+ Throwable [] causeChain = throwableAnalyzer .determineCauseChain (ex );
2115
+ Throwable t = throwableAnalyzer .getFirstThrowableOfType (AccessDeniedException .class , causeChain );
2116
+ if (t != null ) {
2117
+ Map <String , String > responseBody = Map .of ("message" , ex .getMessage ());
2118
+ return ResponseEntity .status (HttpStatus .FORBIDDEN ).body (responseBody );
2119
+ }
2120
+ throw ex ;
2121
+ }
2122
+
2123
+ }
2124
+
2125
+ @ Service
2126
+ static class BasicService {
2127
+
2128
+ @ AuthorizeReturnObject
2129
+ AuthorizedPerson getAuthorizedPerson (String name ) {
2130
+ return new AuthorizedPerson (name );
2131
+ }
2132
+
2133
+ }
2134
+
2135
+ public static class AuthorizedPerson {
2136
+
2137
+ final String name ;
2138
+
2139
+ AuthorizedPerson (String name ) {
2140
+ this .name = name ;
2141
+ }
2142
+
2143
+ @ PostAuthorize ("returnObject == authentication.name" )
2144
+ public String getName () {
2145
+ return this .name ;
2146
+ }
2147
+
2148
+ }
2149
+
1922
2150
}
0 commit comments