/* * RHQ Management Platform * Copyright (C) 2005-2011 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.server.naming; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.event.EventContext; import javax.naming.event.EventDirContext; import javax.naming.ldap.LdapContext; import javax.naming.spi.InitialContextFactory; import javax.naming.spi.InitialContextFactoryBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.enterprise.server.AllowRhqServerInternalsAccessPermission; import org.rhq.enterprise.server.naming.context.AccessCheckingContextDecorator; import org.rhq.enterprise.server.naming.context.AccessCheckingContextDecoratorSetContext; import org.rhq.enterprise.server.naming.context.ContextDecorator; import org.rhq.enterprise.server.naming.context.URLPreferringContextDecoratorSetContext; import org.rhq.enterprise.server.naming.util.DecoratorPicker; /** * This initial context factory builder is installed early on during the RHQ server startup * and is later on used for obtaining the {@link Context}s for all JNDI lookups in the * RHQ server. * <p> * We use a custom initial context factory builder to prevent the potential malicious 3rd party * code (like CLI alert scripts) from supplying custom environment variables to {@link InitialContext} * that would modify the JNDI lookup to skip our security access checks. * <p> * By using a builder we effectively take control of the initial context creation process * and are free to ignore whatever the script is trying to supply. * <p> * This builder makes sure to install the RHQ server's security access checks to whatever * initial context that is configured by the standard environment variables * ({@link Context#INITIAL_CONTEXT_FACTORY}, etc.) * * @see AllowRhqServerInternalsAccessPermission * * @author Lukas Krejci */ public class AccessCheckingInitialContextFactoryBuilder implements InitialContextFactoryBuilder { private static final Log LOG = LogFactory.getLog(AccessCheckingInitialContextFactoryBuilder.class); /** * The list of JNDI name schemes that should be checked for security permissions * (in addition to the names with no scheme). * * @see AccessCheckingContextDecorator */ private static final String[] CHECKED_SCHEMES = { "java" }; private static final Set<Class<? extends Context>> SUPPORTED_CONTEXT_INTERFACES; static { SUPPORTED_CONTEXT_INTERFACES = new HashSet<Class<? extends Context>>(); SUPPORTED_CONTEXT_INTERFACES.add(Context.class); SUPPORTED_CONTEXT_INTERFACES.add(DirContext.class); SUPPORTED_CONTEXT_INTERFACES.add(EventContext.class); SUPPORTED_CONTEXT_INTERFACES.add(EventDirContext.class); SUPPORTED_CONTEXT_INTERFACES.add(LdapContext.class); } private static final Set<InetAddress> SERVER_BIND_IPS; static { SERVER_BIND_IPS = new HashSet<InetAddress>(); try { String bindingAddressString = System.getProperty("jboss.bind.address"); InetAddress bindingAddress = InetAddress.getByName(bindingAddressString); if (bindingAddress.isAnyLocalAddress()) { Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); while (ifaces.hasMoreElements()) { NetworkInterface iface = ifaces.nextElement(); SERVER_BIND_IPS.addAll(Collections.list(iface.getInetAddresses())); } } else { SERVER_BIND_IPS.add(bindingAddress); } } catch (SocketException e) { LOG.error("Could not obtain the list of local IPs", e); } catch (UnknownHostException e) { LOG.error("Failed to get the binding address of the RHQ server.", e); } } //TODO this most probably no longer applies because AS7 doesn't use JNP for JNDI private static final int JNP_PORT = Integer.parseInt(System.getProperty("rhq.server.startup.namingservice.port", "2099")); private static InitialContextFactory getJbossDefaultInitialContextFactory() throws NamingException { try { Class<?> cls = Class.forName("org.jboss.as.naming.InitialContextFactory"); return (InitialContextFactory) cls.newInstance(); } catch (Exception e) { NamingException ne = new NamingException( "Failed to obtain the default initial context factory from JBoss AS."); ne.initCause(e); throw ne; } } private enum FactoryType { ACCESS_CHECKING_URL_PREFERRING { @Override public InitialContextFactory wrap(InitialContextFactory factory) { ArrayList<DecoratorPicker<Context, ContextDecorator>> pickers = new ArrayList<DecoratorPicker<Context, ContextDecorator>>(); pickers.add(getURLPreferringDecoratorPicker()); pickers.add(getAccessCheckingDecoratorPicker()); return new DecoratingInitialContextFactory(factory, pickers); } }, URL_PREFERRING { @Override public InitialContextFactory wrap(InitialContextFactory factory) { ArrayList<DecoratorPicker<Context, ContextDecorator>> pickers = new ArrayList<DecoratorPicker<Context, ContextDecorator>>(); pickers.add(getURLPreferringDecoratorPicker()); return new DecoratingInitialContextFactory(factory, pickers); } }, ACCESS_CHECKING { @Override public InitialContextFactory wrap(InitialContextFactory factory) { ArrayList<DecoratorPicker<Context, ContextDecorator>> pickers = new ArrayList<DecoratorPicker<Context, ContextDecorator>>(); pickers.add(getAccessCheckingDecoratorPicker()); return new DecoratingInitialContextFactory(factory, pickers); } }, PASS_THROUGH { @Override public InitialContextFactory wrap(InitialContextFactory factory) { return factory; } }; public abstract InitialContextFactory wrap(InitialContextFactory factory); public static FactoryType detect(Hashtable<?, ?> environment, boolean pretendNoFactoryBuilder) { String providerUrl = (String) environment.get(Context.PROVIDER_URL); if (providerUrl == null) { return pretendNoFactoryBuilder ? ACCESS_CHECKING_URL_PREFERRING : ACCESS_CHECKING; } else { try { URI uri = new URI(providerUrl); InetAddress providerHost = InetAddress.getByName(uri.getHost()); //check if we are accessing the RHQ server through some remoting //interface. if (uri.getPort() == JNP_PORT && SERVER_BIND_IPS.contains(providerHost)) { return pretendNoFactoryBuilder ? ACCESS_CHECKING_URL_PREFERRING : ACCESS_CHECKING; } else { return pretendNoFactoryBuilder ? URL_PREFERRING : PASS_THROUGH; } } catch (URISyntaxException e) { if (LOG.isDebugEnabled()) { LOG.debug("The " + Context.PROVIDER_URL + " is not a valid URI. Falling back to using the access checking wrapper.", e); } return pretendNoFactoryBuilder ? ACCESS_CHECKING_URL_PREFERRING : ACCESS_CHECKING; } catch (UnknownHostException e) { //let the factory deal with the unknown host... //this most probably shouldn't be secured because localhost addresses //should be resolvable. if (LOG.isDebugEnabled()) { LOG.debug("The " + Context.PROVIDER_URL + " is not resolvable. Falling back to using the URL preferring wrapper.", e); } return pretendNoFactoryBuilder ? URL_PREFERRING : PASS_THROUGH; } } } private static DecoratorPicker<Context, ContextDecorator> getAccessCheckingDecoratorPicker() { DecoratorPicker<Context, ContextDecorator> ret = new DecoratorPicker<Context, ContextDecorator>(); ret.setContext(new AccessCheckingContextDecoratorSetContext(SUPPORTED_CONTEXT_INTERFACES, CHECKED_SCHEMES)); return ret; } private static DecoratorPicker<Context, ContextDecorator> getURLPreferringDecoratorPicker() { DecoratorPicker<Context, ContextDecorator> ret = new DecoratorPicker<Context, ContextDecorator>(); ret.setContext(new URLPreferringContextDecoratorSetContext(SUPPORTED_CONTEXT_INTERFACES)); return ret; } } private final EnumMap<FactoryType, InitialContextFactory> typeDefaults = new EnumMap<FactoryType, InitialContextFactory>( FactoryType.class); private final String defaultFactoryClassName; private final boolean pretendNoFactoryBuilder; /** * @param defaultFactory the default factory to use if none can be deduced from the environment. If null, an attempt * is made to obtain the default InitialContextFactory of JBoss AS (which may fail depending on the classloading * "situation"). * @param pretendNoFactoryBuilder true if the naming contexts should pretend as if there was no initial context * factory builder installed. This is to support environments as AS4, where there really was no builder initially * and the lookup relied on that fact. * * @throws NamingException */ public AccessCheckingInitialContextFactoryBuilder(InitialContextFactory defaultFactory, final boolean pretendNoFactoryBuilder) throws NamingException { if (defaultFactory == null) { defaultFactory = getJbossDefaultInitialContextFactory(); } defaultFactoryClassName = defaultFactory.getClass().getName(); for (FactoryType ft : FactoryType.values()) { typeDefaults.put(ft, ft.wrap(defaultFactory)); } this.pretendNoFactoryBuilder = pretendNoFactoryBuilder; this.defaultFactory = new InitialContextFactory() { public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException { return typeDefaults.get(FactoryType.detect(environment, pretendNoFactoryBuilder)).getInitialContext( environment); } }; } /** * This is the default initial context factory that is returned when no other is * configured using the environment variables. */ private final InitialContextFactory defaultFactory; /** * Create a InitialContext factory. If the environment does not override the factory class it will use the * default context factory. * * @param environment The environment * @return An initial context factory * @throws NamingException If an error occurs loading the factory class. */ public InitialContextFactory createInitialContextFactory(Hashtable<?, ?> environment) throws NamingException { final String factoryClassName = (String) environment.get(Context.INITIAL_CONTEXT_FACTORY); if (factoryClassName == null || factoryClassName.equals(defaultFactoryClassName)) { if (LOG.isDebugEnabled()) { LOG.debug("No " + Context.INITIAL_CONTEXT_FACTORY + " set. Using the default factory."); } return defaultFactory; } final ClassLoader classLoader = getContextClassLoader(); try { final Class<?> factoryClass = Class.forName(factoryClassName, true, classLoader); InitialContextFactory configuredFactory = (InitialContextFactory) factoryClass.newInstance(); return FactoryType.detect(environment, pretendNoFactoryBuilder).wrap(configuredFactory); } catch (Exception e) { NamingException ne = new NamingException("Failed instantiate InitialContextFactory " + factoryClassName + " from classloader " + classLoader); ne.initCause(e); throw ne; } } private ClassLoader getContextClassLoader() { return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { public ClassLoader run() { return Thread.currentThread().getContextClassLoader(); } }); } }