SpringSecurityConfigurationArtifactInstaller.java
/*
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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.opencastproject.kernel.security;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UserDirectoryService;
import org.opencastproject.userdirectory.api.UserReferenceProvider;
import org.opencastproject.util.OsgiUtil;
import org.apache.commons.io.FilenameUtils;
import org.apache.felix.fileinstall.ArtifactInstaller;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.FileSystemResource;
import org.springframework.security.config.SecurityNamespaceHandler;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
/**
* Registers a security filter, which delegates to the spring filter chain appropriate for the current request's
* organization. Organizational security configurations may be added to the security watch directory, and should be
* named <organization_id>.xml.
*/
@Component(
immediate = true,
service = ArtifactInstaller.class,
property = {
"service.description=Security Configuration Scanner"
}
)
public class SpringSecurityConfigurationArtifactInstaller implements ArtifactInstaller {
protected static final Logger logger = LoggerFactory.getLogger(SpringSecurityConfigurationArtifactInstaller.class);
/** This component's bundle context */
protected BundleContext bundleContext = null;
/** The security filter */
protected SecurityFilter securityFilter = null;
/** The security service reference for Spring beans */
protected SecurityService securityService = null;
/** The user directory reference for Spring beans */
protected UserDirectoryService userDirectory = null;
/** The user detail service reference for Spring beans */
protected UserDetailsService userDetailsService = null;
/** The user reference provider service reference for Spring beans */
protected UserReferenceProvider userReferenceProvider = null;
/** LTI 1.1. configuration services */
protected OAuthConsumerDetailsService oAuthConsumerDetailsService = null;
protected LtiLaunchAuthenticationHandler ltiLaunchAuthenticationHandler = null;
/** LDAP authorities populators, keyed by organization id and, inside each organization, keyed by pid/instanceId */
protected Map<String, Map<String, LdapAuthoritiesPopulator>> ldapAuthoritiesPopulators =
new HashMap<String, Map<String, LdapAuthoritiesPopulator>>();
/** List of expected LDAP instances for each organization */
private Map<String, Set<String>> expectedLdapInstances = new HashMap<String, Set<String>>();
/**
* List of security/ORG_ID.xml files that were waiting for LDAP authorities populator instances, keyed by organization
* ID
*/
private Map<String, File> pendingArtifactsToInstall = new HashMap<String, File>();
private static final String LDAP_AUTH_POPULATOR_BEAN_NAME_PREFIX = "ldapAuthoritiesPopulator_";
/** Spring application contexts */
protected Map<String, GenericApplicationContext> appContexts = null;
/**
* Configuration listing all expected LDAP authorities populator instances/pid for each organization.
* Example:
* ldap.instances.mh_default_org=ldap1,ldap2
* ldap.instances.dce=ldap3
* ldap.instances.manchester=ldap4
*/
private static final String LDAP_INSTANCES_PREFIX = "ldap.instances.";
/** OSGi DI. */
@Reference
public void setSecurityFilter(SecurityFilter securityFilter) {
logger.info("Set SecurityFilter");
this.securityFilter = securityFilter;
}
@Reference
void setSecurityService(SecurityService securityService) {
logger.info("Set SecurityService");
this.securityService = securityService;
}
@Reference
void setUserDirectory(UserDirectoryService userDirectory) {
logger.info("Set UserDirectoryService");
this.userDirectory = userDirectory;
}
@Reference
public void setUserDetailsService(UserDetailsService userDetailsService) {
logger.info("Set UserDetailsService");
this.userDetailsService = userDetailsService;
}
@Reference
public void setUserReferenceProvider(UserReferenceProvider userReferenceProvider) {
logger.info("Set UserReferenceProvider");
this.userReferenceProvider = userReferenceProvider;
}
@Reference
void setOAuthConsumerDetailsService(OAuthConsumerDetailsService oAuthConsumerDetailsService) {
logger.info("Set setOAuthConsumerDetailsService");
this.oAuthConsumerDetailsService = oAuthConsumerDetailsService;
}
@Reference
void setLtiLaunchAuthenticationHandler(LtiLaunchAuthenticationHandler ltiLaunchAuthenticationHandler) {
logger.info("Set LtiLaunchAuthenticationHandler");
this.ltiLaunchAuthenticationHandler = ltiLaunchAuthenticationHandler;
}
/* These are set as LdapAuthoritiesPopulator service properties when they are registered */
private static final String INSTANCE_ID_SERVICE_PROPERTY_KEY = "instanceId";
private static final String ORGANIZATION_ID_SERVICE_PROPERTY_KEY = "orgId";
@Reference(
cardinality = ReferenceCardinality.MULTIPLE, // 0..n
policy = ReferencePolicy.DYNAMIC, unbind = "unbindLdapAuthoritiesPopulator"
)
synchronized void bindLdapAuthoritiesPopulator(LdapAuthoritiesPopulator service, Map<String, Object> properties)
throws Exception {
String orgId = (String) properties.get(ORGANIZATION_ID_SERVICE_PROPERTY_KEY);
String instanceId = (String) properties.get(INSTANCE_ID_SERVICE_PROPERTY_KEY);
if (ldapAuthoritiesPopulators.get(orgId) == null) {
ldapAuthoritiesPopulators.put(orgId, new HashMap<String, LdapAuthoritiesPopulator>());
}
ldapAuthoritiesPopulators.get(orgId).put(instanceId, service);
// If all expected LDAP instances ready, process pending artifact if there is one
if (allExpectedAlreadyRegistered(orgId) && pendingArtifactsToInstall.containsKey(orgId)) {
createSecurityContext(pendingArtifactsToInstall.get(orgId));
// Remove from pending list
pendingArtifactsToInstall.remove(orgId);
}
}
synchronized void unbindLdapAuthoritiesPopulator(LdapAuthoritiesPopulator service, Map<String, Object> properties) {
String orgId = (String) properties.get(ORGANIZATION_ID_SERVICE_PROPERTY_KEY);
String instanceId = (String) properties.get(INSTANCE_ID_SERVICE_PROPERTY_KEY);
if (ldapAuthoritiesPopulators.get(orgId) != null) {
ldapAuthoritiesPopulators.get(orgId).remove(instanceId);
}
}
/**
* OSGI activation callback
*/
@Activate
protected void activate(ComponentContext cc) {
this.bundleContext = cc.getBundleContext();
this.appContexts = new HashMap<>();
Collections.list(cc.getProperties().keys()).stream().filter(s -> s.startsWith(LDAP_INSTANCES_PREFIX)).forEach(s -> {
String orgId = s.substring(LDAP_INSTANCES_PREFIX.length());
String instanceList = OsgiUtil.getComponentContextProperty(cc, s);
expectedLdapInstances.put(orgId, new HashSet(Arrays.asList(instanceList.split(","))));
});
expectedLdapInstances.forEach((orgId, instances) -> logger.info(orgId + " = " + instances));
}
/**
* {@inheritDoc}
*
* @see org.apache.felix.fileinstall.ArtifactListener#canHandle(java.io.File)
*/
@Override
public boolean canHandle(File artifact) {
return "security".equals(artifact.getParentFile().getName()) && artifact.getName().endsWith(".xml");
}
/**
* {@inheritDoc}
*
* @see org.apache.felix.fileinstall.ArtifactInstaller#install(java.io.File)
*/
@Override
public synchronized void install(File artifact) throws Exception {
logger.trace("Install " + artifact.getAbsolutePath());
// Are there any expected LDAP authorities populators and have all already registered?
String orgId = FilenameUtils.getBaseName(artifact.getName());
if (allExpectedAlreadyRegistered(orgId)) {
createSecurityContext(artifact);
} else {
logger.warn("Not all LDAP authorities populators registered so artifact installation is pending.");
pendingArtifactsToInstall.put(orgId, artifact);
}
}
/**
* Returns true if all expected LDAP authorities populators have already registered.
*
* @param orgId
* The organization ID
* @return true if all registered
*/
private boolean allExpectedAlreadyRegistered(String orgId) {
Set<String> expected = expectedLdapInstances.get(orgId) != null
? expectedLdapInstances.get(orgId)
: new HashSet<String>();
Set<String> registered = ldapAuthoritiesPopulators.get(orgId) != null
? ldapAuthoritiesPopulators.get(orgId).keySet()
: new HashSet<String>();
return expected.size() == 0 || registered.containsAll(expected);
}
private void createSecurityContext(File artifact) throws Exception {
// If we already have a registration for this ID, take it out of the security filter and close it
String orgId = FilenameUtils.getBaseName(artifact.getName());
logger.info("Creating security context for organization {}", orgId);
GenericApplicationContext orgAppContext = appContexts.get(orgId);
if (orgAppContext != null) {
securityFilter.removeFilter(orgId);
orgAppContext.close();
}
orgAppContext = new GenericApplicationContext();
// this did NOT work orgAppContext.setClassLoader(SecurityNamespaceHandler.class.getClassLoader());
// When loading the beans, setting the spring application context class
// loader to this bundle's and explicitly importing the spring classes in the pom fixed
// the ClassNotFoundExceptions. TODO Is this the best way? Is there more to add?
orgAppContext.setClassLoader(this.getClass().getClassLoader());
// Manually add the OSGI dependencies
orgAppContext.getBeanFactory().registerSingleton("securityService", this.securityService);
orgAppContext.getBeanFactory().registerSingleton("userDirectoryService", this.userDirectory);
orgAppContext.getBeanFactory().registerSingleton("userDetailsService", this.userDetailsService);
orgAppContext.getBeanFactory().registerSingleton("userReferenceProvider", this.userReferenceProvider);
orgAppContext.getBeanFactory().registerSingleton("oAuthConsumerDetailsService", this.oAuthConsumerDetailsService);
orgAppContext.getBeanFactory().registerSingleton("ltiLaunchAuthenticationHandler",
this.ltiLaunchAuthenticationHandler);
if (ldapAuthoritiesPopulators.get(orgId) != null) {
for (Map.Entry<String, LdapAuthoritiesPopulator> entry : ldapAuthoritiesPopulators.get(orgId).entrySet()) {
logger.trace("Registering bean {}", LDAP_AUTH_POPULATOR_BEAN_NAME_PREFIX + entry.getKey());
orgAppContext.getBeanFactory().registerSingleton(LDAP_AUTH_POPULATOR_BEAN_NAME_PREFIX + entry.getKey(),
entry.getValue());
}
}
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(orgAppContext);
// So that it finds META-INF/spring.handlers
xmlBeanDefinitionReader.setNamespaceHandlerResolver(
new DefaultNamespaceHandlerResolver(SecurityNamespaceHandler.class.getClassLoader()));
FileSystemResource beanFile = new FileSystemResource(artifact);
xmlBeanDefinitionReader.loadBeanDefinitions(beanFile);
logger.info("registered {} items in {} for {} from file {}", orgAppContext.getBeanDefinitionCount(),
orgAppContext.getBeanDefinitionNames(), orgId, artifact.getAbsolutePath());
// Refresh the spring application context
try {
orgAppContext.refresh();
} catch (Exception e) {
logger.error("Unable to refresh spring security configuration file {}: {}", artifact, e);
orgAppContext.close();
return;
}
// Keep track of the app context so we can close it later
appContexts.put(orgId, orgAppContext);
// Add the filter chain for this org to the security filter
securityFilter.addFilter(orgId, (Filter) orgAppContext.getBean("springSecurityFilterChain"));
}
/**
* {@inheritDoc}
*
* @see org.apache.felix.fileinstall.ArtifactInstaller#uninstall(java.io.File)
*/
@Override
public void uninstall(File artifact) throws Exception {
String orgId = FilenameUtils.getBaseName(artifact.getName());
GenericApplicationContext appContext = appContexts.get(orgId);
if (appContext != null) {
securityFilter.removeFilter(orgId);
appContexts.remove(orgId);
appContext.close();
}
}
/**
* {@inheritDoc}
*
* @see org.apache.felix.fileinstall.ArtifactInstaller#update(java.io.File)
*/
@Override
public void update(File artifact) throws Exception {
install(artifact);
}
}