/*
* 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 java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.geode.cache.GemFireCache;
import org.apache.geode.internal.security.SecurityService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
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.core.OrderComparator;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* The {@link ApacheShiroSecurityConfiguration} class is a Spring {@link Configuration @Configuration} component
* responsible for configuring and initializing the Apache Shiro security framework in order to secure Apache Geode
* administrative and data access operations.
*
* @author John Blum
* @see org.apache.geode.cache.GemFireCache
* @see org.apache.geode.internal.security.SecurityService
* @see org.apache.shiro.mgt.DefaultSecurityManager
* @see org.apache.shiro.realm.Realm
* @see org.apache.shiro.spring.LifecycleBeanPostProcessor
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.beans.factory.ListableBeanFactory
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Condition
* @see org.springframework.context.annotation.Conditional
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.data.gemfire.config.annotation.ApacheShiroSecurityConfiguration.ApacheShiroPresentCondition
* @since 1.9.0
*/
@Configuration
@Conditional(ApacheShiroSecurityConfiguration.ApacheShiroPresentCondition.class)
@SuppressWarnings("unused")
public class ApacheShiroSecurityConfiguration implements BeanFactoryAware {
private ListableBeanFactory beanFactory;
/**
* @inheritDoc
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
Assert.isInstanceOf(ListableBeanFactory.class, beanFactory);
this.beanFactory = (ListableBeanFactory) beanFactory;
}
/**
* Returns a reference to the Spring {@link BeanFactory}.
*
* @return a reference to the Spring {@link BeanFactory}.
* @throws IllegalStateException if the Spring {@link BeanFactory} was not properly initialized.
* @see org.springframework.beans.factory.BeanFactory
*/
protected ListableBeanFactory getBeanFactory() {
Assert.state(this.beanFactory != null, "BeanFactory was not properly initialized");
return this.beanFactory;
}
/**
* {@link Bean} definition to define, configure and register an Apache Shiro Spring
* {@link LifecycleBeanPostProcessor} to automatically call lifecycle callback methods
* on Shiro security components during Spring container initialization and destruction phases.
*
* @return an instance of the Apache Shiro Spring {@link LifecycleBeanPostProcessor} to handle the lifecycle
* of Apache Shiro security framework components.
* @see org.apache.shiro.spring.LifecycleBeanPostProcessor
*/
@Bean
public BeanPostProcessor shiroLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* {@link Bean} definition to define, configure and register an Apache Shiro
* {@link org.apache.shiro.mgt.SecurityManager} implementation to secure Apache Geode.
*
* The registration of this {@link Bean} definition is dependent upon whether the user is using Apache Shiro
* to secure Apache Geode, which is determined by the presence of Apache Shiro {@link Realm Realms}
* declared in the Spring {@link org.springframework.context.ApplicationContext}.
*
* This {@link Bean} definition declares a dependency on the Apache Geode {@link GemFireCache} instance
* in order to ensure the Geode cache is created and initialized first. This ensures that any internal Geode
* security configuration logic is evaluated and processed before SDG attempts to configure Apache Shiro
* as Apache Geode's security provider.
*
* Additionally, this {@link Bean} definition will register the Apache Shiro
* {@link org.apache.geode.security.SecurityManager} with the Apache Shiro security framework
*
* Finally, this method proceeds to enable Apache Geode security.
* @return an Apache Shiro {@link org.apache.shiro.mgt.SecurityManager} implementation used to secure Apache Geode.
* @throws IllegalStateException if an Apache Shiro {@link org.apache.shiro.mgt.SecurityManager} was registered
* with the Apache Shiro security framework but Apache Geode security could not be enabled.
* @see org.apache.shiro.mgt.SecurityManager
* @see #registerSecurityManager(org.apache.shiro.mgt.SecurityManager)
* @see #enableApacheGeodeSecurity()
* @see #resolveRealms()
* @see #registerSecurityManager(org.apache.shiro.mgt.SecurityManager)
* @see #enableApacheGeodeSecurity()
*/
@Bean
public org.apache.shiro.mgt.SecurityManager shiroSecurityManager(GemFireCache gemfireCache) {
org.apache.shiro.mgt.SecurityManager shiroSecurityManager = null;
List<Realm> realms = resolveRealms();
if (!realms.isEmpty()) {
shiroSecurityManager = registerSecurityManager(new DefaultSecurityManager(realms));
if (!enableApacheGeodeSecurity()) {
throw new IllegalStateException("Failed to enable security services in Apache Geode");
}
}
return shiroSecurityManager;
}
/**
* Resolves all the Apache Shiro {@link Realm Realms} declared and configured as Spring managed beans
* in the Spring {@link org.springframework.context.ApplicationContext}.
*
* This method will order the Realms according to priority order to ensure that the Apache Shiro Realms
* are applied in the correct sequence, as declared/configured.
*
* @return a {@link List} of all Apache Shiro {@link Realm Realms} declared and configured as Spring managed beans
* in the Spring {@link org.springframework.context.ApplicationContext}.
* @see org.springframework.beans.factory.ListableBeanFactory#getBeansOfType(Class, boolean, boolean)
* @see org.springframework.core.OrderComparator
* @see org.apache.shiro.realm.Realm
*/
protected List<Realm> resolveRealms() {
try {
Map<String, Realm> realmBeans = getBeanFactory().getBeansOfType(Realm.class, false, true);
List<Realm> realms = new ArrayList<>(CollectionUtils.nullSafeMap(realmBeans).values());
Collections.sort(realms, OrderComparator.INSTANCE);
return realms;
}
catch (Exception ignore) {
return Collections.emptyList();
}
}
/**
* Registers the given Apache Shiro {@link org.apache.shiro.mgt.SecurityManager} with the Apache Shiro
* security framework.
*
* @param securityManager {@link org.apache.shiro.mgt.SecurityManager} to register.
* @return the given {@link org.apache.shiro.mgt.SecurityManager} reference.
* @throws IllegalArgumentException if {@link org.apache.shiro.mgt.SecurityManager} is {@literal null}.
* @see org.apache.shiro.SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager)
* @see org.apache.shiro.mgt.SecurityManager
*/
protected org.apache.shiro.mgt.SecurityManager registerSecurityManager(
org.apache.shiro.mgt.SecurityManager securityManager) {
Assert.notNull(securityManager, "The Apache Shiro SecurityManager to register must not be null");
SecurityUtils.setSecurityManager(securityManager);
return securityManager;
}
/**
* Sets the Apache Geode, Integrated Security {@link SecurityService} property {@literal isIntegratedSecurity}
* to {@literal true} to indicate that Apache Geode security is enabled.
*
* @return a boolean value indicating whether Apache Geode's Integrated Security framework services
* were successfully enabled.
* @see org.apache.geode.internal.security.SecurityService#getSecurityService()
*/
protected boolean enableApacheGeodeSecurity() {
SecurityService securityService = SecurityService.getSecurityService();
if (securityService != null) {
String isIntegratedSecurityFieldName = "isIntegratedSecurity";
Field isIntegratedSecurity = ReflectionUtils.findField(securityService.getClass(),
isIntegratedSecurityFieldName, Boolean.class);
isIntegratedSecurity = (isIntegratedSecurity != null ? isIntegratedSecurity
: ReflectionUtils.findField(securityService.getClass(), isIntegratedSecurityFieldName, Boolean.TYPE));
if (isIntegratedSecurity != null) {
ReflectionUtils.makeAccessible(isIntegratedSecurity);
ReflectionUtils.setField(isIntegratedSecurity, securityService, Boolean.TRUE);
return true;
}
}
return false;
}
/**
* A Spring {@link Condition} to determine whether the user has included (declared) the 'shiro-spring' dependency
* on their application's classpath, which is necessary for configuring Apache Shiro to secure Apache Geode
* in a Spring context.
*
* @see org.springframework.context.annotation.Condition
*/
public static class ApacheShiroPresentCondition implements Condition {
protected static final String APACHE_SHIRO_LIFECYCLE_BEAN_POST_PROCESSOR_CLASS_NAME =
"org.apache.shiro.spring.LifecycleBeanPostProcessor";
/**
* @inheritDoc
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return ClassUtils.isPresent(APACHE_SHIRO_LIFECYCLE_BEAN_POST_PROCESSOR_CLASS_NAME,
context.getClassLoader());
}
}
}