package org.dcache.gplazma;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Service;
import com.google.common.util.concurrent.ServiceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.Subject;
import java.lang.reflect.Modifier;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.dcache.auth.LoginNamePrincipal;
import org.dcache.auth.Origin;
import org.dcache.auth.PasswordCredential;
import org.dcache.util.NDC;
import org.dcache.gplazma.configuration.Configuration;
import org.dcache.gplazma.configuration.ConfigurationItem;
import org.dcache.gplazma.configuration.ConfigurationItemControl;
import org.dcache.gplazma.configuration.ConfigurationItemType;
import org.dcache.gplazma.configuration.ConfigurationLoadingStrategy;
import org.dcache.gplazma.configuration.parser.FactoryConfigurationException;
import org.dcache.gplazma.loader.PluginFactory;
import org.dcache.gplazma.loader.PluginLoader;
import org.dcache.gplazma.loader.PluginLoadingException;
import org.dcache.gplazma.loader.XmlResourcePluginLoader;
import org.dcache.gplazma.monitor.CombinedLoginMonitor;
import org.dcache.gplazma.monitor.LoggingLoginMonitor;
import org.dcache.gplazma.monitor.LoginMonitor;
import org.dcache.gplazma.monitor.LoginMonitor.Result;
import org.dcache.gplazma.monitor.LoginResult;
import org.dcache.gplazma.monitor.LoginResultPrinter;
import org.dcache.gplazma.monitor.RecordingLoginMonitor;
import org.dcache.gplazma.plugins.GPlazmaAccountPlugin;
import org.dcache.gplazma.plugins.GPlazmaAuthenticationPlugin;
import org.dcache.gplazma.plugins.GPlazmaIdentityPlugin;
import org.dcache.gplazma.plugins.GPlazmaMappingPlugin;
import org.dcache.gplazma.plugins.GPlazmaPlugin;
import org.dcache.gplazma.plugins.GPlazmaSessionPlugin;
import org.dcache.gplazma.strategies.AccountStrategy;
import org.dcache.gplazma.strategies.AuthenticationStrategy;
import org.dcache.gplazma.strategies.GPlazmaPluginService;
import org.dcache.gplazma.strategies.IdentityStrategy;
import org.dcache.gplazma.strategies.MappingStrategy;
import org.dcache.gplazma.strategies.SessionStrategy;
import org.dcache.gplazma.strategies.StrategyFactory;
import org.dcache.gplazma.validation.ValidationStrategy;
import org.dcache.gplazma.validation.ValidationStrategyFactory;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.instanceOf;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.Collections2.filter;
import static com.google.common.collect.Iterables.*;
public class GPlazma
{
private static final Logger LOGGER =
LoggerFactory.getLogger( GPlazma.class);
private static final LoginMonitor LOGGING_LOGIN_MONITOR =
new LoggingLoginMonitor();
private KnownFailedLogins _failedLogins = new KnownFailedLogins();
private Properties _globalProperties;
private boolean _globalPropertiesHaveUpdated;
private final PluginFactory _customPluginFactory;
private GPlazmaInternalException _lastLoadPluginsProblem;
private final ConfigurationLoadingStrategy configurationLoadingStrategy;
private ValidationStrategy validationStrategy;
private Setup setup;
/**
* Storage class for failed login attempts. This allows gPlazma to
* refrain from filling up log files should a client attempt multiple
* login attempts that all fail. We must be careful about how we store
* the incoming Subjects.
*
* This class is thread-safe.
*/
private static class KnownFailedLogins
{
private final Set<Subject> _failedLogins =
new CopyOnWriteArraySet<>();
/**
* In general, this class does not store any private credential since
* doing this would be against the general security advise of
* only storing sensitive material (e.g., passwords) for as long as
* is necessary.
*
* However, the class may wish to distinguish between different login
* attempts based information contained in private credentials. To
* support this, principals may be added that contain
* non-sensitive information contained in a private credential.
*/
private static void addPrincipalsForPrivateCredentials(
Set<Principal> principals, Set<Object> privateCredentials)
{
PasswordCredential password =
getFirst(Iterables.filter(privateCredentials,
PasswordCredential.class), null);
if(password != null) {
Principal loginName =
new LoginNamePrincipal(password.getUsername());
principals.add(loginName);
}
}
/**
* Calculate the storage Subject, given an incoming subject. The
* storage subject is similar to the supplied Subject but has sensitive
* material (like passwords) removed and is location agnostic
* (e.g., any Origin principals are removed).
*/
private static Subject storageSubjectFor(Subject subject)
{
Subject storage = new Subject();
storage.getPublicCredentials().addAll(subject.getPublicCredentials());
/*
* Do not store any private credentials as doing so would be a
* security risk.
*/
Collection<Principal> allExceptOrigin =
filter(subject.getPrincipals(), not(instanceOf(Origin.class)));
storage.getPrincipals().addAll(allExceptOrigin);
addPrincipalsForPrivateCredentials(storage.getPrincipals(),
subject.getPrivateCredentials());
return storage;
}
private boolean has(Subject subject)
{
Subject storage = storageSubjectFor(subject);
return _failedLogins.contains(storage);
}
private void add(Subject subject)
{
Subject storage = storageSubjectFor(subject);
_failedLogins.add(storage);
}
private void remove(Subject subject)
{
Subject storage = storageSubjectFor(subject);
_failedLogins.remove(storage);
}
private void clear()
{
_failedLogins.clear();
}
}
/**
* @param configurationLoadingStrategy The strategy for loading the plugin configuration.
* @param properties General configuration for plugins
*/
public GPlazma(ConfigurationLoadingStrategy configurationLoadingStrategy,
Properties properties)
{
this(configurationLoadingStrategy, properties, null);
}
/**
* @param configurationLoadingStrategy The strategy for loading the plugin configuration.
* @param properties General configuration for plugins
* @param factory Custom PluginFactory to allow customisation of plugins
*/
public GPlazma(ConfigurationLoadingStrategy configurationLoadingStrategy,
Properties properties, PluginFactory factory)
{
this.configurationLoadingStrategy = configurationLoadingStrategy;
_globalProperties = properties;
_customPluginFactory = factory;
try {
reload();
} catch (GPlazmaInternalException e) {
/* Ignore this error. Subsequent attempts to use gPlazma will
* fail with the same error. gPlazma will try to rectify the
* problem if configuration file is edited.
*/
}
}
public void shutdown()
{
Setup setup = this.setup;
if (setup != null) {
setup.stop();
}
}
public LoginReply login(Subject subject) throws AuthenticationException
{
RecordingLoginMonitor record = new RecordingLoginMonitor();
LoginMonitor combined = CombinedLoginMonitor.of(record,
LOGGING_LOGIN_MONITOR);
try {
LoginReply reply = login(subject, combined);
_failedLogins.remove(subject);
return reply;
} catch(AuthenticationException e) {
if(!_failedLogins.has(subject)) {
_failedLogins.add(subject);
LoginResult result = record.getResult();
if(result.hasStarted()) {
LoginResultPrinter printer = new LoginResultPrinter(result);
LOGGER.warn("Login attempt failed; " +
"detailed explanation follows:\n{}",
printer.print());
} else {
LOGGER.warn("Login attempt failed: {}", e.getMessage());
}
}
throw e;
}
}
public LoginReply login(Subject subject, LoginMonitor monitor)
throws AuthenticationException
{
checkNotNull(subject, "subject is null");
Setup setup;
synchronized (configurationLoadingStrategy) {
try {
checkPluginConfig();
} catch(GPlazmaInternalException e) {
throw new AuthenticationException("internal gPlazma error: " + e.getMessage());
}
setup = this.setup;
}
Set<Principal> principals = new HashSet<>();
setup.doAuthPhase(monitor, subject, principals);
setup.doMapPhase(monitor, principals);
setup.doAccountPhase(monitor, principals);
Set<Object> attributes = setup.doSessionPhase(monitor, principals);
removeIf(principals, p -> !isPublic(p));
return buildReply(monitor, subject, principals, attributes);
}
private static boolean isPublic(Principal p)
{
return Modifier.isPublic(p.getClass().getModifiers());
}
public LoginReply buildReply(LoginMonitor monitor, Subject originalSubject,
Set<Principal> principals, Set<Object> attributes)
throws AuthenticationException
{
Set<Object> publicCredentials = originalSubject.getPublicCredentials();
Set<Object> privateCredentials = originalSubject.getPrivateCredentials();
LoginReply reply = new LoginReply();
Subject subject = new Subject(false, principals, publicCredentials,
privateCredentials);
reply.setSubject(subject);
reply.setSessionAttributes(attributes);
Result result = Result.FAIL;
String error = null;
NDC.push("VALIDATION");
try {
validationStrategy.validate(reply);
result = Result.SUCCESS;
} catch(AuthenticationException e) {
error = e.getMessage();
throw e;
} finally {
NDC.pop();
monitor.validationResult(result, error);
}
return reply;
}
public Principal map(Principal principal) throws NoSuchPrincipalException
{
try {
return getIdentityStrategy().map(principal);
} catch (GPlazmaInternalException e) {
throw new NoSuchPrincipalException("internal gPlazma error: " +
e.getMessage());
}
}
public Set<Principal> reverseMap(Principal principal)
throws NoSuchPrincipalException
{
try {
return getIdentityStrategy().reverseMap(principal);
} catch (GPlazmaInternalException e) {
throw new NoSuchPrincipalException("internal gPlazma error: " +
e.getMessage());
}
}
private IdentityStrategy getIdentityStrategy() throws GPlazmaInternalException
{
synchronized (configurationLoadingStrategy) {
checkPluginConfig();
return setup.identityStrategy;
}
}
private void reload() throws GPlazmaInternalException
{
LOGGER.debug("reloading plugins");
try {
validationStrategy = ValidationStrategyFactory.getInstance().newValidationStrategy();
Setup newSetup = buildSetup();
try {
newSetup.start();
} catch (GPlazmaInternalException e) {
newSetup.stop();
throw e;
}
Setup oldSetup = this.setup;
if (oldSetup != null) {
oldSetup.stop();
}
this.setup = newSetup;
if(isPreviousLoadPluginsProblematic()) {
/* FIXME: this should be logged at info level but we want it to
* appear in the log file. */
LOGGER.warn("gPlazma configuration successfully loaded");
_lastLoadPluginsProblem = null;
}
} catch(GPlazmaInternalException e) {
LOGGER.error(e.getMessage());
_lastLoadPluginsProblem = e;
throw e;
}
}
private Setup buildSetup() throws GPlazmaInternalException
{
PluginLoader pluginLoader = XmlResourcePluginLoader.newPluginLoader();
if (_customPluginFactory != null) {
pluginLoader.setPluginFactory(_customPluginFactory);
}
pluginLoader.init();
SetupBuilder setup = new SetupBuilder();
Configuration configuration = configurationLoadingStrategy.load();
List<ConfigurationItem> items = configuration.getConfigurationItemList();
for (ConfigurationItem item : items) {
String pluginName = item.getPluginName();
Properties pluginProperties = item.getPluginConfiguration();
Properties combinedProperties = new Properties(_globalProperties);
combinedProperties.putAll(pluginProperties);
GPlazmaPlugin plugin;
try {
plugin = pluginLoader.newPluginByName(pluginName, combinedProperties);
} catch (PluginLoadingException e) {
throw new PluginLoadingException("failed to create " + pluginName + ": " + e.getMessage(), e);
}
ConfigurationItemControl control = item.getControl();
ConfigurationItemType type = item.getType();
setup.add(type, plugin, pluginName, control);
}
return setup.build();
}
private void checkPluginConfig() throws GPlazmaInternalException
{
if (_globalPropertiesHaveUpdated || configurationLoadingStrategy.hasUpdated()) {
_globalPropertiesHaveUpdated = false;
_failedLogins.clear();
reload();
}
if(isPreviousLoadPluginsProblematic()) {
throw _lastLoadPluginsProblem;
}
}
private boolean isPreviousLoadPluginsProblematic()
{
return _lastLoadPluginsProblem != null;
}
/**
* Container for plugins of a particular type.
*/
private static class Plugins<T extends GPlazmaPlugin> extends ArrayList<GPlazmaPluginService<T>>
{
private static final long serialVersionUID = 3696582098049455967L;
void add(T plugin, String pluginName, ConfigurationItemControl control)
{
add(new GPlazmaPluginService<>(plugin, pluginName, control));
}
}
private static class SetupBuilder
{
private final Plugins<GPlazmaAuthenticationPlugin> authenticationPlugins = new Plugins<>();
private final Plugins<GPlazmaMappingPlugin> mappingPlugins = new Plugins<>();
private final Plugins<GPlazmaAccountPlugin> accountPlugins = new Plugins<>();
private final Plugins<GPlazmaSessionPlugin> sessionPlugins = new Plugins<>();
private final Plugins<GPlazmaIdentityPlugin> identityPlugins = new Plugins<>();
void add(ConfigurationItemType type, GPlazmaPlugin plugin, String pluginName, ConfigurationItemControl control)
throws PluginLoadingException
{
if (!type.getType().isAssignableFrom(plugin.getClass())) {
throw new PluginLoadingException("plugin " + pluginName + " (java class " +
plugin.getClass().getCanonicalName() +
") does not support being loaded as type " + type);
}
switch (type) {
case AUTHENTICATION:
authenticationPlugins.add((GPlazmaAuthenticationPlugin) plugin, pluginName, control);
break;
case MAPPING:
mappingPlugins.add((GPlazmaMappingPlugin) plugin, pluginName, control);
break;
case ACCOUNT:
accountPlugins.add((GPlazmaAccountPlugin) plugin, pluginName, control);
break;
case SESSION:
sessionPlugins.add((GPlazmaSessionPlugin) plugin, pluginName, control);
break;
case IDENTITY:
identityPlugins.add((GPlazmaIdentityPlugin) plugin, pluginName, control);
break;
default:
throw new PluginLoadingException("unknown plugin type " + type);
}
}
Setup build() throws GPlazmaInternalException
{
return new Setup(authenticationPlugins, mappingPlugins, accountPlugins, sessionPlugins, identityPlugins);
}
}
/**
* A particular gPlazma setup with plugins grouped into several phases.
*/
private static class Setup extends ServiceManager.Listener
{
private final AuthenticationStrategy authStrategy;
private final MappingStrategy mapStrategy;
private final AccountStrategy accountStrategy;
private final SessionStrategy sessionStrategy;
private final IdentityStrategy identityStrategy;
private final ServiceManager manager;
private Throwable failure;
Setup(Plugins<GPlazmaAuthenticationPlugin> authenticationPlugins,
Plugins<GPlazmaMappingPlugin> mappingPlugins, Plugins<GPlazmaAccountPlugin> accountPlugins,
Plugins<GPlazmaSessionPlugin> sessionPlugins, Plugins<GPlazmaIdentityPlugin> identityPlugins)
throws FactoryConfigurationException
{
StrategyFactory factory = StrategyFactory.getInstance();
authStrategy = factory.newAuthenticationStrategy();
mapStrategy = factory.newMappingStrategy();
accountStrategy = factory.newAccountStrategy();
sessionStrategy = factory.newSessionStrategy();
identityStrategy = factory.newIdentityStrategy();
authStrategy.setPlugins(authenticationPlugins);
mapStrategy.setPlugins(mappingPlugins);
accountStrategy.setPlugins(accountPlugins);
sessionStrategy.setPlugins(sessionPlugins);
identityStrategy.setPlugins(identityPlugins);
manager = new ServiceManager(
concat(authenticationPlugins, mappingPlugins, accountPlugins, sessionPlugins, identityPlugins));
manager.addListener(this);
}
@Override
public void failure(Service service)
{
failure = service.failureCause();
}
void start() throws GPlazmaInternalException
{
try {
manager.startAsync().awaitHealthy();
} catch (IllegalStateException e) {
if (failure != null) {
throw new PluginLoadingException(failure.getMessage(), failure);
}
throw new PluginLoadingException(e.getMessage(), e);
}
}
void stop()
{
manager.stopAsync().awaitStopped();
}
void doAuthPhase(LoginMonitor monitor, Subject subject, Set<Principal> principals)
throws AuthenticationException
{
Set<Object> publicCredentials = subject.getPublicCredentials();
Set<Object> privateCredentials = subject.getPrivateCredentials();
principals.addAll(subject.getPrincipals());
NDC.push("AUTH");
Result result = Result.FAIL;
try {
monitor.authBegins(publicCredentials, privateCredentials, principals);
authStrategy.authenticate(monitor, publicCredentials, privateCredentials, principals);
result = Result.SUCCESS;
} finally {
NDC.pop();
monitor.authEnds(principals, result);
}
}
void doMapPhase(LoginMonitor monitor, Set<Principal> principals)
throws AuthenticationException
{
NDC.push("MAP");
Result result = Result.FAIL;
try {
monitor.mapBegins(principals);
mapStrategy.map(monitor, principals);
result = Result.SUCCESS;
} finally {
NDC.pop();
monitor.mapEnds(principals, result);
}
}
void doAccountPhase(LoginMonitor monitor, Set<Principal> principals)
throws AuthenticationException
{
NDC.push("ACCOUNT");
Result result = Result.FAIL;
try {
monitor.accountBegins(principals);
accountStrategy.account(monitor, principals);
result = Result.SUCCESS;
} finally {
NDC.pop();
monitor.accountEnds(principals, result);
}
}
Set<Object> doSessionPhase(LoginMonitor monitor, Set<Principal> principals)
throws AuthenticationException
{
Set<Object> attributes = new HashSet<>();
NDC.push("SESSION");
Result result = Result.FAIL;
try {
monitor.sessionBegins(principals);
sessionStrategy.session(monitor, principals, attributes);
result = Result.SUCCESS;
} finally {
NDC.pop();
monitor.sessionEnds(principals, attributes, result);
}
return attributes;
}
}
}