usr/src/java/vpanels/client/org/opensolaris/os/vp/client/common/RadLoginManager.java
author Stephen Talley <stephen.talley@oracle.com>
Mon, 28 Mar 2011 10:53:34 -0400
changeset 685 767674b0a2fb
parent 677 fbc09f84f958
child 700 fe2885279e58
permissions -rw-r--r--
18094 s/StringBuffer/StringBuilder/g

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
 */

package org.opensolaris.os.vp.client.common;

import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.util.*;
import java.util.logging.*;
import javax.management.*;
import javax.management.remote.*;
import javax.swing.JOptionPane;
import org.opensolaris.os.adr.Stability;
import org.opensolaris.os.rad.*;
import org.opensolaris.os.rad.api.pam.*;
import org.opensolaris.os.rad.api.zoneadmin.*;
import org.opensolaris.os.rad.jmx.RadConnector;
import org.opensolaris.os.rad.jmx.IncompatibleVersionException;
import org.opensolaris.os.rad.jmx.RadJMX;
import org.opensolaris.os.vp.common.panel.MBeanUtil;
import org.opensolaris.os.vp.panel.common.*;
import org.opensolaris.os.vp.panel.common.action.*;
import org.opensolaris.os.vp.util.misc.*;
import org.opensolaris.os.vp.util.misc.finder.Finder;

public abstract class RadLoginManager {
    //
    // Inner classes
    //

    private static abstract class AuthPrompter {
	//
	// Instance data
	//

	private boolean acknowledged;

	//
	// AuthPrompter methods
	//

	/**
	 * _,__/|
	 *  `O.o'
	 * =(_x_)=
	 *    U
	 */
	protected void ack() {
	    acknowledged = true;
	}

	public boolean isAcknowledged() {
	    return acknowledged;
	}

	public abstract Block initiate(LoginRequest request,
	    AuthenticatorMXBean auth) throws ActionAbortedException,
	    ObjectException;

	public abstract void prompt(LoginRequest request,
	    List<LoginProperty> properties, boolean isFirst)
	    throws ActionAbortedException, ActionRegressedException;
    }

    private class UserPrompter extends AuthPrompter {
	@Override
	public Block initiate(LoginRequest request,
	    AuthenticatorMXBean auth) throws ActionAbortedException,
	    ObjectException {

	    setLoginStatus(request, Finder.getString("login.status.login",
		request.getUser().getValue()));

	    return auth.login(Locale.getDefault().getLanguage(),
		request.getUser().getValue());
	}

	@Override
	public void prompt(LoginRequest request, List<LoginProperty> properties,
	    boolean isFirst) throws ActionAbortedException,
	    ActionRegressedException {

	    try {
		promptForAuth(request, properties, false, true, isFirst);
	    } finally {
		ack();
		request.getHost().setErrored(false);
		request.getUser().setErrored(false);
	    }
	}
    }

    private class RolePrompter extends AuthPrompter {
	@Override
	public Block initiate(LoginRequest request,
	    AuthenticatorMXBean auth) throws ActionAbortedException,
	    ObjectException {

	    setLoginStatus(request, Finder.getString("login.status.assume",
		request.getRole().getValue()));

	    return auth.assume(Locale.getDefault().getLanguage(),
		request.getRole().getValue());
	}

	@Override
	public void prompt(LoginRequest request, List<LoginProperty> properties,
	    boolean isFirst) throws ActionAbortedException,
	    ActionRegressedException {

	    try {
		promptForAuth(request, properties, false, false, isFirst);
	    } finally {
		ack();
		request.getRole().setErrored(false);
	    }
	}
    }

    private class ZoneUserPrompter extends AuthPrompter {
	@Override
	public Block initiate(LoginRequest request,
	    AuthenticatorMXBean auth) throws ActionAbortedException,
	    ObjectException {

	    return auth.login(Locale.getDefault().getLanguage(),
		request.getZoneUser().getValue());
	}

	@Override
	public void prompt(LoginRequest request, List<LoginProperty> properties,
	    boolean isFirst) throws ActionAbortedException,
	    ActionRegressedException {

	    try {
		promptForAuth(request, properties, true, true, isFirst);
	    } finally {
		ack();
		request.getZone().setErrored(false);
		request.getZoneUser().setErrored(false);
	    }
	}
    }

    private class ZoneRolePrompter extends AuthPrompter {
	@Override
	public Block initiate(LoginRequest request,
	    AuthenticatorMXBean auth) throws ActionAbortedException,
	    ObjectException {

	    return auth.assume(Locale.getDefault().getLanguage(),
		request.getZoneRole().getValue());
	}

	@Override
	public void prompt(LoginRequest request, List<LoginProperty> properties,
	    boolean isFirst) throws ActionAbortedException,
	    ActionRegressedException {

	    try {
		promptForAuth(request, properties, true, false, isFirst);
	    } finally {
		ack();
		request.getZoneRole().setErrored(false);
	    }
	}
    }

    private static class LoginData {
	//
	// Instance data
	//

	private LinkedList<ConnectionInfo> depChain =
	    new LinkedList<ConnectionInfo>();

	private LinkedList<Boolean> acks = new LinkedList<Boolean>();

	//
	// LoginData methods
	//

	public boolean isAcknowledged() {
	    return acks.peek();
	}

	public List<ConnectionInfo> getDepChain() {
	    return depChain;
	}

	public ConnectionInfo peek(int offset) {
	    return depChain.get(offset);
	}

	public void pop() {
	    depChain.pop();
	    acks.pop();
	}

	public void push(ConnectionInfo info, boolean acknowledged) {
	    depChain.push(info);
	    acks.push(acknowledged);
	}

	public void setDepChain(List<ConnectionInfo> depChain,
	    boolean acknowledged) {

	    assert compatible(depChain);

	    this.depChain.clear();
	    acks.clear();

	    for (int i = depChain.size() - 1; i >= 0; i--) {
		push(depChain.get(i), acknowledged);
	    }
	}

	//
	// Object methods
	//

	@Override
	public String toString() {
	    StringBuilder buffer = new StringBuilder();
	    for (int i = depChain.size() - 1; i >= 0; i--) {
		buffer.append(String.format("%d. %s (%s)\n", i,
		    depChain.get(i), acks.get(i) ? "acknowledged" :
		    "not acknowledged"));
	    }
	    return buffer.toString();
	}

	//
	// Private methods
	//

	private void clearConnectionsTo(int level) {
	    int size = depChain.size();
	    for (int i = size - 1; i >= level; i--) {
		depChain.remove(i);
	    }
	}

	private boolean compatible(List<ConnectionInfo> depChain) {
	    boolean compatible = false;
	    if (depChain.size() == this.depChain.size() + 1) {
		compatible = true;
		for (int i = 0, n = this.depChain.size(); i < n; i++) {
		    if (!this.depChain.get(i).equals(depChain.get(i + 1))) {
			compatible = false;
			break;
		    }
		}
	    }
	    return compatible;
	}
    }

    //
    // Static data
    //

    public static final String TRUSTSTORE_PASSWORD = "trustpass";
    public static final String LOCAL_USER = System.getProperty("user.name");
    public static final String LOCAL_HOST = "localhost";

    //
    // Instance data
    //

    private ConnectionManager connManager;

    //
    // Constructors
    //

    public RadLoginManager(ConnectionManager connManager) {
	this.connManager = connManager;
    }

    //
    // RadLoginManager methods
    //

    /**
     * Creates an empty truststore file.
     */
    protected void createTrustStore(File truststore) throws KeyStoreException,
	IOException, NoSuchAlgorithmException, CertificateException {

	File truststoreDir = truststore.getParentFile();

	if (!truststoreDir.exists()) {
	    if (!truststoreDir.mkdirs()) {
		throw new IOException(
		    "could not create truststore directory: " +
		    truststoreDir.getAbsolutePath());
	    }
	}

	KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
	char[] password = getTrustStorePassword().toCharArray();

	// Create empty keystore
	keyStore.load(null, password);

	FileOutputStream fos = new FileOutputStream(truststore);
	keyStore.store(fos, password);
	fos.close();
    }

    /**
     * Guides the user through the login process, then returns a dependency
     * chain of {@link ConnectionInfo}s.  The first element of the chain is the
     * {@code ConnectionInfo} that satisfies the given request.  Each additional
     * {@code ConnectionInfo} is a dependency of the previous {@code
     * ConnectionInfo}.
     * <p/>
     * For example, a role-based connection ("root@nerd (via talley)", say)
     * would have an dependency on the user-based connection ("talley@nerd")
     * used to create it.  The returned chain would contain the role-based
     * connection just before the user-based connection.
     * <p/>
     * This chain of {@code ConnectionInfo} dependencies can be {@link
     * ConnectionManager#add added} to the {@code ConnectionManager} for
     * automatic dependency-based management.
     *
     * @param	    request
     *		    the {@link LoginRequest} encapsulating the preset values and
     *		    editability of each core {@link LoginProperty}
     *
     * @param	    current
     *		    if non-{@code null}, ensures that the user is aware of any
     *		    change in login, preventing the use of cached connections
     *		    without the user's knowledge
     *
     * @exception   ActionAbortedException
     *		    if the user cancels the operation
     *
     * @exception   ActionFailedException
     *		    if the given request fails
     */
    @SuppressWarnings({"fallthrough"})
    public List<ConnectionInfo> getConnectionInfo(LoginRequest request,
	LoginInfo current) throws ActionAbortedException, ActionFailedException
    {
	LoginData data = new LoginData();
	Boolean doZone;
	for (int step = 0; ; ) {
	    try {
		switch (step) {
		case 0:
		    gatherHostAndUserData(request, data);
		    step++;

		case 1:
		    gatherRoleData(request, data);
		    step++;

		case 2:
		    doZone = request.getZonePrompt().getValue();
		    if (doZone != null && doZone) {
			gatherZoneHostAndUserData(request, data);
		    }
		    step++;

		case 3:
		    doZone = request.getZonePrompt().getValue();
		    if (doZone != null && doZone) {
			gatherZoneRoleData(request, data);
		    }
		    step++;
		}
		break;
	    } catch (ActionRegressedException e) {
		step--;
	    }
	}

	List<ConnectionInfo> depChain = data.getDepChain();

	// To prevent rogue connections, if the chosen connection differs from
	// the current one, ensure that the user has acknowledged it at some
	// point in the authentication process.  If not, prompt for
	// acknowledgement now.
	if (!data.isAcknowledged() && !depChain.get(0).matches(current)) {
	    promptForAck(request);
	}

	return depChain;
    }

    public ConnectionManager getConnectionManager() {
	return connManager;
    }

    /**
     * Gets the truststore file.
     */
    public abstract File getTrustStoreFile();

    /**
     * Gets the truststore password.  This default implementation returns
     * "{@code trustpass}".
     */
    public String getTrustStorePassword() {
	return TRUSTSTORE_PASSWORD;
    }

    protected boolean handleCertFailure(String host, File truststore,
	Certificate certificate) throws ActionAbortedException,
	KeyStoreException, IOException, NoSuchAlgorithmException,
	CertificateException {

	KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
	char[] password = getTrustStorePassword().toCharArray();

	// Load truststore
	FileInputStream fis = new FileInputStream(truststore);
	keyStore.load(fis, password);
	fis.close();

	// Does the truststore already contain the certificate?
	String alias = keyStore.getCertificateAlias(certificate);
	if (alias != null) {
	    return false;
	}

	boolean acceptNeeded = true;

	if (NetUtil.isLocalAddress(host)) {
	    FileInputStream certFileIn = null;
	    try {
		File certFile = new File("/etc/rad/cert.pem");
		certFileIn = new FileInputStream(certFile);
		Certificate localCert = CertificateFactory.
		    getInstance("X.509").generateCertificate(certFileIn);

		if (localCert.equals(certificate)) {
		    acceptNeeded = false;
		}
	    } catch (Throwable ignore) {
	    } finally {
		IOUtil.closeIgnore(certFileIn);
	    }
	}

	if (acceptNeeded) {
	    // Display the certificate, prompt user to accept
	    promptForCertificate(host, certificate);
	}

	// Add certificate
	alias = ((X509Certificate)certificate).getIssuerDN().toString();
	KeyStore.Entry entry =
	    new KeyStore.TrustedCertificateEntry(certificate);
	keyStore.setEntry(alias, entry, null);

	FileOutputStream fos = new FileOutputStream(truststore);
	keyStore.store(fos, password);
	fos.close();

	return true;
    }

    /**
     * Prompt the user to acknowledge or reject the imminent completion of the
     * given request.
     *
     * @param	    request
     *		    the {@link LoginRequest} encapsulating the preset values of
     *		    each core {@link LoginProperty}
     *
     * @exception   ActionAbortedException
     *		    if the user cancels the operation
     */
    protected abstract void promptForAck(LoginRequest request)
	throws ActionAbortedException;

    /**
     * Prompts the user to enter data for each of the given {@link
     * LoginProperty}s required for user or role authentication by the server.
     *
     * @param	    request
     *		    the {@link LoginRequest} encapsulating the preset values and
     *		    editability of each core {@link LoginProperty}
     *
     * @param	    properties
     *		    a {@code List} of {@link LoginProperty}s from the server
     *		    ({@code LoginProperty<String>} or {@code
     *		    PasswordLoginProperty} only)
     *
     * @exception   ActionAbortedException
     *		    if the user cancels the operation
     *
     * @exception   ActionRegressedException
     *		    if the chooses to re-edit the host or user fields
     *
     * @param	    isZone
     *		    {@code true} if prompting for a zone user or role
     *		    authentication
     *
     * @param	    isUserAuth
     *		    {@code true} if for user authentication, {@code false} if
     *		    for role authentication
     *
     * @param	    isFirst
     *		    {@code true} if this is the first prompt in the
     *		    authentication conversation
     */
    protected abstract void promptForAuth(LoginRequest request,
	List<LoginProperty> properties, boolean isZone, boolean isUserAuth,
	boolean isFirst) throws ActionAbortedException,
	ActionRegressedException;

    /**
     * Display the given {@code Certificate} details and prompt for user
     * confirmation to add it to the truststore.
     *
     * @param	    host
     *		    the owner of the {@code Certificate}
     *
     * @param	    certificate
     *		    the {@code Certificate} to verify
     *
     * @exception   ActionAbortedException
     *		    if the user cancels the operation
     */
    protected abstract void promptForCertificate(String host,
	Certificate certificate) throws ActionAbortedException;

    /**
     * Prompts the user to acknowledge failure of the given request.
     *
     * @param	    request
     *		    the {@link LoginRequest} encapsulating the values and
     *		    error status of each core {@link LoginProperty}
     */
    protected abstract void promptForFailedRequest(LoginRequest request);

    /**
     * Prompt the user to enter host/user data, subject to the editability and
     * preset values of the host and user {@link LoginProperty}s of the given
     * request.
     *
     * @param	    request
     *		    the {@link LoginRequest} encapsulating the preset values and
     *		    editability of each core {@link LoginProperty}
     *
     * @exception   ActionAbortedException
     *		    if the user cancels the operation
     */
    protected abstract void promptForHostAndUser(LoginRequest request)
	throws ActionAbortedException;

    /**
     * Prompt the user to select a role, subject to the editability and preset
     * value of the role {@link LoginProperty} of the given request.
     *
     * @param	    request
     *		    the {@link LoginRequest} encapsulating the preset values and
     *		    editability of each core {@link LoginProperty}
     *
     * @param	    roles
     *		    a list of valid roles for the selected user
     *
     * @param	    isZone
     *		    {@code true} if prompting for a zone role
     *
     * @exception   ActionAbortedException
     *		    if the user cancels the operation
     *
     * @exception   ActionRegressedException
     *		    if the chooses to re-edit the host or user fields
     */
    protected abstract void promptForRole(LoginRequest request,
	List<String> roles, boolean isZone) throws ActionAbortedException,
	ActionRegressedException;

    /**
     * Prompt the user to enter zone/user data, subject to the editability and
     * preset values of the zone and user {@link LoginProperty}s of the given
     * request.
     *
     * @param	    request
     *		    the {@link LoginRequest} encapsulating the preset values and
     *		    editability of each core {@link LoginProperty}
     *
     * @param	    zones
     *		    a list of valid zones for the selected host
     *
     * @exception   ActionAbortedException
     *		    if the user cancels the operation
     *
     * @exception   ActionRegressedException
     *		    if the chooses to re-edit the host or user fields
     */
    protected abstract void promptForZoneAndUser(LoginRequest request,
	List<String> zones) throws ActionAbortedException,
	ActionRegressedException;

    /**
     * Set login status.
     *
     * @param	    request
     *		    the {@link LoginRequest} encapsulating the preset values of
     *		    each core {@link LoginProperty}
     *
     * @param	    status
     *		    the login status
     */
    protected abstract void setLoginStatus(LoginRequest request, String status);

    //
    // Private methods
    //

    @SuppressWarnings({"fallthrough"})
    private boolean authConverse(LoginRequest request, AuthenticatorMXBean auth,
	AuthPrompter prompter) throws ActionAbortedException,
	ActionRegressedException {

	List<DialogMessage> messages = request.getMessages();
	boolean isFirst = true;

	try {
	    Block answer = prompter.initiate(request, auth);

	    List<LoginProperty> properties =
		new LinkedList<LoginProperty>();

	    BlockType type;
	    while (true) {
		properties.clear();

		switch (type = answer.getType()) {
		case success:
		    // Display any lingering messages from the server
		    if (!messages.isEmpty()) {
			prompter.prompt(request, properties, isFirst);
			isFirst = false;
			messages.clear();
		    }

		    auth.complete();
		    return true;

		case error:
		    return false;

		default:
		    assert type == BlockType.conv;
		case conv:
		    for (Message m : answer.getMessages()) {
			String text = m.getMessage();
			switch (m.getStyle()) {
			case prompt_echo_off:
			    properties.add(new PasswordLoginProperty(
				text, new char[0], true));
			    break;

			case prompt_echo_on:
			    properties.add(new LoginProperty<String>(
				text, "", true));
			    break;

			case error_msg:
			    messages.add(new DialogMessage(text,
				JOptionPane.ERROR_MESSAGE));
			    break;

			case text_info:
			    messages.add(new DialogMessage(text,
				JOptionPane.INFORMATION_MESSAGE));
			    break;
			}
		    }

		    if (!properties.isEmpty()) {
			prompter.prompt(request, properties, isFirst);
			isFirst = false;
			messages.clear();
		    }

		    List<char[]> response = new LinkedList<char[]>();
		    for (LoginProperty property : properties) {
			Object value = property.getValue();
			if (value != null) {
			    response.add(value instanceof char[] ?
				(char[])value : ((String)value).toCharArray());
			}
		    }

		    answer = auth.submit(response);
		    // clear out passwords
		    for (char[] res : response) {
			Arrays.fill(res, (char)0);
			res = null;
		    }
		}
	    }
	} catch (ObjectException e) {
	    messages.add(new DialogMessage(
		Finder.getString("login.err.io",
		request.getHost().getValue()),
		JOptionPane.ERROR_MESSAGE));
	    return false;
	}
    }

    private <T> T createMXBeanProxy(LoginRequest request, ConnectionInfo info,
	Class<T> ifaceClass, Stability s, String domain, String name)
	throws ActionFailedException {

	MBeanServerConnection mbsc = getMBeanServerConnection(request, info);
	if (mbsc == null) {
	    return null;
	}

	ObjectName oName = MBeanUtil.makeObjectName(domain, name);

	try {
	    return ifaceClass.cast(RadJMX.newMXBeanProxy(mbsc, oName,
		ifaceClass, s));
	} catch (IncompatibleVersionException e) {
	    List<DialogMessage> messages = request.getMessages();
	    messages.add(new DialogMessage(Finder.getString(
		"proxy.error.version", e.getClientVersion(),
		e.getServerVersion(), ifaceClass.getSimpleName()),
		JOptionPane.ERROR_MESSAGE));
	    requestFailed(request);
	} catch (JMException e) {
	    List<DialogMessage> messages = request.getMessages();
	    messages.add(new DialogMessage(Finder.getString(
		"proxy.error.general", oName),
		JOptionPane.ERROR_MESSAGE));
	    requestFailed(request);
	} catch (IOException e) {
	    List<DialogMessage> messages = request.getMessages();
	    messages.add(new DialogMessage(Finder.getString(
		"proxy.error.io", ifaceClass.getSimpleName()),
		JOptionPane.ERROR_MESSAGE));
	    requestFailed(request);
	}
	return null;
    }

    private AuthenticatorMXBean createAuthBean(LoginRequest request,
	ConnectionInfo info) throws ActionFailedException {
	return createMXBeanProxy(request, info, AuthenticatorMXBean.class,
	    Stability.PRIVATE, "org.opensolaris.os.rad", "authentication");
    }

    private JMXConnector createConnector(String host)
	throws KeyStoreException, NoSuchAlgorithmException,
	CertificateException, MalformedURLException, IOException,
	ActionAbortedException {

	if (NetUtil.isLocalAddress(host)) {
	    String[] paths = {
		"/var/run/radsocket",
		"/var/run/radsocket-unauth"
	    };

	    for (String path : paths) {
		JMXServiceURL url = null;
		try {
		    url = new JMXServiceURL(RadConnector.PROTOCOL_UDS, "", 0,
			path);
		    return JMXConnectorFactory.connect(url);
		} catch (IOException e) {
		    // Not necessarily an error
		    Logger.getLogger(getClass().getName()).log(Level.CONFIG,
			"unable to utilize local uds connector: " +
			(url == null ? path : url), e);
		}
	    }
	}

	File truststore = getTrustStoreFile();
	if (!truststore.exists()) {
	    createTrustStore(truststore);
	}

	Map<String, Object> env = new HashMap<String, Object>();
	env.put(RadConnector.KEY_TLS_TRUSTSTORE,
	    truststore.getAbsolutePath());
	env.put(RadConnector.KEY_TLS_TRUSTPASS,
	    getTrustStorePassword());

	JMXServiceURL url = new JMXServiceURL(
	    RadConnector.PROTOCOL_TLS, host, 0);

	// Throws MalformedURLException
	JMXConnector connector = JMXConnectorFactory.newJMXConnector(url, null);

	for (;;) {
	    RadTrustManager mtm = new RadTrustManager();
	    env.put(RadConnector.KEY_TLS_RADMANAGER, mtm);

	    try {
		connector.connect(env);
		break;
	    } catch (IOException e) {
		X509Certificate[] chain = mtm.getBadChain();
		if (chain == null) {
		    throw e;
		}

		if (!handleCertFailure(host, truststore, chain[0])) {
		    throw e;
		}
	    }
	}

	return connector;
    }

    private JMXConnector createConnector(LoginRequest request)
	throws ActionAbortedException {

	JMXConnector connector = null;
	LoginProperty<String> host = request.getHost();
	String hostVal = host.getValue();
	List<DialogMessage> messages = request.getMessages();
	boolean success = false;

	try {
	    setLoginStatus(request, Finder.getString("login.status.host",
		hostVal));

	    connector = createConnector(hostVal);
	    success = true;

	// Thrown by JMXConnector.connect
	} catch (UnknownHostException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.host.unknown", hostVal), JOptionPane.ERROR_MESSAGE));

	// Thrown by JMXConnector.connect
	} catch (ConnectException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.host.refused", hostVal), JOptionPane.ERROR_MESSAGE));

	// Thrown by JMXConnector.connect
	} catch (SecurityException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.security", hostVal),
		JOptionPane.ERROR_MESSAGE));

	// Thrown by createTrustStore
	} catch (KeyStoreException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.keystore", e.getMessage()),
		JOptionPane.ERROR_MESSAGE));

	// Thrown by createTrustStore
	} catch (NoSuchAlgorithmException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.keystore", e.getMessage()),
		JOptionPane.ERROR_MESSAGE));

	// Thrown by getDaemonCertificateChain
	} catch (CertificateException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.nocerts", hostVal),
		JOptionPane.ERROR_MESSAGE));

	// Thrown by new JMXServiceURL
	} catch (MalformedURLException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.url.invalid"),
		JOptionPane.ERROR_MESSAGE));

	// Thrown by JMXConnector.connect et al
	} catch (IOException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.io", hostVal),
		JOptionPane.ERROR_MESSAGE));

	} finally {
	    if (!success) {
		host.setErrored(true);
	    }
	}

	return connector;
    }

    private ZoneAdminMXBean createZoneAdminBean(LoginRequest request,
	ConnectionInfo info) throws ActionFailedException {
	return createMXBeanProxy(request, info, ZoneAdminMXBean.class,
	    Stability.PRIVATE, "org.opensolaris.os.rad.api.zoneadmin",
	    "ZoneAdmin");
    }


    private JMXConnector createZoneConnector(LoginRequest request,
	ZoneAdminMXBean bean) {

	JMXConnector connector = null;
	LoginProperty<String> zone = request.getZone();
	String zoneVal = zone.getValue();
	String zoneUserVal = request.getZoneUser().getValue();
	List<DialogMessage> messages = request.getMessages();
	boolean success = false;

	try {
	    JMXServiceURL url = new JMXServiceURL(
		RadConnector.PROTOCOL_ZONEADMIN, zoneVal, 0, "/" + zoneUserVal);

	    Map<String, Object> env = new HashMap<String, Object>();
	    env.put(RadConnector.KEY_ZONEADMIN_MXBEAN, bean);

	    connector = JMXConnectorFactory.connect(url, env);
	    success = true;

	// Thrown by JMXConnector.connect
	} catch (SecurityException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.zone.security", zoneVal, zoneUserVal),
		JOptionPane.ERROR_MESSAGE));

	// Thrown by new JMXServiceURL
	} catch (MalformedURLException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.url.invalid"), JOptionPane.ERROR_MESSAGE));

	// Thrown by JMXConnector.connect et al
	} catch (IOException e) {
	    messages.add(new DialogMessage(Finder.getString(
		"login.err.io", zoneVal), JOptionPane.ERROR_MESSAGE));

	} finally {
	    if (!success) {
		zone.setErrored(true);
	    }
	}

	return connector;
    }

    private void gatherHostAndUserData(LoginRequest request, LoginData data)
	throws ActionAbortedException, ActionFailedException {

	LoginProperty<String> host = request.getHost();
	LoginProperty<String> user = request.getUser();
	LoginProperty<Boolean> zonePrompt = request.getZonePrompt();
	List<DialogMessage> messages = request.getMessages();

	// Validate any preset values prior to prompting user
	if (host.getValue() != null || !host.isEditable()) {
	    isHostValid(request);
	}
	if (user.getValue() != null || !user.isEditable()) {
	    isUserValid(request);
	}
	if (zonePrompt.getValue() != null || !zonePrompt.isEditable()) {
	    isPropertyValid(zonePrompt, request);
	}

	// Loop until connected to host and authenticated as user
	while (true) {
	    boolean acknowledged = false;

	    // Refresh each iteration in case a prop isEditableOnError()
	    boolean hostEditable = host.isEditable();
	    boolean userEditable = user.isEditable();
	    boolean zonePromptEditable = zonePrompt.isEditable();

	    // If an error cannot be fixed by the user...
	    if ((!zonePromptEditable && zonePrompt.isErrored()) ||
		(!hostEditable && (host.isErrored() ||
		(!userEditable && user.isErrored())))) {

		requestFailed(request);
	    }

	    if (hostEditable || userEditable || zonePromptEditable ||
		!messages.isEmpty()) {

		try {
		    promptForHostAndUser(request);
		} finally {
		    acknowledged = true;

		    // User only needs to see any message once, presumably
		    messages.clear();

		    host.setErrored(false);
		    user.setErrored(false);
		    zonePrompt.setErrored(false);
		}

		if (!isHostValid(request) || !isUserValid(request) ||
		    !isPropertyValid(zonePrompt, request)) {
		    continue;
		}
	    }

	    String hostVal = host.getValue();
	    String userVal = user.getValue();

	    // Search for existing connection
	    List<ConnectionInfo> depChain =
		getConnectionManager().getDepChain(hostVal, userVal, null, null,
		null, null);
	    if (depChain != null) {
		data.setDepChain(depChain, acknowledged);
		return;
	    }

	    // Create connection, append to messages on error
	    JMXConnector connector = createConnector(request);
	    if (connector != null) {
		ConnectionInfo info = new ConnectionInfo(hostVal, userVal, null,
		    connector);

		// Get/create auth bean, append to messages on error
		AuthenticatorMXBean auth = createAuthBean(request, info);
		if (auth != null) {
		    setLoginStatus(request,
			Finder.getString("login.status.user"));

		    if (userVal.equals(auth.getuser())) {
			data.push(info, acknowledged);
			return;
		    }

		    try {
			AuthPrompter prompter = new UserPrompter();
			do {
			    if (authConverse(request, auth, prompter)) {
				acknowledged |= prompter.isAcknowledged();
				data.push(info, acknowledged);
				return;
			    }

			    // Authentication failed
			    user.setErrored(true);

			    // Add generic auth failure message if not already
			    // provided by server
			    if (messages.isEmpty()) {
				messages.add(new DialogMessage(
				    Finder.getString("login.err.user.auth",
				    hostVal, userVal),
				    JOptionPane.ERROR_MESSAGE));
			    }

			// No chance to edit, so keep iterating here
			} while (!host.isEditable() && !user.isEditable());

		    // Thrown by authConverse
		    } catch (ActionRegressedException e) {
		    }
		}
	    }
	}
    }

    private void gatherRoleData(LoginRequest request, LoginData data)
	throws ActionAbortedException, ActionFailedException,
	ActionRegressedException {

	LoginProperty<String> host = request.getHost();
	String hostVal = host.getValue();

	LoginProperty<String> user = request.getUser();
	String userVal = user.getValue();

	LoginProperty<String> zone = request.getZone();
	String zoneVal = zone.getValue();

	LoginProperty<String> zoneUser = request.getZoneUser();
	String zoneUserVal = zoneUser.getValue();

	// Get/create auth bean, append to messages on error
	AuthenticatorMXBean userAuth = createAuthBean(request, data.peek(0));
	if (userAuth == null) {
	    // Not likely, but handle it anyway
	    requestFailed(request);
	}

	LoginProperty<String> role = request.getRole();
	List<DialogMessage> messages = request.getMessages();

	setLoginStatus(request, Finder.getString("login.status.roles",
	    request.getUser().getValue()));
	List<String> roles = userAuth.getroles();

	// Validate any preset value prior to prompting user
	if (role.getValue() != null || !role.isEditable()) {
	    isRoleValid(request, roles, false);
	}

	// Loop until no role is chosen, or chosen role is authenticated
	while (true) {
	    boolean acknowledged = false;

	    // Refresh each iteration in case role.isEditableOnError()
	    boolean roleEditable = role.isEditable();

	    // If an error cannot be fixed by the user...
	    if (!roleEditable && role.isErrored()) {
		requestFailed(request);
	    }

	    if ((roleEditable && !roles.isEmpty()) || !messages.isEmpty()) {
		try {
		    promptForRole(request, roles, false);
		} finally {
		    acknowledged = true;

		    // User only needs to see any message once, presumably
		    messages.clear();

		    role.setErrored(false);
		}

		if (!isRoleValid(request, roles, false)) {
		    continue;
		}
	    }

	    String roleVal = role.getValue();
	    if (roleVal == null) {
		// No need to keep going
		return;
	    }

	    // Search for existing connection
	    List<ConnectionInfo> depChain = getConnectionManager().getDepChain(
		hostVal, userVal, roleVal, null, null, null);
	    if (depChain != null) {
		data.setDepChain(depChain, acknowledged);
		return;
	    }

	    try {
		byte[] token = userAuth.createToken();

		// Create connection, append to messages on error
		JMXConnector connector = createConnector(request);
		if (connector != null) {
		    ConnectionInfo info = new ConnectionInfo(hostVal, userVal,
			roleVal, connector);

		    // Create auth bean, append to messages on error
		    AuthenticatorMXBean roleAuth = createAuthBean(request,
			info);
		    if (roleAuth != null) {
			roleAuth.redeemToken(userVal, token);

			AuthPrompter prompter = new RolePrompter();
			do {
			    if (authConverse(request, roleAuth, prompter)) {
				acknowledged |= prompter.isAcknowledged();
				data.push(info, acknowledged);
				return;
			    }

			    // Authentication failed
			    role.setErrored(true);

			    // Add generic auth failure message if not already
			    // provided by server
			    if (messages.isEmpty()) {
				messages.add(new DialogMessage(
				    Finder.getString("login.err.role.auth",
				    hostVal, userVal, roleVal),
				    JOptionPane.ERROR_MESSAGE));
			    }

			// No chance to edit role, so keep iterating here
			} while (!role.isEditable());
		    }
		}

	    // Thrown by createToken/redeemToken
	    } catch (ObjectException e) {
		messages.add(new DialogMessage(Finder.getString(
		    "login.err.io", hostVal), JOptionPane.ERROR_MESSAGE));

	    // Thrown by authConverse
	    } catch (ActionRegressedException e) {
	    }
	}
    }

    private void gatherZoneHostAndUserData(LoginRequest request, LoginData data)
	throws ActionAbortedException, ActionFailedException,
	ActionRegressedException {

	ZoneAdminMXBean zcon = createZoneAdminBean(request, data.peek(0));
	if (zcon == null) {
	    requestFailed(request);
	}

	LoginProperty<String> zone = request.getZone();
	LoginProperty<String> zoneUser = request.getZoneUser();
	List<DialogMessage> messages = request.getMessages();

	List<String> zones = null;
	try {
	    zones = zcon.getZones(ZoneState.RUNNING);
	} catch (ObjectException e) {
	    messages.add(new DialogMessage(Finder.getString(
                "login.err.io", request.getHost().getValue()),
                JOptionPane.ERROR_MESSAGE));
	    requestFailed(request);
	}

	// Validate any preset value prior to prompting user
	if (zone.getValue() != null || !zone.isEditable()) {
	    isZoneValid(request, zones);
	}
	if (zoneUser.getValue() != null || !zoneUser.isEditable()) {
	    isZoneUserValid(request);
	}

	// Loop until connected to zone and authenticated as zoneUser
	while (true) {
	    boolean acknowledged = false;

	    // Refresh each iteration in case zone/zoneUser.isEditableOnError()
	    boolean zoneEditable = zone.isEditable();
	    boolean zoneUserEditable = zoneUser.isEditable();

	    // If an error cannot be fixed by the user...
	    if (!zoneEditable && (zone.isErrored() ||
		(!zoneUserEditable && zoneUser.isErrored()))) {

		requestFailed(request);
	    }

	    if (zoneEditable || zoneUserEditable || !messages.isEmpty()) {
		try {
		    promptForZoneAndUser(request, zones);
		} finally {
		    acknowledged = true;

		    // User only needs to see any message once, presumably
		    messages.clear();

		    zone.setErrored(false);
		    zoneUser.setErrored(false);
		}

		if (!isZoneValid(request, zones) || !isZoneUserValid(request)) {
		    continue;
		}
	    }

	    String zoneVal = zone.getValue();
	    if (zoneVal == null) {
		// No need to keep going
		return;
	    }

	    String zoneUserVal = zoneUser.getValue();

	    // Search for existing connection
	    List<ConnectionInfo> depChain =
		getConnectionManager().getDepChain(
		request.getHost().getValue(), request.getUser().getValue(),
		request.getRole().getValue(), zoneVal, zoneUserVal, null);
	    if (depChain != null) {
		data.setDepChain(depChain, acknowledged);
		return;
	    }

	    JMXConnector connector = createZoneConnector(request, zcon);
	    if (connector != null) {
		ConnectionInfo info = new ConnectionInfo(
		    request.getHost().getValue(), request.getUser().getValue(),
		    request.getRole().getValue(), zoneVal, zoneUserVal, null,
		    connector);

		// Get/create auth bean, append to messages on error
		AuthenticatorMXBean auth = createAuthBean(request, info);
		if (auth != null) {
		    if (zoneUserVal.equals(auth.getuser())) {
			data.push(info, acknowledged);
			return;
		    }

		    try {
			AuthPrompter prompter = new ZoneUserPrompter();
			do {
			    if (authConverse(request, auth, prompter)) {
				acknowledged |= prompter.isAcknowledged();
				data.push(info, acknowledged);
				return;
			    }

			    // Authentication failed
			    zoneUser.setErrored(true);

			    // Add generic auth failure message if not already
			    // provided by server
			    if (messages.isEmpty()) {
				messages.add(new DialogMessage(
				    Finder.getString("login.err.user.auth",
				    zoneVal, zoneUserVal),
				    JOptionPane.ERROR_MESSAGE));
			    }

			// No chance to edit, so keep iterating here
			} while (!zone.isEditable() && !zoneUser.isEditable());

		    // Thrown by authConverse
		    } catch (ActionRegressedException e) {
		    }
		}
	    }
	}
    }

    private void gatherZoneRoleData(LoginRequest request, LoginData data)
	throws ActionAbortedException, ActionFailedException,
	ActionRegressedException {

	LoginProperty<String> host = request.getHost();
	String hostVal = host.getValue();

	LoginProperty<String> user = request.getUser();
	String userVal = user.getValue();

	LoginProperty<String> role = request.getRole();
	String roleVal = role.getValue();

	LoginProperty<String> zone = request.getZone();
	String zoneVal = zone.getValue();

	LoginProperty<String> zoneUser = request.getZoneUser();
	String zoneUserVal = zoneUser.getValue();

	// Get/create auth bean, append to messages on error
	AuthenticatorMXBean userAuth = createAuthBean(request, data.peek(0));
	if (userAuth == null) {
	    // Not likely, but handle it anyway
	    requestFailed(request);
	}

	LoginProperty<String> zoneRole = request.getZoneRole();
	List<DialogMessage> messages = request.getMessages();

	setLoginStatus(request, Finder.getString("login.status.roles",
	    request.getZoneUser().getValue()));
	List<String> roles = userAuth.getroles();

	// Validate any preset value prior to prompting user
	if (zoneRole.getValue() != null || !zoneRole.isEditable()) {
	    isRoleValid(request, roles, true);
	}

	ZoneAdminMXBean zcon = null;

	// Loop until no role is chosen, or chosen role is authenticated
	while (true) {
	    boolean acknowledged = false;

	    // Refresh each iteration in case zoneRole.isEditableOnError()
	    boolean zoneRoleEditable = zoneRole.isEditable();

	    // If an error cannot be fixed by the user...
	    if (!zoneRoleEditable && zoneRole.isErrored()) {
		requestFailed(request);
	    }

	    if ((zoneRoleEditable && !roles.isEmpty()) || !messages.isEmpty()) {
		try {
		    promptForRole(request, roles, true);
		} finally {
		    acknowledged = true;

		    // User only needs to see any message once, presumably
		    messages.clear();

		    zoneRole.setErrored(false);
		}

		if (!isRoleValid(request, roles, true)) {
		    continue;
		}
	    }

	    String zoneRoleVal = zoneRole.getValue();
	    if (zoneRoleVal == null) {
		// No need to keep going
		return;
	    }

	    // Search for existing connection
	    List<ConnectionInfo> depChain = getConnectionManager().getDepChain(
		hostVal, userVal, roleVal, zoneVal, zoneUserVal, zoneRoleVal);
	    if (depChain != null) {
		data.setDepChain(depChain, acknowledged);
		return;
	    }

	    try {
		byte[] token = userAuth.createToken();

		if (zcon == null) {
		    // Peek back to the host-based connection
		    zcon = createZoneAdminBean(request, data.peek(1));
		    if (zcon == null) {
			requestFailed(request);
		    }
		}

		// Create connection, append to messages on error
		JMXConnector connector = createZoneConnector(request, zcon);
		if (connector != null) {
		    ConnectionInfo info = new ConnectionInfo(hostVal, userVal,
			roleVal, zoneVal, zoneUserVal, zoneRoleVal, connector);

		    // Create auth bean, append to messages on error
		    AuthenticatorMXBean roleAuth = createAuthBean(request,
			info);
		    if (roleAuth != null) {
			roleAuth.redeemToken(userVal, token);

			AuthPrompter prompter = new ZoneRolePrompter();
			do {
			    if (authConverse(request, roleAuth, prompter)) {
				acknowledged |= prompter.isAcknowledged();
				data.push(info, acknowledged);
				return;
			    }

			    // Authentication failed
			    zoneRole.setErrored(true);

			    // Add generic auth failure message if not already
			    // provided by server
			    if (messages.isEmpty()) {
				messages.add(new DialogMessage(
				    Finder.getString("login.err.zonerole.auth",
				    hostVal, userVal, roleVal, zoneVal,
				    zoneUserVal, zoneRoleVal),
				    JOptionPane.ERROR_MESSAGE));
			    }

			// No chance to edit role, so keep iterating here
			} while (!zoneRole.isEditable());
		    }
		}

	    // Thrown by createToken/redeemToken
	    } catch (ObjectException e) {
		messages.add(new DialogMessage(Finder.getString(
		    "login.err.io", hostVal), JOptionPane.ERROR_MESSAGE));

	    // Thrown by authConverse
	    } catch (ActionRegressedException e) {
	    }
	}
    }

    private MBeanServerConnection getMBeanServerConnection(LoginRequest request,
	ConnectionInfo info) {

	try {
	    return info.getConnector().getMBeanServerConnection();
	} catch (IOException e) {
	    request.getMessages().add(new DialogMessage(
		Finder.getString("login.err.io",
		request.getHost().getValue()),
		JOptionPane.ERROR_MESSAGE));
	}
	return null;
    }

    private <T> boolean inSet(LoginProperty<T> property, List<T> valid,
	String resource, LoginRequest request) {

	if (!valid.contains(property.getValue())) {
	    String message = Finder.getString(resource,
		request.getHost().getValue(),
		request.getUser().getValue(),
		request.getRole().getValue(),
		request.getZone().getValue(),
		request.getZoneUser().getValue(),
		request.getZoneRole().getValue());

	    request.getMessages().add(new DialogMessage(message,
		JOptionPane.ERROR_MESSAGE));

	    property.setErrored(true);

	    if (property.isEditable()) {
		property.setValue(null);
	    }

	    return false;
	}

	return true;
    }

    private boolean isHostValid(LoginRequest request)
	throws ActionFailedException {

	LoginProperty<String> host = request.getHost();
	return isPropertyNonEmpty(host, request, "login.err.host.empty") &&
	    isPropertyValid(host, request);
    }

    private boolean isPropertyNonEmpty(LoginProperty<String> property,
	LoginRequest request, String resource) {

	String value = property.getValue();
	if (value == null || value.isEmpty()) {
	    request.getMessages().add(new DialogMessage(Finder.getString(
		resource), JOptionPane.ERROR_MESSAGE));
	    property.setErrored(true);
	    return false;
	}

	return true;
    }

    private <T> boolean isPropertyValid(LoginProperty<T> property,
	LoginRequest request, T... valid) throws ActionFailedException {

	try {
	    property.validate(request, valid);
	    return true;
	} catch (LoginPropertyException e) {
	    property.setErrored(true);
	    request.getMessages().add(e.getDialogMessage());
	    if (e.isFatal()) {
		requestFailed(request);
	    }
	    return false;
	}
    }

    private boolean isRoleValid(LoginRequest request, List<String> roles,
	boolean isZone) throws ActionFailedException {

	LoginProperty<String> role = isZone ? request.getZoneRole() :
	    request.getRole();

	return (role.getValue() == null ||
	    inSet(role, roles, "login.err.role.invalid", request)) &&
	    isPropertyValid(role, request,
	    roles.toArray(new String[roles.size()]));
    }

    private boolean isUserValid(LoginRequest request)
	throws ActionFailedException {

	LoginProperty<String> user = request.getUser();
	return isPropertyNonEmpty(user, request, "login.err.user.empty") &&
	    isPropertyValid(user, request);
    }

    private boolean isZoneValid(LoginRequest request, List<String> zones)
	throws ActionFailedException {

	LoginProperty<String> zone = request.getZone();
	if (zone.getValue() == null) {
	    request.getMessages().add(new DialogMessage(Finder.getString(
		"login.err.zone.empty"), JOptionPane.ERROR_MESSAGE));
	    zone.setErrored(true);
	    return false;
	}

	String resource = "login.err.zone.invalid";
	if (request.getRole().getValue() != null) {
	    resource += ".role";
	}

	return inSet(zone, zones, resource, request) &&
	    isPropertyValid(zone, request,
	    zones.toArray(new String[zones.size()]));
    }

    private boolean isZoneUserValid(LoginRequest request)
	throws ActionFailedException {

	LoginProperty<String> zoneUser = request.getZoneUser();
	return isPropertyNonEmpty(zoneUser, request,
	    "login.err.zoneuser.empty") && isPropertyValid(zoneUser, request);
    }

    private void requestFailed(LoginRequest request)
	throws ActionFailedException {

	promptForFailedRequest(request);

	List<DialogMessage> messages = request.getMessages();
	throw new ActionFailedException(messages.isEmpty() ? null :
	    messages.get(0).getText());
    }
}