blob: 8d53438bb72eb471ec7345f05cd614e51bacff96 [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.qpid.client.security;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.qpid.util.FileUtils;
/**
* CallbackHandlerRegistry is a registry for call back handlers for user authentication and interaction during user
* authentication. It is capable of reading its configuration from a properties file containing call back handler
* implementing class names for different SASL mechanism names. Instantiating this registry also has the effect of
* configuring and registering the SASL client factory implementations using {@link DynamicSaslRegistrar}.
* <p>
* The callback configuration should be specified in a properties file, refered to by the System property
* "amp.callbackhandler.properties". The format of the properties file is:
* <p>
* <pre>
* CallbackHanlder.n.mechanism=fully.qualified.class.name where n is an ordinal
* </pre>
* <p>
* Where mechanism is an IANA-registered mechanism name and the fully qualified class name refers to a
* class that implements org.apache.qpid.client.security.AMQCallbackHanlder and provides a call back handler for the
* specified mechanism.
*/
public class CallbackHandlerRegistry
{
private static final Logger _logger = LoggerFactory.getLogger(CallbackHandlerRegistry.class);
/** The name of the system property that holds the name of the callback handler properties file. */
private static final String FILE_PROPERTY = "amq.callbackhandler.properties";
/** The default name of the callback handler properties resource. */
public static final String DEFAULT_RESOURCE_NAME = "org/apache/qpid/client/security/CallbackHandlerRegistry.properties";
/** A static reference to the singleton instance of this registry. */
private static final CallbackHandlerRegistry _instance;
/** Holds a map from SASL mechanism names to call back handlers. */
private Map<String, Class<AMQCallbackHandler>> _mechanismToHandlerClassMap = new HashMap<String, Class<AMQCallbackHandler>>();
/** Ordered collection of mechanisms for which callback handlers exist. */
private Collection<String> _mechanisms;
private static final Collection<String> MECHS_THAT_NEED_USERPASS = Arrays.asList(new String [] {"PLAIN", "AMQPLAIN", "CRAM-MD5","CRAM-MD5-HASHED", "SCRAM-SHA-1", "SCRAM-SHA-256"});
static
{
// Register any configured SASL client factories.
DynamicSaslRegistrar.registerSaslProviders();
String filename = System.getProperty(FILE_PROPERTY);
InputStream is =
FileUtils.openFileOrDefaultResource(filename, DEFAULT_RESOURCE_NAME,
CallbackHandlerRegistry.class.getClassLoader());
final Properties props = new Properties();
try
{
props.load(is);
}
catch (IOException e)
{
_logger.error("Error reading properties: " + e, e);
}
finally
{
if (is != null)
{
try
{
is.close();
}
catch (IOException e)
{
_logger.error("Unable to close properties stream: " + e, e);
}
}
}
_instance = new CallbackHandlerRegistry(props);
_logger.info("Callback handlers available for SASL mechanisms: " + _instance._mechanisms);
}
/**
* Gets the singleton instance of this registry.
*
* @return The singleton instance of this registry.
*/
public static CallbackHandlerRegistry getInstance()
{
return _instance;
}
public AMQCallbackHandler createCallbackHandler(final String mechanism)
{
final Class<AMQCallbackHandler> mechanismClass = _mechanismToHandlerClassMap.get(mechanism);
if (mechanismClass == null)
{
throw new IllegalArgumentException("Mechanism " + mechanism + " not known");
}
try
{
return mechanismClass.newInstance();
}
catch (InstantiationException e)
{
throw new IllegalArgumentException("Unable to create an instance of mechanism " + mechanism, e);
}
catch (IllegalAccessException e)
{
throw new IllegalArgumentException("Unable to create an instance of mechanism " + mechanism, e);
}
}
/**
* Gets collections of supported SASL mechanism names, ordered by preference
*
* @return collection of SASL mechanism names.
*/
public Collection<String> getMechanisms()
{
return Collections.unmodifiableCollection(_mechanisms);
}
/**
* Creates the call back handler registry from its configuration resource or file.
*
* This also has the side effect of configuring and registering the SASL client factory
* implementations using {@link DynamicSaslRegistrar}.
*
* This constructor is default protection to allow for effective unit testing. Clients must use
* {@link #getInstance()} to obtain the singleton instance.
*/
CallbackHandlerRegistry(final Properties props)
{
parseProperties(props);
}
/**
* Scans the specified properties as a mapping from IANA registered SASL mechanism to call back handler
* implementations, that provide the necessary call back handling for obtaining user log in credentials
* during authentication for the specified mechanism, and builds a map from mechanism names to handler
* classes.
*
* @param props
*/
private void parseProperties(Properties props)
{
final Map<Integer, String> mechanisms = new TreeMap<Integer, String>();
Enumeration e = props.propertyNames();
while (e.hasMoreElements())
{
final String propertyName = (String) e.nextElement();
final String[] parts = propertyName.split("\\.", 2);
checkPropertyNameFormat(propertyName, parts);
final String mechanism = parts[0];
final int ordinal = getPropertyOrdinal(propertyName, parts);
final String className = props.getProperty(propertyName);
Class clazz = null;
try
{
clazz = Class.forName(className);
if (!AMQCallbackHandler.class.isAssignableFrom(clazz))
{
_logger.warn("SASL provider " + clazz + " does not implement " + AMQCallbackHandler.class
+ ". Skipping");
continue;
}
_mechanismToHandlerClassMap.put(mechanism, clazz);
mechanisms.put(ordinal, mechanism);
}
catch (ClassNotFoundException ex)
{
_logger.warn("Unable to load class " + className + ". Skipping that SASL provider");
continue;
}
}
_mechanisms = mechanisms.values(); // order guaranteed by keys of treemap (i.e. our ordinals)
}
private void checkPropertyNameFormat(final String propertyName, final String[] parts)
{
if (parts.length != 2)
{
throw new IllegalArgumentException("Unable to parse property " + propertyName + " when configuring SASL providers");
}
}
private int getPropertyOrdinal(final String propertyName, final String[] parts)
{
try
{
return Integer.parseInt(parts[1]);
}
catch(NumberFormatException nfe)
{
throw new IllegalArgumentException("Unable to parse property " + propertyName + " when configuring SASL providers", nfe);
}
}
/**
* Selects a SASL mechanism that is mutually available to both parties. If more than one
* mechanism is mutually available the one appearing first (by ordinal) will be returned.
*
* @param peerMechanismList space separated list of mechanisms
* @return selected mechanism, or null if none available
*/
public String selectMechanism(final String peerMechanismList)
{
final Set<String> peerList = mechListToSet(peerMechanismList);
return selectMechInternal(peerList, Collections.<String>emptySet());
}
/**
* Selects a SASL mechanism that is mutually available to both parties.
*
* @param peerMechanismList space separated list of mechanisms
* @param restrictionList space separated list of mechanisms
* @return selected mechanism, or null if none available
*/
public String selectMechanism(final String peerMechanismList, final String restrictionList)
{
final Set<String> peerList = mechListToSet(peerMechanismList);
final Set<String> restrictionSet = mechListToSet(restrictionList);
return selectMechInternal(peerList, restrictionSet);
}
private String selectMechInternal(final Set<String> peerSet, final Set<String> restrictionSet)
{
for (final String mech : _mechanisms)
{
if (peerSet.contains(mech))
{
if (restrictionSet.isEmpty() || restrictionSet.contains(mech))
{
return mech;
}
}
}
return null;
}
private Set<String> mechListToSet(final String mechanismList)
{
if (mechanismList == null)
{
return Collections.emptySet();
}
final StringTokenizer tokenizer = new StringTokenizer(mechanismList, " ");
final Set<String> mechanismSet = new HashSet<String>(tokenizer.countTokens());
while (tokenizer.hasMoreTokens())
{
mechanismSet.add(tokenizer.nextToken());
}
return Collections.unmodifiableSet(mechanismSet);
}
public boolean isUserPassRequired(String selectedMech)
{
return MECHS_THAT_NEED_USERPASS.contains(selectedMech);
}
}