diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/ApacheShiroSecurityConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/ApacheShiroSecurityConfiguration.java index cebd5a7d..1beb526d 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/ApacheShiroSecurityConfiguration.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/ApacheShiroSecurityConfiguration.java @@ -101,7 +101,7 @@ public class ApacheShiroSecurityConfiguration extends AbstractAnnotationConfigSu public void setBeanFactory(BeanFactory beanFactory) throws BeansException { super.setBeanFactory(Optional.ofNullable(beanFactory) - .filter(it -> it instanceof ListableBeanFactory) + .filter(ListableBeanFactory.class::isInstance) .orElseThrow(() -> newIllegalArgumentException( "BeanFactory [%s] must be an instance of ListableBeanFactory", ObjectUtils.nullSafeClassName(beanFactory)))); @@ -192,8 +192,8 @@ public class ApacheShiroSecurityConfiguration extends AbstractAnnotationConfigSu try { - Map realmBeans = getListableBeanFactory().getBeansOfType(Realm.class, - false, false); + Map realmBeans = + getListableBeanFactory().getBeansOfType(Realm.class, false, false); List realms = new ArrayList<>(CollectionUtils.nullSafeMap(realmBeans).values()); diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/AutoConfiguredAuthenticationConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/AutoConfiguredAuthenticationConfiguration.java index 837d4e03..2a134ee0 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/AutoConfiguredAuthenticationConfiguration.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/AutoConfiguredAuthenticationConfiguration.java @@ -13,21 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.config.annotation; +import java.io.IOException; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URI; import java.util.Optional; import java.util.Properties; +import org.apache.geode.management.internal.security.ResourceConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.data.gemfire.config.annotation.support.AutoConfiguredAuthenticationInitializer; +import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.StringUtils; /** @@ -59,9 +71,105 @@ public class AutoConfiguredAuthenticationConfiguration { protected static final String AUTO_CONFIGURED_AUTH_INIT_STATIC_FACTORY_METHOD = AutoConfiguredAuthenticationInitializer.class.getName().concat(".newAuthenticationInitializer"); + protected static final String DEFAULT_USERNAME = "test"; + protected static final String DEFAULT_PASSWORD = DEFAULT_USERNAME; + protected static final String HTTP_PROTOCOL = "HTTP"; protected static final String SECURITY_CLIENT_AUTH_INIT = "security-client-auth-init"; protected static final String SECURITY_PEER_AUTH_INIT = "security-peer-auth-init"; + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Bean("GemFireSecurityAuthenticator") + public Authenticator authenticator(Environment environment) { + + Authenticator authenticator = new Authenticator() { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + + String username = + environment.getProperty(AutoConfiguredAuthenticationInitializer.SDG_SECURITY_USERNAME_PROPERTY, + DEFAULT_USERNAME); + + String password = + environment.getProperty(AutoConfiguredAuthenticationInitializer.SDG_SECURITY_PASSWORD_PROPERTY, + DEFAULT_PASSWORD); + + return new PasswordAuthentication(username, password.toCharArray()); + } + }; + + Authenticator.setDefault(authenticator); + + return authenticator; + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public ClientHttpRequestInterceptor loggingAwareClientHttpRequestInterceptor() { + + return (request, body, execution) -> { + + logger.debug("HTTP Request URI [{}]", request.getURI()); + + Optional.ofNullable(request.getHeaders()) + .ifPresent(httpHeaders -> { + + CollectionUtils.nullSafeSet(httpHeaders.keySet()).forEach(httpHeaderName -> { + logger.debug("HTTP Request Header Name [{}] Value [{}]", + httpHeaderName, httpHeaders.get(httpHeaderName)); + }); + }); + + ClientHttpResponse response = execution.execute(request, body); + + Optional.ofNullable(response) + .ifPresent(it -> { + + try { + logger.debug("HTTP Response Status Code [{}] Message [{}]", + it.getRawStatusCode(), it.getStatusText()); + } + catch (IOException cause) { + logger.debug("Error occurred getting HTTP Response Status Code and Message", cause); + } + }); + + return response; + }; + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public ClientHttpRequestInterceptor securityAwareClientHttpRequestInterceptor(Authenticator authenticator) { + + return (request, body, execution) -> { + + URI uri = request.getURI(); + + PasswordAuthentication passwordAuthentication = + Authenticator.requestPasswordAuthentication(uri.getHost(), null, uri.getPort(), + HTTP_PROTOCOL, null, uri.getScheme()); + + String username = passwordAuthentication.getUserName(); + char[] password = passwordAuthentication.getPassword(); + + if (isAuthenticationEnabled(username, password)) { + + HttpHeaders requestHeaders = request.getHeaders(); + + requestHeaders.add(ResourceConstants.USER_NAME, username); + requestHeaders.add(ResourceConstants.PASSWORD, String.valueOf(password)); + } + + return execution.execute(request, body); + }; + } + + private boolean isAuthenticationEnabled(String username, char[] password) { + return StringUtils.hasText(username) && password != null && password.length > 0; + } + @Bean public ClientCacheConfigurer authenticationCredentialsSettingClientCacheConfigurer(Environment environment) { return (beanName, beanFactory) -> setAuthenticationCredentials(beanFactory.getProperties(), environment); diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/GeodeIntegratedSecurityConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/GeodeIntegratedSecurityConfiguration.java index 7035187b..2c213926 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/GeodeIntegratedSecurityConfiguration.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/GeodeIntegratedSecurityConfiguration.java @@ -14,7 +14,6 @@ * limitations under the License. * */ - package org.springframework.data.gemfire.config.annotation; import java.lang.annotation.Annotation; @@ -61,6 +60,7 @@ public class GeodeIntegratedSecurityConfiguration extends EmbeddedServiceConfigu * @see #isShiroSecurityNotConfigured() */ protected boolean isShiroSecurityConfigured() { + try { //return resolveBean(ApacheShiroSecurityConfiguration.class).isRealmsPresent(); return false; @@ -92,6 +92,7 @@ public class GeodeIntegratedSecurityConfiguration extends EmbeddedServiceConfigu (String) annotationAttributes.get("clientAuthenticationInitializer"))); if (isShiroSecurityNotConfigured()) { + gemfireProperties.setPropertyIfNotDefault(SECURITY_MANAGER, annotationAttributes.get("securityManagerClass"), Void.class); diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/support/AutoConfiguredAuthenticationInitializer.java b/src/main/java/org/springframework/data/gemfire/config/annotation/support/AutoConfiguredAuthenticationInitializer.java index bfd9a7d8..ebed4863 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/support/AutoConfiguredAuthenticationInitializer.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/support/AutoConfiguredAuthenticationInitializer.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.config.annotation.support; import java.util.Properties; diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterConfigurationWithSecurityIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterConfigurationWithSecurityIntegrationTests.java new file mode 100644 index 00000000..86a1d66b --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/EnableClusterConfigurationWithSecurityIntegrationTests.java @@ -0,0 +1,237 @@ +/* + * Copyright 2018 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 + * + * http://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.data.gemfire.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.data.gemfire.config.annotation.ClusterConfigurationConfiguration.ClusterSchemaObjectInitializer; +import static org.springframework.data.gemfire.config.annotation.ClusterConfigurationConfiguration.SchemaObjectContext; + +import java.lang.reflect.Field; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URI; +import java.util.Optional; +import java.util.Properties; + +import org.apache.geode.management.internal.security.ResourceConstants; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.data.gemfire.config.admin.GemfireAdminOperations; +import org.springframework.data.gemfire.config.admin.remote.RestHttpGemfireAdminTemplate; +import org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.InterceptingClientHttpRequestFactory; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.client.RestTemplate; + +/** + * Integration Test for {@link EnableClusterConfiguration} and {@link EnableSecurity}. + * + * @author John Blum + * @see java.net.Authenticator + * @see java.net.PasswordAuthentication + * @see java.net.URI + * @see java.util.Properties + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.context.ApplicationContextInitializer + * @see org.springframework.context.ConfigurableApplicationContext + * @see org.springframework.core.env.ConfigurableEnvironment + * @see org.springframework.core.env.PropertiesPropertySource + * @see org.springframework.data.gemfire.config.admin.GemfireAdminOperations + * @see org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects + * @see org.springframework.http.client.ClientHttpRequestInterceptor + * @see org.springframework.http.client.InterceptingClientHttpRequestFactory + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @see org.springframework.web.client.RestTemplate + * @since 2.2.0 + */ +@SuppressWarnings("unused") +@RunWith(SpringRunner.class) +@ContextConfiguration(initializers = EnableClusterConfigurationWithSecurityIntegrationTests.SecurityConfigurationApplicationContextInitializer.class) +public class EnableClusterConfigurationWithSecurityIntegrationTests { + + @Autowired + @Qualifier("GemFireSecurityAuthenticator") + private Authenticator authenticator; + + @Autowired + @Qualifier("loggingAwareClientHttpRequestInterceptor") + private ClientHttpRequestInterceptor loggingAwareClientHttpRequestInterceptor; + + @Autowired + @Qualifier("securityAwareClientHttpRequestInterceptor") + private ClientHttpRequestInterceptor securityAwareClientHttpRequestInterceptor; + + @Autowired + private ClusterSchemaObjectInitializer initializer; + + @SuppressWarnings("unchecked") + private T resolveFieldValue(Object target, String fieldName) throws NoSuchFieldException { + + Field field = ReflectionUtils.findField(target.getClass(), fieldName); + + return Optional.ofNullable(field) + .map(it -> { + ReflectionUtils.makeAccessible(it); + return field; + }) + .map(it -> (T) ReflectionUtils.getField(it, target)) + .orElseThrow(() -> + new NoSuchFieldException(String.format("Field with name [%s] was not found on Object of type [%s]", + fieldName, target.getClass().getName()))); + } + + @Before + public void setup() { + + assertThat(this.authenticator).isNotNull(); + assertThat(this.initializer).isNotNull(); + assertThat(this.loggingAwareClientHttpRequestInterceptor).isNotNull(); + assertThat(this.securityAwareClientHttpRequestInterceptor).isNotNull(); + } + + @Test + public void authenticatorReturnsPasswordAuthenticationWithGemFireSecurityProperties() { + + PasswordAuthentication passwordAuthentication = + Authenticator.requestPasswordAuthentication("localhost", null, 80, + "https", null, "https"); + + assertThat(passwordAuthentication).isNotNull(); + assertThat(passwordAuthentication.getUserName()).isEqualTo("skeletor"); + assertThat(String.valueOf(passwordAuthentication.getPassword())).isEqualTo("s3cr3t"); + } + + @Test + public void initializerRestTemplateIncludesClientHttpRequestInterceptors() throws Exception { + + SchemaObjectContext schemaObjectContext = this.initializer.getSchemaObjectContext(); + + assertThat(schemaObjectContext).isNotNull(); + assertThat(schemaObjectContext.getGemfireAdminOperations()) + .isInstanceOf(RestHttpGemfireAdminTemplate.class); + + RestHttpGemfireAdminTemplate template = schemaObjectContext.getGemfireAdminOperations(); + + RestTemplate restTemplate = resolveFieldValue(template, "restTemplate"); + + assertThat(restTemplate).isNotNull(); + assertThat(restTemplate.getInterceptors()).containsExactly(this.securityAwareClientHttpRequestInterceptor, + this.loggingAwareClientHttpRequestInterceptor); + assertThat(restTemplate.getRequestFactory()).isInstanceOf(InterceptingClientHttpRequestFactory.class); + } + + @Test + public void securityAwareClientHttpRequestInterceptorAppliesGemFireSecurityPropertiesToHttpHeaders() + throws Exception { + + byte[] body = new byte[0]; + + URI uri = URI.create("https://localhost:8080/gemfire/v1"); + + ClientHttpRequestExecution mockClientHttpRequestExecution = mock(ClientHttpRequestExecution.class); + + HttpHeaders httpHeaders = new HttpHeaders(); + + HttpRequest mockHttpRequest = mock(HttpRequest.class); + + when(mockHttpRequest.getHeaders()).thenReturn(httpHeaders); + when(mockHttpRequest.getURI()).thenReturn(uri); + + this.securityAwareClientHttpRequestInterceptor.intercept(mockHttpRequest, body, mockClientHttpRequestExecution); + + assertThat(httpHeaders).containsKeys(ResourceConstants.USER_NAME, ResourceConstants.PASSWORD); + assertThat(httpHeaders.getFirst(ResourceConstants.USER_NAME)).isEqualTo("skeletor"); + assertThat(httpHeaders.getFirst(ResourceConstants.PASSWORD)).isEqualTo("s3cr3t"); + + verify(mockClientHttpRequestExecution, times(1)).execute(eq(mockHttpRequest), eq(body)); + verify(mockHttpRequest, times(1)).getHeaders(); + verify(mockHttpRequest, times(1)).getURI(); + } + + static class SecurityConfigurationApplicationContextInitializer + implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + + Properties gemfireSecurityProperties = new Properties(); + + gemfireSecurityProperties.setProperty("spring.data.gemfire.security.username", "skeletor"); + gemfireSecurityProperties.setProperty("spring.data.gemfire.security.password", "s3cr3t"); + + environment.getPropertySources(). + addFirst(new PropertiesPropertySource("EnableClusterConfigurationWithSecurityIntegrationTests", + gemfireSecurityProperties)); + } + } + + @ClientCacheApplication + @EnableSecurity + @EnableGemFireMockObjects + @EnableClusterConfiguration(useHttp = true) + static class TestConfiguration { + + @Bean + BeanPostProcessor clusterSchemaObjectInitializerPostProcessor() { + + return new BeanPostProcessor() { + + @Nullable @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + if (bean instanceof ClusterSchemaObjectInitializer) { + + ClusterSchemaObjectInitializer initializer = spy((ClusterSchemaObjectInitializer) bean); + + doReturn(false).when(initializer).isAutoStartup(); + + bean = initializer; + } + + return bean; + } + }; + } + } +}