2
0
Эх сурвалжийг харах

Allow configuration of HTTP basic through nested builder

Issue: gh-5557
Fixes: gh-6885
Eleftheria Stein 6 жил өмнө
parent
commit
12da990b6b

+ 46 - 0
config/src/main/java/org/springframework/security/config/Customizer.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config;
+
+/**
+ * Callback interface that accepts a single input argument and returns no result,
+ * with the ability to throw a (checked) exception.
+ *
+ * @author Eleftheria Stein
+ * @param <T> the type of the input to the operation
+ * @since 5.2
+ */
+@FunctionalInterface
+public interface Customizer<T> {
+
+	/**
+	 * Performs the customizations on the input argument.
+	 *
+	 * @param t the input argument
+	 * @throws Exception if any error occurs
+	 */
+	void customize(T t) throws Exception;
+
+	/**
+	 * Returns a {@link Customizer} that does not alter the input argument.
+	 *
+	 * @return a {@link Customizer} that does not alter the input argument.
+	 */
+	static <T> Customizer<T> withDefaults() {
+		return t -> {};
+	}
+}

+ 37 - 1
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.SecurityBuilder;
@@ -1094,6 +1095,41 @@ public final class HttpSecurity extends
 		return getOrApply(new HttpBasicConfigurer<>());
 	}
 
+	/**
+	 * Configures HTTP Basic authentication.
+	 *
+	 * <h2>Example Configuration</h2>
+	 *
+	 * The example below demonstrates how to configure HTTP Basic authentication for an
+	 * application. The default realm is "Realm", but can be
+	 * customized using {@link HttpBasicConfigurer#realmName(String)}.
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class HttpBasicSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	&#064;Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
+	 * 				.and()
+	 * 			.httpBasic(withDefaults());
+	 * 	}
+	 * }
+	 * </pre>
+	 *
+	 * @param httpBasicCustomizer the {@link Customizer} to provide more options for
+	 * the {@link HttpBasicConfigurer}
+	 * @return the {@link HttpSecurity} for further customizations
+	 * @throws Exception
+	 */
+	public HttpSecurity httpBasic(Customizer<HttpBasicConfigurer<HttpSecurity>> httpBasicCustomizer) throws Exception {
+		httpBasicCustomizer.customize(getOrApply(new HttpBasicConfigurer<>()));
+		return HttpSecurity.this;
+	}
+
 	public <C> void setSharedObject(Class<C> sharedType, C object) {
 		super.setSharedObject(sharedType, object);
 	}

+ 32 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java

@@ -40,6 +40,7 @@ import javax.servlet.http.HttpServletResponse;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.*;
+import static org.springframework.security.config.Customizer.withDefaults;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@@ -91,6 +92,37 @@ public class HttpBasicConfigurerTests {
 		}
 	}
 
+	@Test
+	public void httpBasicWhenUsingDefaultsInLambdaThenResponseIncludesBasicChallenge() throws Exception {
+		this.spring.register(DefaultsLambdaEntryPointConfig.class).autowire();
+
+		this.mvc.perform(get("/"))
+				.andExpect(status().isUnauthorized())
+				.andExpect(header().string("WWW-Authenticate", "Basic realm=\"Realm\""));
+	}
+
+	@EnableWebSecurity
+	static class DefaultsLambdaEntryPointConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests()
+					.anyRequest().authenticated()
+					.and()
+				.httpBasic(withDefaults());
+			// @formatter:on
+		}
+
+		@Override
+		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+			// @formatter:off
+			auth
+				.inMemoryAuthentication();
+			// @formatter:on
+		}
+	}
+
 	//SEC-2198
 	@Test
 	public void httpBasicWhenUsingDefaultsThenResponseIncludesBasicChallenge() throws Exception {

+ 123 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.java

@@ -38,6 +38,7 @@ import org.springframework.test.web.servlet.MockMvc;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.springframework.security.config.Customizer.withDefaults;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
@@ -102,6 +103,36 @@ public class NamespaceHttpBasicTests {
 		}
 	}
 
+	@Test
+	public void basicAuthenticationWhenUsingDefaultsInLambdaThenMatchesNamespace() throws Exception {
+		this.spring.register(HttpBasicLambdaConfig.class, UserConfig.class).autowire();
+
+		this.mvc.perform(get("/"))
+				.andExpect(status().isUnauthorized());
+
+		this.mvc.perform(get("/")
+				.with(httpBasic("user", "invalid")))
+				.andExpect(status().isUnauthorized())
+				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Realm\""));
+
+		this.mvc.perform(get("/")
+				.with(httpBasic("user", "password")))
+				.andExpect(status().isNotFound());
+	}
+
+	@EnableWebSecurity
+	static class HttpBasicLambdaConfig extends WebSecurityConfigurerAdapter {
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests()
+					.anyRequest().hasRole("USER")
+					.and()
+				.httpBasic(withDefaults());
+			// @formatter:on
+		}
+	}
+
 	/**
 	 * http@realm equivalent
 	 */
@@ -127,6 +158,30 @@ public class NamespaceHttpBasicTests {
 		}
 	}
 
+	@Test
+	public void basicAuthenticationWhenUsingCustomRealmInLambdaThenMatchesNamespace() throws Exception {
+		this.spring.register(CustomHttpBasicLambdaConfig.class, UserConfig.class).autowire();
+
+		this.mvc.perform(get("/")
+				.with(httpBasic("user", "invalid")))
+				.andExpect(status().isUnauthorized())
+				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Custom Realm\""));
+	}
+
+	@EnableWebSecurity
+	static class CustomHttpBasicLambdaConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests()
+					.anyRequest().hasRole("USER")
+					.and()
+				.httpBasic(httpBasicConfig -> httpBasicConfig.realmName("Custom Realm"));
+			// @formatter:on
+		}
+	}
+
 	/**
 	 * http/http-basic@authentication-details-source-ref equivalent
 	 */
@@ -161,6 +216,40 @@ public class NamespaceHttpBasicTests {
 		}
 	}
 
+	@Test
+	public void basicAuthenticationWhenUsingAuthenticationDetailsSourceRefInLambdaThenMatchesNamespace()
+			throws Exception {
+		this.spring.register(AuthenticationDetailsSourceHttpBasicLambdaConfig.class, UserConfig.class).autowire();
+
+		AuthenticationDetailsSource<HttpServletRequest, ?> source =
+				this.spring.getContext().getBean(AuthenticationDetailsSource.class);
+
+		this.mvc.perform(get("/")
+				.with(httpBasic("user", "password")));
+
+		verify(source).buildDetails(any(HttpServletRequest.class));
+	}
+
+	@EnableWebSecurity
+	static class AuthenticationDetailsSourceHttpBasicLambdaConfig extends WebSecurityConfigurerAdapter {
+		AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
+				mock(AuthenticationDetailsSource.class);
+
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.httpBasic(httpBasicConfig ->
+						httpBasicConfig.authenticationDetailsSource(this.authenticationDetailsSource));
+			// @formatter:on
+		}
+
+		@Bean
+		AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource() {
+			return this.authenticationDetailsSource;
+		}
+	}
+
 	/**
 	 * http/http-basic@entry-point-ref
 	 */
@@ -195,4 +284,38 @@ public class NamespaceHttpBasicTests {
 					.authenticationEntryPoint(this.authenticationEntryPoint);
 		}
 	}
+
+	@Test
+	public void basicAuthenticationWhenUsingEntryPointRefInLambdaThenMatchesNamespace() throws Exception {
+		this.spring.register(EntryPointRefHttpBasicLambdaConfig.class, UserConfig.class).autowire();
+
+		this.mvc.perform(get("/"))
+				.andExpect(status().is(999));
+
+		this.mvc.perform(get("/")
+				.with(httpBasic("user", "invalid")))
+				.andExpect(status().is(999));
+
+		this.mvc.perform(get("/")
+				.with(httpBasic("user", "password")))
+				.andExpect(status().isNotFound());
+	}
+
+	@EnableWebSecurity
+	static class EntryPointRefHttpBasicLambdaConfig extends WebSecurityConfigurerAdapter {
+		AuthenticationEntryPoint authenticationEntryPoint =
+				(request, response, ex) -> response.setStatus(999);
+
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests()
+					.anyRequest().hasRole("USER")
+					.and()
+				.httpBasic(httpBasicConfig ->
+						httpBasicConfig.authenticationEntryPoint(this.authenticationEntryPoint));
+			// @formatter:on
+		}
+	}
 }