/* * Copyright 2016 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.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import java.io.IOException; import java.io.Serializable; import java.security.Principal; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.PostConstruct; import javax.annotation.Resource; import org.apache.geode.LogWriter; import org.apache.geode.cache.CacheLoader; import org.apache.geode.cache.CacheLoaderException; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.LoaderHelper; import org.apache.geode.cache.Region; import org.apache.geode.cache.client.ClientRegionShortcut; import org.apache.geode.cache.client.ServerOperationException; import org.apache.geode.distributed.DistributedMember; import org.apache.geode.security.AuthInitialize; import org.apache.geode.security.AuthenticationFailedException; import org.apache.geode.security.NotAuthorizedException; import org.apache.geode.security.ResourcePermission; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runners.MethodSorters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; import org.springframework.data.gemfire.LocalRegionFactoryBean; import org.springframework.data.gemfire.client.ClientRegionFactoryBean; import org.springframework.data.gemfire.process.ProcessWrapper; import org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport; import org.springframework.data.gemfire.util.CollectionUtils; import org.springframework.data.gemfire.util.PropertiesBuilder; import org.springframework.test.annotation.DirtiesContext; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; /** * Abstract base test class for implementing Apache Geode Integrated Security Integration Tests. * * @author John Blum * @see org.junit.FixMethodOrder * @see org.junit.Test * @see lombok * @see org.apache.geode.cache.GemFireCache * @see org.springframework.data.gemfire.test.support.ClientServerIntegrationTestsSupport * @since 1.0.0 */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) @SuppressWarnings("unused") public abstract class AbstractGeodeSecurityIntegrationTests extends ClientServerIntegrationTestsSupport { protected static final int CACHE_SERVER_PORT = 13531; protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractGeodeSecurityIntegrationTests.class); protected static final String CACHE_SERVER_HOST = "localhost"; protected static final String GEODE_SECURITY_PROFILE_PROPERTY = "geode.security.profile"; protected static final String SECURITY_PASSWORD_PROPERTY = "security-password"; protected static final String SECURITY_USERNAME_PROPERTY = "security-username"; private static final AtomicInteger RUN_COUNT = new AtomicInteger(0); private static ProcessWrapper geodeServerProcess; @BeforeClass public static void runGeodeServer() throws IOException { String geodeSecurityProfile = System.getProperty(GEODE_SECURITY_PROFILE_PROPERTY); if (StringUtils.hasText(geodeSecurityProfile)) { runGeodeServer(geodeSecurityProfile); } } protected static ProcessWrapper runGeodeServer(String geodeSecurityProfile) throws IOException { Assert.hasText(geodeSecurityProfile, String.format("The '%s' System property must be set", GEODE_SECURITY_PROFILE_PROPERTY)); geodeServerProcess = run(GeodeServerConfiguration.class, String.format("-Dgemfire.log-file=%s", logFile()), String.format("-Dgemfire.log-level=%s", logLevel(TEST_GEMFIRE_LOG_LEVEL)), String.format("-Dspring.profiles.active=apache-geode-server,%s", geodeSecurityProfile)); waitForServerToStart(CACHE_SERVER_HOST, CACHE_SERVER_PORT); return geodeServerProcess; } @AfterClass public static void stopGeodeServer() { System.clearProperty(GEODE_SECURITY_PROFILE_PROPERTY); stop(geodeServerProcess); } @Rule public ExpectedException exception = ExpectedException.none(); @Resource(name = "Echo") private Region<String, String> echo; @Test @DirtiesContext public void authorizedUser() { assertThat(echo.get("one")).isEqualTo("one"); assertThat(echo.put("two", "four")).isNull(); assertThat(echo.get("two")).isEqualTo("four"); } @Test public void unauthorizedUser() { assertThat(echo.get("one")).isEqualTo("one"); exception.expect(ServerOperationException.class); exception.expectCause(is(instanceOf(NotAuthorizedException.class))); exception.expectMessage(containsString("analyst not authorized for DATA:WRITE:Echo:two")); echo.put("two", "four"); } protected static abstract class AuthInitializeSupport implements AuthInitialize { /** * @inheritDoc */ @Override public void init(LogWriter systemLogger, LogWriter securityLogger) throws AuthenticationFailedException { } /** * @inheritDoc */ @Override public Properties getCredentials(Properties securityProperties, DistributedMember server, boolean isPeer) throws AuthenticationFailedException { return getCredentials(securityProperties); } /** * @inheritDoc */ @Override public void close() { } } public static class GeodeClientAuthInitialize extends AuthInitializeSupport { protected static final User ANALYST = User.newUser("analyst").with("p@55w0rd"); protected static final User SCIENTIST = User.newUser("scientist").with("w0rk!ng4u"); private final User user; /* (non-Javadoc) */ public static GeodeClientAuthInitialize create() { return new GeodeClientAuthInitialize(RUN_COUNT.incrementAndGet() < 2 ? SCIENTIST : ANALYST); } /* (non-Javadoc) */ public GeodeClientAuthInitialize(User user) { Assert.notNull(user, "User cannot be null"); this.user = user; } /** * @inheritDoc */ @Override public Properties getCredentials(Properties securityProperties) { User user = getUser(); return new PropertiesBuilder() .setProperty(SECURITY_USERNAME_PROPERTY, user.getName()) .setProperty(SECURITY_PASSWORD_PROPERTY, user.getCredentials()) .build(); } /* (non-Javadoc) */ protected User getUser() { return this.user; } /** * @inheritDoc */ @Override public String toString() { User user = getUser(); return String.format("%1$s:%2$s", user.getName(), user.getCredentials()); } } @ClientCacheApplication(name = "GeodeSecurityIntegrationTestsClient", logLevel = TEST_GEMFIRE_LOG_LEVEL, servers = { @ClientCacheApplication.Server(port = CACHE_SERVER_PORT) }) @EnableAuth(clientAuthenticationInitializer = "org.springframework.data.gemfire.config.annotation.AbstractGeodeSecurityIntegrationTests$GeodeClientAuthInitialize.create") @Profile("apache-geode-client") static class GeodeClientConfiguration { @Bean("Echo") ClientRegionFactoryBean<String, String> echoRegion(GemFireCache gemfireCache) { ClientRegionFactoryBean<String, String> echoRegion = new ClientRegionFactoryBean<>(); echoRegion.setCache(gemfireCache); echoRegion.setClose(false); echoRegion.setShortcut(ClientRegionShortcut.PROXY); return echoRegion; } } @CacheServerApplication(name = "GeodeSecurityIntegrationTestsServer", logLevel = TEST_GEMFIRE_LOG_LEVEL, port = CACHE_SERVER_PORT) @Import({ ApacheShiroIniGeodeSecurityIntegrationTests.ApacheShiroIniConfiguration.class, ApacheShiroRealmGeodeSecurityIntegrationTests.ApacheShiroRealmConfiguration.class, ApacheGeodeSecurityManagerGeodeSecurityIntegrationTests.ApacheGeodeSecurityManagerConfiguration.class }) @Profile("apache-geode-server") public static class GeodeServerConfiguration { public static void main(String[] args) { runSpringApplication(GeodeServerConfiguration.class, args); } @Autowired private GemFireCache gemfireCache; @Bean("Echo") LocalRegionFactoryBean<String, String> echoRegion(GemFireCache gemfireCache) { LocalRegionFactoryBean<String, String> echoRegion = new LocalRegionFactoryBean<>(); echoRegion.setCache(gemfireCache); echoRegion.setCacheLoader(echoCacheLoader()); echoRegion.setClose(false); echoRegion.setPersistent(false); return echoRegion; } CacheLoader<String, String> echoCacheLoader() { return new CacheLoader<String, String>() { @Override public String load(LoaderHelper<String, String> helper) throws CacheLoaderException { return helper.getKey(); } @Override public void close() { } }; } @PostConstruct public void postProcess() { if (LOGGER.isInfoEnabled()) { LOGGER.info("Geode Distributed System Properties [{}]", CollectionUtils.toString(gemfireCache.getDistributedSystem().getProperties())); } } } @Getter @EqualsAndHashCode(of = { "name", "credentials" }) @RequiredArgsConstructor(staticName = "newUser") @SuppressWarnings("all") public static class User implements Iterable<Role>, Principal, Serializable { private Set<Role> roles = new HashSet<>(); @NonNull private String name; private String credentials; /* (non-Javadoc) */ public boolean hasPermission(ResourcePermission permission) { for (Role role : this) { if (role.hasPermission(permission)) { return true; } } return false; } /* (non-Javadoc) */ public boolean hasRole(Role role) { return this.roles.contains(role); } /** * @inheritDoc */ @Override public Iterator<Role> iterator() { return Collections.unmodifiableSet(this.roles).iterator(); } /** * @inheritDoc */ @Override public String toString() { return getName(); } /* (non-Javadoc) */ public User with(String credentials) { this.credentials = credentials; return this; } /* (non-Javadoc) */ public User with(Role... roles) { Collections.addAll(this.roles, roles); return this; } } @Getter @EqualsAndHashCode(of = "name") @RequiredArgsConstructor(staticName = "newRole") @SuppressWarnings("unsed") public static class Role implements Iterable<ResourcePermission>, Serializable { @NonNull private String name; private Set<ResourcePermission> permissions = new HashSet<>(); /* (non-Javadoc) */ public boolean hasPermission(ResourcePermission permission) { for (ResourcePermission thisPermission : this) { if (thisPermission.implies(permission)) { return true; } } return false; } /** * @inheritDoc */ @Override public Iterator<ResourcePermission> iterator() { return Collections.unmodifiableSet(this.permissions).iterator(); } /** * @inheritDoc */ @Override public String toString() { return getName(); } /* (non-Javadoc) */ public Role with(ResourcePermission... permissions) { Collections.addAll(this.permissions, permissions); return this; } } }