Implement HTTP client authentication on cluster configuration push when sending configuration meta-data from client to server (cluster Manager).

Resolves gh-16.
This commit is contained in:
John Blum
2019-04-24 00:27:30 -07:00
parent 786deaeef9
commit a293b97113
6 changed files with 208 additions and 10 deletions

View File

@@ -35,6 +35,7 @@ import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
@@ -58,6 +59,7 @@ import org.springframework.lang.Nullable;
* @see org.springframework.boot.cloud.CloudPlatform
* @see org.springframework.boot.env.EnvironmentPostProcessor
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Import
* @see org.springframework.core.env.ConfigurableEnvironment
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.PropertiesPropertySource
@@ -65,6 +67,7 @@ import org.springframework.lang.Nullable;
* @see org.springframework.data.gemfire.config.annotation.EnableSecurity
* @see org.springframework.data.gemfire.config.annotation.support.AutoConfiguredAuthenticationInitializer
* @see org.springframework.geode.boot.autoconfigure.ClientCacheAutoConfiguration
* @see org.springframework.geode.boot.autoconfigure.HttpBasicAuthenticationSecurityConfiguration
* @see org.springframework.geode.core.env.VcapPropertySource
* @see org.springframework.geode.core.env.support.CloudCacheService
* @see org.springframework.geode.core.env.support.Service
@@ -76,6 +79,7 @@ import org.springframework.lang.Nullable;
@ConditionalOnClass({ ClientCacheFactoryBean.class, ClientCache.class })
@ConditionalOnMissingBean(GemFireCache.class)
@EnableSecurity
@Import(HttpBasicAuthenticationSecurityConfiguration.class)
@SuppressWarnings("unused")
public class ClientSecurityAutoConfiguration {

View File

@@ -0,0 +1,198 @@
/*
* Copyright 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.geode.boot.autoconfigure;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.util.Optional;
import org.apache.geode.management.internal.security.ResourceConstants;
import org.apache.shiro.util.Assert;
import org.apache.shiro.util.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.gemfire.config.admin.remote.RestHttpGemfireAdminTemplate;
import org.springframework.data.gemfire.config.annotation.ClusterConfigurationConfiguration;
import org.springframework.geode.core.util.ObjectUtils;
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.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.client.RestTemplate;
/**
* Spring {@link Configuration} class used to configure and initialize a Java Platform {@link Authenticator},
* which is used by the Java Platform anytime a Java process needs to make a secure network connection.
*
* @author John Blum
* @see java.net.Authenticator
* @see java.net.PasswordAuthentication
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.core.env.Environment
* @see org.springframework.http.client.ClientHttpRequestInterceptor
* @since 1.0.0
*/
@Configuration
@SuppressWarnings("unused")
public class HttpBasicAuthenticationSecurityConfiguration {
private static final String DEFAULT_USERNAME = "test";
private static final String DEFAULT_PASSWORD = DEFAULT_USERNAME;
private static final String SPRING_DATA_GEMFIRE_SECURITY_USERNAME_PROPERTY =
"spring.data.gemfire.security.username";
private static final String SPRING_DATA_GEMFIRE_SECURITY_PASSWORD_PROPERTY =
"spring.data.gemfire.security.password";
@Bean
public Authenticator authenticator(Environment environment) {
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
String username =
environment.getProperty(SPRING_DATA_GEMFIRE_SECURITY_USERNAME_PROPERTY, DEFAULT_USERNAME);
String password =
environment.getProperty(SPRING_DATA_GEMFIRE_SECURITY_PASSWORD_PROPERTY, DEFAULT_PASSWORD);
return new PasswordAuthentication(username, password.toCharArray());
}
};
Authenticator.setDefault(authenticator);
return authenticator;
}
@Bean
BeanPostProcessor schemaObjectInitializerPostProcessor(Environment environment) {
return new BeanPostProcessor() {
@Nullable @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ClusterConfigurationConfiguration.ClusterSchemaObjectInitializer) {
Optional.of(bean)
.map(ClusterConfigurationConfiguration.ClusterSchemaObjectInitializer.class::cast)
.map(schemaObjectInitializer -> invokeMethod(schemaObjectInitializer, "getSchemaObjectContext"))
.filter(ClusterConfigurationConfiguration.SchemaObjectContext.class::isInstance)
.map(ClusterConfigurationConfiguration.SchemaObjectContext.class::cast)
.map(schemaObjectContext -> invokeMethod(schemaObjectContext, "getGemfireAdminOperations"))
.filter(RestHttpGemfireAdminTemplate.class::isInstance)
.map(RestHttpGemfireAdminTemplate.class::cast)
.map(gemfireAdminTemplate -> invokeMethod(gemfireAdminTemplate, "getRestOperations"))
.filter(RestTemplate.class::isInstance)
.map(RestTemplate.class::cast)
.ifPresent(restTemplate -> registerInterceptor(restTemplate,
new SecurityAwareClientHttpRequestInterceptor(environment)));
}
return bean;
}
};
}
private RestTemplate registerInterceptor(RestTemplate restTemplate,
ClientHttpRequestInterceptor clientHttpRequestInterceptor) {
restTemplate.getInterceptors().add(clientHttpRequestInterceptor);
return restTemplate;
}
@SuppressWarnings("unchecked")
private <T> T invokeMethod(Object target, String methodName) {
return ObjectUtils.doOperationSafely(() -> {
Method method = target.getClass().getDeclaredMethod(methodName);
ReflectionUtils.makeAccessible(method);
return (T) ReflectionUtils.invokeMethod(method, target);
});
}
private <T> T doOperationSafely(ExceptionThrowingOperation<T> operation) {
try {
return operation.doOperation();
}
catch (Exception ignore) {
return null;
}
}
@FunctionalInterface
interface ExceptionThrowingOperation<T> {
T doOperation() throws Exception;
}
public static class SecurityAwareClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private final Environment environment;
public SecurityAwareClientHttpRequestInterceptor(Environment environment) {
Assert.notNull(environment, "Environment is required");
this.environment = environment;
}
protected boolean isAuthenticationEnabled() {
return StringUtils.hasText(getUsername()) && StringUtils.hasText(getPassword());
}
protected String getUsername() {
return this.environment.getProperty(SPRING_DATA_GEMFIRE_SECURITY_USERNAME_PROPERTY);
}
protected String getPassword() {
return this.environment.getProperty(SPRING_DATA_GEMFIRE_SECURITY_PASSWORD_PROPERTY);
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
HttpHeaders requestHeaders = request.getHeaders();
if (isAuthenticationEnabled()) {
requestHeaders.add(ResourceConstants.USER_NAME, getUsername());
requestHeaders.add(ResourceConstants.PASSWORD, getPassword());
}
return execution.execute(request, body);
}
}
}

View File

@@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.gemfire.client.ClientCacheFactoryBean;
import org.springframework.data.gemfire.config.annotation.ApacheShiroSecurityConfiguration;
import org.springframework.data.gemfire.config.annotation.EnableBeanFactoryLocator;
@@ -34,12 +35,14 @@ import org.springframework.data.gemfire.config.annotation.GeodeIntegratedSecurit
* @see org.apache.geode.security.SecurityManager
* @see org.springframework.boot.autoconfigure.EnableAutoConfiguration
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Import
* @see org.springframework.data.gemfire.CacheFactoryBean
* @see org.springframework.data.gemfire.client.ClientCacheFactoryBean
* @see org.springframework.data.gemfire.config.annotation.ApacheShiroSecurityConfiguration
* @see org.springframework.data.gemfire.config.annotation.EnableBeanFactoryLocator
* @see org.springframework.data.gemfire.config.annotation.EnableSecurity
* @see org.springframework.data.gemfire.config.annotation.GeodeIntegratedSecurityConfiguration
* @see org.springframework.geode.boot.autoconfigure.HttpBasicAuthenticationSecurityConfiguration
* @see org.springframework.geode.security.support.SecurityManagerProxy
* @since 1.0.0
*/
@@ -52,6 +55,7 @@ import org.springframework.data.gemfire.config.annotation.GeodeIntegratedSecurit
})
@EnableBeanFactoryLocator
@EnableSecurity(securityManagerClassName = "org.springframework.geode.security.support.SecurityManagerProxy")
@Import(HttpBasicAuthenticationSecurityConfiguration.class)
@SuppressWarnings("unused")
public class PeerSecurityAutoConfiguration {

View File

@@ -0,0 +1,176 @@
/*
* Copyright 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.geode.boot.autoconfigure.cluster;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.geode.cache.GemFireCache;
import org.apache.geode.cache.Region;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.data.gemfire.config.annotation.CacheServerApplication;
import org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration;
import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions;
import org.springframework.data.gemfire.config.annotation.EnableLogging;
import org.springframework.data.gemfire.config.annotation.EnableManager;
import org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport;
import org.springframework.geode.security.TestSecurityManager;
import org.springframework.test.context.junit4.SpringRunner;
import example.app.model.Book;
import example.app.model.ISBN;
/**
* Integration tests testing the SDG {@link EnableClusterConfiguration} annotation functionality
* when the GemFire/Geode server is configured with Security (Authentication).
*
* @author John Blum
* @see org.junit.Test
* @see org.springframework.boot.test.context.SpringBootTest
* @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration
* @see org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport
* @see org.springframework.test.context.junit4.SpringRunner
* @since 1.0.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest(
classes = ClusterConfigurationWithAuthenticationIntegrationTests.GeodeClientConfiguration.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = {
"spring.data.gemfire.security.username=test",
"spring.data.gemfire.security.password=test"
}
)
@SuppressWarnings("unused")
public class ClusterConfigurationWithAuthenticationIntegrationTests extends ForkingClientServerIntegrationTestsSupport {
private static final String GEMFIRE_LOG_LEVEL = "error";
@BeforeClass
public static void startGemFireServer() throws IOException {
startGemFireServer(GeodeServerConfiguration.class);
}
@Autowired
@Qualifier("booksTemplate")
private GemfireTemplate booksTemplate;
@Before
public void setup() {
assertThat(this.booksTemplate).isNotNull();
}
@Test
public void clusterConfigurationAndRegionDataAccessOperationsAreSuccessful() {
Book expetedSeriesOfUnfortunateEvents = Book
.newBook("A Serious of Unfortunate Events")
.identifiedBy(ISBN.autoGenerated());
this.booksTemplate.put(expetedSeriesOfUnfortunateEvents.getIsbn(), expetedSeriesOfUnfortunateEvents);
Book actualSeriesOfUnfortunateEvents = this.booksTemplate.get(expetedSeriesOfUnfortunateEvents.getIsbn());
assertThat(actualSeriesOfUnfortunateEvents).isNotNull();
assertThat(actualSeriesOfUnfortunateEvents).isEqualTo(expetedSeriesOfUnfortunateEvents);
assertThat(actualSeriesOfUnfortunateEvents).isNotSameAs(expetedSeriesOfUnfortunateEvents);
}
@SpringBootApplication
@EnableLogging(logLevel = GEMFIRE_LOG_LEVEL)
@EnableEntityDefinedRegions(basePackageClasses = Book.class)
@EnableClusterConfiguration(useHttp = true)
static class GeodeClientConfiguration { }
@SpringBootApplication
@ComponentScan(excludeFilters =
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = GeodeClientConfiguration.class)
)
@CacheServerApplication(name = "ClusterConfigurationWithAuthenticationIntegrationTests", logLevel = GEMFIRE_LOG_LEVEL)
@EnableManager(start = true)
static class GeodeServerConfiguration {
private static final String GEODE_HOME_PROPERTY = DistributionConfig.GEMFIRE_PREFIX + "home";
public static void main(String[] args) throws IOException {
resolveAndConfigureGeodeHome();
//System.err.printf("%s [%s]%n", GEODE_HOME_PROPERTY, System.getProperty(GEODE_HOME_PROPERTY));
new SpringApplicationBuilder(GeodeServerConfiguration.class)
.web(WebApplicationType.NONE)
.build()
.run(args);
}
private static void resolveAndConfigureGeodeHome() throws IOException {
ClassPathResource resource = new ClassPathResource("/geode-home");
File resourceFile = resource.getFile();
System.setProperty(GEODE_HOME_PROPERTY, resourceFile.getAbsolutePath());
}
@Bean
org.apache.geode.security.SecurityManager testSecurityManager() {
return new TestSecurityManager();
}
@Bean
ApplicationRunner peerCacheVerifier(GemFireCache cache) {
return args -> {
assertThat(cache).isNotNull();
assertThat(GemfireUtils.isPeer(cache)).isTrue();
assertThat(cache.getName())
.isEqualTo(ClusterConfigurationWithAuthenticationIntegrationTests.class.getSimpleName());
List<String> regionNames = cache.rootRegions().stream()
.map(Region::getName)
.collect(Collectors.toList());
assertThat(regionNames)
.describedAs("Expected no Regions; but was [%s]", regionNames)
.isEmpty();
};
}
}
}