From cca7533b86fe7f033a494fc138278d4cc5e07705 Mon Sep 17 00:00:00 2001 From: John Blum Date: Mon, 28 May 2018 14:17:31 -0700 Subject: [PATCH] Add security support for configuring an Apache Geode/Pivotal GemFire o.a.g.security.SecurityManager as a managed bean in the Spring context. --- .../support/SecurityManagerProxy.java | 155 ++++++++++++++++++ .../SecurityManagerProxyIntegrationTests.java | 70 ++++++++ .../SecurityManagerProxyUnitTests.java | 150 +++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxy.java create mode 100644 geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxyIntegrationTests.java create mode 100644 geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxyUnitTests.java diff --git a/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxy.java b/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxy.java new file mode 100644 index 00000000..2e346be8 --- /dev/null +++ b/geode-spring-boot-starter/src/main/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxy.java @@ -0,0 +1,155 @@ +/* + * 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.boot.data.geode.security.support; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException; + +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.geode.security.AuthenticationFailedException; +import org.apache.geode.security.ResourcePermission; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.gemfire.support.LazyWiringDeclarableSupport; +import org.springframework.util.Assert; + +/** + * The {@link SecurityManagerProxy} class is an Apache Geode {@link org.apache.geode.security.SecurityManager} + * proxy implementation delegating to a backing {@link org.apache.geode.security.SecurityManager} implementation + * which is registered as a managed bean in a Spring context. + * + * The idea behind this {@link org.apache.geode.security.SecurityManager} is to enable users to be able to configure + * and manage the {@code SecurityManager} as a Spring bean. However, Apache Geode/Pivotal GemFire require + * the {@link org.apache.geode.security.SecurityManager} to be configured using a System property when launching + * Apache Geode Servers with Gfsh, which makes it difficult to "manage" the {@code SecurityManager} instance. + * + * Therefore, this implementation allows a developer to set the Apache Geode System property using this proxy... + * + * + * gemfire.security-manager=org.springframework.boot.data.geode.security.support.SecurityManagerProxy + * + * + * And then declare and define a bean in the Spring context implementing the + * {@link org.apache.geode.security.SecurityManager} interface... + * + * + * @Configuration + * class MyApplicationConfiguration { + * + * @Bean + * ExampleSecurityManager exampleSecurityManager(Environment environment) { + * return new ExampleSecurityManager(environment); + * } + * + * ... + * } + * + * + * @author John Blum + * @see org.apache.geode.security.SecurityManager + * @see org.springframework.data.gemfire.support.LazyWiringDeclarableSupport + * @since 1.0.0 + */ +@SuppressWarnings("unused") +public class SecurityManagerProxy extends LazyWiringDeclarableSupport + implements org.apache.geode.security.SecurityManager { + + private static final AtomicReference INSTANCE = new AtomicReference<>(); + + private org.apache.geode.security.SecurityManager securityManager; + + /** + * Returns a reference to the single {@link SecurityManagerProxy} instance configured by + * Apache Geode/Pivotal GemFire in startup. + * + * @return a reference to the single {@link SecurityManagerProxy} instance. + */ + public static SecurityManagerProxy getInstance() { + + return Optional.ofNullable(INSTANCE.get()) + .orElseThrow(() -> newIllegalStateException("SecurityManagerProxy was not configured")); + } + + + /** + * Constructs a new instance of {@link SecurityManagerProxy}, which will delegate all Apache Geode + * security operations to a Spring managed {@link org.apache.geode.security.SecurityManager} bean. + */ + public SecurityManagerProxy() { + + // TODO remove init() call when GEODE-2083 (https://issues.apache.org/jira/browse/GEODE-2083) is resolved! + // NOTE: the init(:Properties) call in the constructor is less than ideal since... + // 1) it allows the *this* reference to escape, and... + // 2) it is Geode's responsibility to identify Geode Declarable objects and invoke their init(:Properties) method + // However, the init(:Properties) method invocation in the constructor is necessary to enable this Proxy to be + // identified and auto-wired in a Spring context. + + INSTANCE.compareAndSet(null, this); + init(new Properties()); + } + + /** + * Configures a reference to the Apache Geode {@link org.apache.geode.security.SecurityManager} instance + * delegated to by this {@link SecurityManagerProxy}. + * + * @param securityManager reference to the underlying Apache Geode {@link org.apache.geode.security.SecurityManager} + * instance delegated to by this {@link SecurityManagerProxy}. + * @throws IllegalArgumentException if the {@link org.apache.geode.security.SecurityManager} reference + * is {@literal null}. + * @see org.apache.geode.security.SecurityManager + */ + @Autowired + public void setSecurityManager(org.apache.geode.security.SecurityManager securityManager) { + + Assert.notNull(securityManager, "SecurityManager must not be null"); + + this.securityManager = securityManager; + } + + /** + * Returns a reference to the Apache Geode {@link org.apache.geode.security.SecurityManager} instance + * delegated to by this {@link SecurityManagerProxy}. + * + * @return a reference to the underlying {@link org.apache.geode.security.SecurityManager} instance + * delegated to by this {@link SecurityManagerProxy}. + * @throws IllegalStateException if the configured {@link org.apache.geode.security.SecurityManager} + * was not properly configured. + * @see org.apache.geode.security.SecurityManager + */ + protected org.apache.geode.security.SecurityManager getSecurityManager() { + + Assert.state(this.securityManager != null, "No SecurityManager configured"); + + return this.securityManager; + } + + @Override + public Object authenticate(Properties properties) throws AuthenticationFailedException { + return getSecurityManager().authenticate(properties); + } + + @Override + public boolean authorize(Object principal, ResourcePermission permission) { + return getSecurityManager().authorize(principal, permission); + } + + @Override + public void close() { + getSecurityManager().close(); + } +} diff --git a/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxyIntegrationTests.java b/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxyIntegrationTests.java new file mode 100644 index 00000000..54b1c97a --- /dev/null +++ b/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxyIntegrationTests.java @@ -0,0 +1,70 @@ +/* + * 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.boot.data.geode.security.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import org.apache.geode.cache.GemFireCache; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.config.annotation.EnableSecurity; +import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link SecurityManagerProxy}. + * + * @author John Blum + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.security.SecurityManager + * @see org.springframework.context.annotation.Bean + * @see org.springframework.data.gemfire.config.annotation.EnableSecurity + * @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @since 1.0.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +@SuppressWarnings("unused") +public class SecurityManagerProxyIntegrationTests { + + private static final String GEMFIRE_LOG_LEVEL = "error"; + + @Autowired + private org.apache.geode.security.SecurityManager mockSecurityManager; + + @Test + public void securityManagerProxyWasConfiguredWithMockSecurityManager() { + assertThat(SecurityManagerProxy.getInstance().getSecurityManager()).isEqualTo(this.mockSecurityManager); + } + + @PeerCacheApplication(logLevel = GEMFIRE_LOG_LEVEL, useBeanFactoryLocator = true) + @EnableSecurity(securityManagerClassName = + "org.springframework.boot.data.geode.security.support.SecurityManagerProxy") + static class TestConfiguration { + + @Bean + org.apache.geode.security.SecurityManager mockSecurityManager(GemFireCache gemfireCache) { + return mock(org.apache.geode.security.SecurityManager.class); + } + } +} diff --git a/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxyUnitTests.java b/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxyUnitTests.java new file mode 100644 index 00000000..93e18cea --- /dev/null +++ b/geode-spring-boot-starter/src/test/java/org/springframework/boot/data/geode/security/support/SecurityManagerProxyUnitTests.java @@ -0,0 +1,150 @@ +/* + * 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.boot.data.geode.security.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.security.Principal; +import java.util.Properties; + +import org.apache.geode.security.ResourcePermission; +import org.junit.Test; + +/** + * Unit tests for {@link SecurityManagerProxy} + * + * @author John Blum + * @see java.security.Principal + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.boot.data.geode.security.support.SecurityManagerProxy + * @since 1.0.0 + */ +public class SecurityManagerProxyUnitTests { + + @Test + public void setAndGetSecurityManager() { + + org.apache.geode.security.SecurityManager mockSecurityManager = + mock(org.apache.geode.security.SecurityManager.class); + + SecurityManagerProxy securityManagerProxy = new SecurityManagerProxy(); + + securityManagerProxy.setSecurityManager(mockSecurityManager); + + assertThat(securityManagerProxy.getSecurityManager()).isEqualTo(mockSecurityManager); + } + + @Test(expected = IllegalArgumentException.class) + public void setSecurityManagerToNullThrowsIllegalArgumentException() { + + try { + new SecurityManagerProxy().setSecurityManager(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("SecurityManager must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalStateException.class) + public void getSecurityManagerWhenUninitializedThrowsIllegalStateException() { + + try { + new SecurityManagerProxy().getSecurityManager(); + } + catch (IllegalStateException expected) { + + assertThat(expected).hasMessage("No SecurityManager configured"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void authenticateDelegatesToConfiguredSecurityManager() { + + Properties securityProperties = new Properties(); + + org.apache.geode.security.SecurityManager mockSecurityManager = + mock(org.apache.geode.security.SecurityManager.class); + + when(mockSecurityManager.authenticate(any(Properties.class))).thenReturn("TestUser"); + + SecurityManagerProxy securityManagerProxy = new SecurityManagerProxy(); + + securityManagerProxy.setSecurityManager(mockSecurityManager); + + assertThat(securityManagerProxy.getSecurityManager()).isEqualTo(mockSecurityManager); + assertThat(securityManagerProxy.authenticate(securityProperties)).isEqualTo("TestUser"); + + verify(mockSecurityManager, times(1)).authenticate(eq(securityProperties)); + } + + @Test + public void authorizeDelegatesToConfiguredSecurityManager() { + + Principal mockPrincipal = mock(Principal.class); + + ResourcePermission resourcePermission = + new ResourcePermission(ResourcePermission.Resource.DATA, ResourcePermission.Operation.READ); + + org.apache.geode.security.SecurityManager mockSecurityManager = + mock(org.apache.geode.security.SecurityManager.class); + + when(mockSecurityManager.authorize(any(Object.class), any(ResourcePermission.class))).thenReturn(true); + + SecurityManagerProxy securityManagerProxy = new SecurityManagerProxy(); + + securityManagerProxy.setSecurityManager(mockSecurityManager); + + assertThat(securityManagerProxy.getSecurityManager()).isEqualTo(mockSecurityManager); + assertThat(securityManagerProxy.authorize(mockPrincipal, resourcePermission)).isTrue(); + + verify(mockSecurityManager, times(1)) + .authorize(eq(mockPrincipal), eq(resourcePermission)); + } + + @Test + public void closeDelegatesToConfiguredSecurityManager() { + + org.apache.geode.security.SecurityManager mockSecurityManager = + mock(org.apache.geode.security.SecurityManager.class); + + when(mockSecurityManager.authorize(any(Object.class), any(ResourcePermission.class))).thenReturn(true); + + SecurityManagerProxy securityManagerProxy = new SecurityManagerProxy(); + + securityManagerProxy.setSecurityManager(mockSecurityManager); + + assertThat(securityManagerProxy.getSecurityManager()).isEqualTo(mockSecurityManager); + + securityManagerProxy.close(); + + verify(mockSecurityManager, times(1)).close(); + } +}