package org.jgroups.auth.sasl; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Properties; import java.util.Timer; import java.util.concurrent.TimeUnit; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.sasl.AuthorizeCallback; import javax.security.sasl.RealmCallback; import org.jgroups.logging.Log; import org.jgroups.logging.LogFactory; import org.jgroups.util.Util; /** * SimpleAuthorizingCallbackHandler. This class implements a simple callback handler which can be * used to configure cluster authentication for the JGroups transport. It is configured via system * properties. The following properties are available: * * <ul> * <li>sasl.credentials.properties - the path to a property file which contains principal/credential * mappings represented as principal=password</li> * <li>sasl.local.principal - the name of the principal that is used to identify the local node. It * must exist in the sasl.credentials.properties file</li> * <li>sasl.roles.properties - (optional) the path to a property file which contains principal/roles * mappings represented as principal=role1,role2,role3</li> * <li>sasl.role - (optional) if present, authorizes joining nodes only if their principal is * <li>sasl.realm - (optional) the name of the realm to use for the SASL mechanisms that require it * </li> * </ul> * * @author Tristan Tarrant * @since 3.6 */ public class SimpleAuthorizingCallbackHandler implements CallbackHandler { private static final Log log = LogFactory.getLog(SimpleAuthorizingCallbackHandler.class); private final Properties credentials; private final Properties roles; private final Timer timer; private final String localPrincipal; private final String role; private final String realm; public SimpleAuthorizingCallbackHandler() { this(SecurityActions.getSystemProperties()); } public SimpleAuthorizingCallbackHandler(Properties properties) { this.credentials = new Properties(); this.roles = new Properties(); localPrincipal = requireProperty(properties, "sasl.local.principal"); String credentialsFile = requireProperty(properties, "sasl.credentials.properties"); timer = new Timer(); File fCredentials = new File(credentialsFile); timer.scheduleAtFixedRate( new FileWatchTask(fCredentials, new PropertiesReloadFileObserver(fCredentials, credentials)), 0, TimeUnit.SECONDS.toMillis(10)); role = properties.getProperty("sasl.role"); String rolesFile = properties.getProperty("sasl.roles.properties"); if (role != null) { if (rolesFile == null) { throw new IllegalStateException( "To enable role authorization, both sasl.role and sasl.roles.properties system properties must be set"); } else { File fRoles = new File(rolesFile); timer.scheduleAtFixedRate( new FileWatchTask(fRoles, new PropertiesReloadFileObserver(fRoles, roles)), 0, TimeUnit.SECONDS.toMillis(10)); } } realm = properties.getProperty("sasl.realm"); } private static String requireProperty(Properties properties, String propertyName) { String value = properties.getProperty(propertyName); if (value == null) { throw new IllegalStateException("The required system property " + propertyName + " has not been set"); } else { return value; } } @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { List<Callback> responseCallbacks = new LinkedList<>(); String remotePrincipal = null; boolean remotePrincipalFound = false; for (Callback current : callbacks) { if (current instanceof AuthorizeCallback) { responseCallbacks.add(current); } else if (current instanceof NameCallback) { NameCallback nameCallback = (NameCallback) current; remotePrincipal = nameCallback.getDefaultName(); if (remotePrincipal != null) { // server remotePrincipalFound = credentials.containsKey(remotePrincipal); } else { // client, we need to respond responseCallbacks.add(current); } } else if (current instanceof PasswordCallback) { responseCallbacks.add(current); } else if (current instanceof RealmCallback) { String realmLocal = ((RealmCallback) current).getDefaultText(); if (realmLocal != null && !this.realm.equals(realmLocal)) { throw new IOException("Invalid realm " + realmLocal); } responseCallbacks.add(current); } else { throw new UnsupportedCallbackException(current); } } for (Callback current : responseCallbacks) { if (current instanceof NameCallback) { ((NameCallback) current).setName(localPrincipal); } else if (current instanceof AuthorizeCallback) { AuthorizeCallback acb = (AuthorizeCallback) current; String authenticationId = acb.getAuthenticationID(); String authorizationId = acb.getAuthorizationID(); acb.setAuthorized(authenticationId.equals(authorizationId)); if (role != null) { String principalRoleNames = roles.getProperty(acb.getAuthorizationID()); List<String> principalRoles = (List<String>) (principalRoleNames != null ? Arrays.asList(principalRoleNames.split("\\s*,\\s*")) : Collections.emptyList()); if (!principalRoles.contains(role)) { throw new IOException("Unauthorized user " + authorizationId); } } } else if (current instanceof PasswordCallback) { String password; if (remotePrincipal == null) { // client, send our password password = credentials.getProperty(localPrincipal); } else if (remotePrincipalFound) { // server, validate incoming password password = credentials.getProperty(remotePrincipal); } else { throw new IOException("Unauthorized user " + remotePrincipal); } ((PasswordCallback) current).setPassword(password.toCharArray()); } else if (current instanceof RealmCallback) { ((RealmCallback)current).setText(realm); } } } public static class PropertiesReloadFileObserver implements FileObserver { private final Properties properties; PropertiesReloadFileObserver(File file, Properties properties) { this.properties = properties; loadProperties(file); } private void loadProperties(File file) { FileInputStream fis = null; try { fis = new FileInputStream(file); properties.load(fis); } catch (IOException e) { log.error(Util.getMessage("AnErrorOccurredWhileLoadingPropertiesFrom") + file, e); } finally { Util.close(fis); } } @Override public void fileChanged(File file) { loadProperties(file); } } }