org.apache.hadoop.registry.client.impl.zk.RegistrySecurity.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.registry.client.impl.zk.RegistrySecurity.java

Source

/*
 * 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.hadoop.registry.client.impl.zk;

import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.util.KerberosUtil;
import org.apache.hadoop.service.AbstractService;
import org.apache.hadoop.service.ServiceStateException;
import org.apache.hadoop.util.ZKUtil;
import org.apache.zookeeper.Environment;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;
import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.security.auth.login.AppConfigurationEntry;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;

import static org.apache.hadoop.registry.client.impl.zk.ZookeeperConfigOptions.*;
import static org.apache.hadoop.registry.client.api.RegistryConstants.*;

/**
 * Implement the registry security ... a self contained service for
 * testability.
 * <p>
 * This class contains:
 * <ol>
 *   <li>
 *     The registry security policy implementation, configuration reading, ACL
 * setup and management
 *   </li>
 *   <li>Lots of static helper methods to aid security setup and debugging</li>
 * </ol>
 */

public class RegistrySecurity extends AbstractService {

    private static final Logger LOG = LoggerFactory.getLogger(RegistrySecurity.class);

    public static final String E_UNKNOWN_AUTHENTICATION_MECHANISM = "Unknown/unsupported authentication mechanism; ";

    /**
     * there's no default user to add with permissions, so it would be
     * impossible to create nodes with unrestricted user access
     */
    public static final String E_NO_USER_DETERMINED_FOR_ACLS = "No user for ACLs determinable from current user or registry option "
            + KEY_REGISTRY_USER_ACCOUNTS;

    /**
     * Error raised when the registry is tagged as secure but this
     * process doesn't have hadoop security enabled.
     */
    public static final String E_NO_KERBEROS = "Registry security is enabled -but Hadoop security is not enabled";

    /**
     * Access policy options
     */
    private enum AccessPolicy {
        anon, sasl, digest
    }

    /**
     * Access mechanism
     */
    private AccessPolicy access;

    /**
     * User used for digest auth
     */

    private String digestAuthUser;

    /**
     * Password used for digest auth
     */

    private String digestAuthPassword;

    /**
     * Auth data used for digest auth
     */
    private byte[] digestAuthData;

    /**
     * flag set to true if the registry has security enabled.
     */
    private boolean secureRegistry;

    /**
     * An ACL with read-write access for anyone
     */
    public static final ACL ALL_READWRITE_ACCESS = new ACL(ZooDefs.Perms.ALL, ZooDefs.Ids.ANYONE_ID_UNSAFE);

    /**
     * An ACL with read access for anyone
     */
    public static final ACL ALL_READ_ACCESS = new ACL(ZooDefs.Perms.READ, ZooDefs.Ids.ANYONE_ID_UNSAFE);

    /**
     * An ACL list containing the {@link #ALL_READWRITE_ACCESS} entry.
     * It is copy on write so can be shared without worry
     */
    public static final List<ACL> WorldReadWriteACL;

    static {
        List<ACL> acls = new ArrayList<ACL>();
        acls.add(ALL_READWRITE_ACCESS);
        WorldReadWriteACL = new CopyOnWriteArrayList<ACL>(acls);
    }

    /**
     * the list of system ACLs
     */
    private final List<ACL> systemACLs = new ArrayList<ACL>();

    /**
     * A list of digest ACLs which can be added to permissions
     * and cleared later.
     */
    private final List<ACL> digestACLs = new ArrayList<ACL>();

    /**
     * the default kerberos realm
     */
    private String kerberosRealm;

    /**
     * Client context
     */
    private String jaasClientContext;

    /**
     * Client identity
     */
    private String jaasClientIdentity;

    /**
     * Create an instance
     * @param name service name
     */
    public RegistrySecurity(String name) {
        super(name);
    }

    /**
     * Init the service: this sets up security based on the configuration
     * @param conf configuration
     * @throws Exception
     */
    @Override
    protected void serviceInit(Configuration conf) throws Exception {
        super.serviceInit(conf);
        String auth = conf.getTrimmed(KEY_REGISTRY_CLIENT_AUTH, REGISTRY_CLIENT_AUTH_ANONYMOUS);

        switch (auth) {
        case REGISTRY_CLIENT_AUTH_KERBEROS:
            access = AccessPolicy.sasl;
            break;
        case REGISTRY_CLIENT_AUTH_DIGEST:
            access = AccessPolicy.digest;
            break;
        case REGISTRY_CLIENT_AUTH_ANONYMOUS:
            access = AccessPolicy.anon;
            break;
        default:
            throw new ServiceStateException(E_UNKNOWN_AUTHENTICATION_MECHANISM + "\"" + auth + "\"");
        }
        initSecurity();
    }

    /**
     * Init security.
     *
     * After this operation, the {@link #systemACLs} list is valid.
     * @throws IOException
     */
    private void initSecurity() throws IOException {

        secureRegistry = getConfig().getBoolean(KEY_REGISTRY_SECURE, DEFAULT_REGISTRY_SECURE);
        systemACLs.clear();
        if (secureRegistry) {
            addSystemACL(ALL_READ_ACCESS);

            // determine the kerberos realm from JVM and settings
            kerberosRealm = getConfig().get(KEY_REGISTRY_KERBEROS_REALM, getDefaultRealmInJVM());

            // System Accounts
            String system = getOrFail(KEY_REGISTRY_SYSTEM_ACCOUNTS, DEFAULT_REGISTRY_SYSTEM_ACCOUNTS);

            systemACLs.addAll(buildACLs(system, kerberosRealm, ZooDefs.Perms.ALL));

            // user accounts (may be empty, but for digest one user AC must
            // be built up
            String user = getConfig().get(KEY_REGISTRY_USER_ACCOUNTS, DEFAULT_REGISTRY_USER_ACCOUNTS);
            List<ACL> userACLs = buildACLs(user, kerberosRealm, ZooDefs.Perms.ALL);

            // add self if the current user can be determined
            ACL self;
            if (UserGroupInformation.isSecurityEnabled()) {
                self = createSaslACLFromCurrentUser(ZooDefs.Perms.ALL);
                if (self != null) {
                    userACLs.add(self);
                }
            }

            // here check for UGI having secure on or digest + ID
            switch (access) {
            case sasl:
                // secure + SASL => has to be authenticated
                if (!UserGroupInformation.isSecurityEnabled()) {
                    throw new IOException("Kerberos required for secure registry access");
                }
                UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
                jaasClientContext = getOrFail(KEY_REGISTRY_CLIENT_JAAS_CONTEXT,
                        DEFAULT_REGISTRY_CLIENT_JAAS_CONTEXT);
                jaasClientIdentity = currentUser.getShortUserName();
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Auth is SASL user=\"{}\" JAAS context=\"{}\"", jaasClientIdentity,
                            jaasClientContext);
                }
                break;

            case digest:
                String id = getOrFail(KEY_REGISTRY_CLIENT_AUTHENTICATION_ID, "");
                String pass = getOrFail(KEY_REGISTRY_CLIENT_AUTHENTICATION_PASSWORD, "");
                if (userACLs.isEmpty()) {
                    //
                    throw new ServiceStateException(E_NO_USER_DETERMINED_FOR_ACLS);
                }
                digest(id, pass);
                ACL acl = new ACL(ZooDefs.Perms.ALL, toDigestId(id, pass));
                userACLs.add(acl);
                digestAuthUser = id;
                digestAuthPassword = pass;
                String authPair = id + ":" + pass;
                digestAuthData = authPair.getBytes("UTF-8");
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Auth is Digest ACL: {}", aclToString(acl));
                }
                break;

            case anon:
                // nothing is needed; account is read only.
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Auth is anonymous");
                }
                userACLs = new ArrayList<ACL>(0);
                break;
            }
            systemACLs.addAll(userACLs);

        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Registry has no security");
            }
            // wide open cluster, adding system acls
            systemACLs.addAll(WorldReadWriteACL);
        }
    }

    /**
     * Add another system ACL
     * @param acl add ACL
     */
    public void addSystemACL(ACL acl) {
        systemACLs.add(acl);
    }

    /**
     * Add a digest ACL
     * @param acl add ACL
     */
    public boolean addDigestACL(ACL acl) {
        if (secureRegistry) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Added ACL {}", aclToString(acl));
            }
            digestACLs.add(acl);
            return true;
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Ignoring added ACL - registry is insecure{}", aclToString(acl));
            }
            return false;
        }
    }

    /**
     * Reset the digest ACL list
     */
    public void resetDigestACLs() {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Cleared digest ACLs");
        }
        digestACLs.clear();
    }

    /**
     * Flag to indicate the cluster is secure
     * @return true if the config enabled security
     */
    public boolean isSecureRegistry() {
        return secureRegistry;
    }

    /**
     * Get the system principals
     * @return the system principals
     */
    public List<ACL> getSystemACLs() {
        Preconditions.checkNotNull(systemACLs, "registry security is unitialized");
        return Collections.unmodifiableList(systemACLs);
    }

    /**
     * Get all ACLs needed for a client to use when writing to the repo.
     * That is: system ACLs, its own ACL, any digest ACLs
     * @return the client ACLs
     */
    public List<ACL> getClientACLs() {
        List<ACL> clientACLs = new ArrayList<ACL>(systemACLs);
        clientACLs.addAll(digestACLs);
        return clientACLs;
    }

    /**
     * Create a SASL ACL for the user
     * @param perms permissions
     * @return an ACL for the current user or null if they aren't a kerberos user
     * @throws IOException
     */
    public ACL createSaslACLFromCurrentUser(int perms) throws IOException {
        UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
        if (currentUser.hasKerberosCredentials()) {
            return createSaslACL(currentUser, perms);
        } else {
            return null;
        }
    }

    /**
     * Given a UGI, create a SASL ACL from it
     * @param ugi UGI
     * @param perms permissions
     * @return a new ACL
     */
    public ACL createSaslACL(UserGroupInformation ugi, int perms) {
        String userName = ugi.getUserName();
        return new ACL(perms, new Id(SCHEME_SASL, userName));
    }

    /**
     * Get a conf option, throw an exception if it is null/empty
     * @param key key
     * @param defval default value
     * @return the value
     * @throws IOException if missing
     */
    private String getOrFail(String key, String defval) throws IOException {
        String val = getConfig().get(key, defval);
        if (StringUtils.isEmpty(val)) {
            throw new IOException("Missing value for configuration option " + key);
        }
        return val;
    }

    /**
     * Check for an id:password tuple being valid.
     * This test is stricter than that in {@link DigestAuthenticationProvider},
     * which splits the string, but doesn't check the contents of each
     * half for being non-"".
     * @param idPasswordPair id:pass pair
     * @return true if the pass is considered valid.
     */
    public boolean isValid(String idPasswordPair) {
        String[] parts = idPasswordPair.split(":");
        return parts.length == 2 && !StringUtils.isEmpty(parts[0]) && !StringUtils.isEmpty(parts[1]);
    }

    /**
     * Get the derived kerberos realm.
     * @return this is built from the JVM realm, or the configuration if it
     * overrides it. If "", it means "don't know".
     */
    public String getKerberosRealm() {
        return kerberosRealm;
    }

    /**
     * Generate a base-64 encoded digest of the idPasswordPair pair
     * @param idPasswordPair id:password
     * @return a string that can be used for authentication
     */
    public String digest(String idPasswordPair) throws IOException {
        if (StringUtils.isEmpty(idPasswordPair) || !isValid(idPasswordPair)) {
            throw new IOException("Invalid id:password");
        }
        try {
            return DigestAuthenticationProvider.generateDigest(idPasswordPair);
        } catch (NoSuchAlgorithmException e) {
            // unlikely since it is standard to the JVM, but maybe JCE restrictions
            // could trigger it
            throw new IOException(e.toString(), e);
        }
    }

    /**
     * Generate a base-64 encoded digest of the idPasswordPair pair
     * @param id ID
     * @param password pass
     * @return a string that can be used for authentication
     * @throws IOException
     */
    public String digest(String id, String password) throws IOException {
        return digest(id + ":" + password);
    }

    /**
     * Given a digest, create an ID from it
     * @param digest digest
     * @return ID
     */
    public Id toDigestId(String digest) {
        return new Id(SCHEME_DIGEST, digest);
    }

    /**
     * Create a Digest ID from an id:pass pair
     * @param id ID
     * @param password password
     * @return an ID
     * @throws IOException
     */
    public Id toDigestId(String id, String password) throws IOException {
        return toDigestId(digest(id, password));
    }

    /**
     * Split up a list of the form
     * <code>sasl:mapred@,digest:5f55d66, sasl@yarn@EXAMPLE.COM</code>
     * into a list of possible ACL values, trimming as needed
     *
     * The supplied realm is added to entries where
     * <ol>
     *   <li>the string begins "sasl:"</li>
     *   <li>the string ends with "@"</li>
     * </ol>
     * No attempt is made to validate any of the acl patterns.
     *
     * @param aclString list of 0 or more ACLs
     * @param realm realm to add
     * @return a list of split and potentially patched ACL pairs.
     *
     */
    public List<String> splitAclPairs(String aclString, String realm) {
        List<String> list = Lists.newArrayList(Splitter.on(',').omitEmptyStrings().trimResults().split(aclString));
        ListIterator<String> listIterator = list.listIterator();
        while (listIterator.hasNext()) {
            String next = listIterator.next();
            if (next.startsWith(SCHEME_SASL + ":") && next.endsWith("@")) {
                listIterator.set(next + realm);
            }
        }
        return list;
    }

    /**
     * Parse a string down to an ID, adding a realm if needed
     * @param idPair id:data tuple
     * @param realm realm to add
     * @return the ID.
     * @throws IllegalArgumentException if the idPair is invalid
     */
    public Id parse(String idPair, String realm) {
        int firstColon = idPair.indexOf(':');
        int lastColon = idPair.lastIndexOf(':');
        if (firstColon == -1 || lastColon == -1 || firstColon != lastColon) {
            throw new IllegalArgumentException("ACL '" + idPair + "' not of expected form scheme:id");
        }
        String scheme = idPair.substring(0, firstColon);
        String id = idPair.substring(firstColon + 1);
        if (id.endsWith("@")) {
            Preconditions.checkArgument(StringUtils.isNotEmpty(realm), "@ suffixed account but no realm %s", id);
            id = id + realm;
        }
        return new Id(scheme, id);
    }

    /**
     * Parse the IDs, adding a realm if needed, setting the permissions
     * @param principalList id string
     * @param realm realm to add
     * @param perms permissions
     * @return the relevant ACLs
     * @throws IOException
     */
    public List<ACL> buildACLs(String principalList, String realm, int perms) throws IOException {
        List<String> aclPairs = splitAclPairs(principalList, realm);
        List<ACL> ids = new ArrayList<ACL>(aclPairs.size());
        for (String aclPair : aclPairs) {
            ACL newAcl = new ACL();
            newAcl.setId(parse(aclPair, realm));
            newAcl.setPerms(perms);
            ids.add(newAcl);
        }
        return ids;
    }

    /**
     * Parse an ACL list. This includes configuration indirection
     * {@link ZKUtil#resolveConfIndirection(String)}
     * @param zkAclConf configuration string
     * @return an ACL list
     * @throws IOException on a bad ACL parse
     */
    public List<ACL> parseACLs(String zkAclConf) throws IOException {
        try {
            return ZKUtil.parseACLs(ZKUtil.resolveConfIndirection(zkAclConf));
        } catch (ZKUtil.BadAclFormatException e) {
            throw new IOException("Parsing " + zkAclConf + " :" + e, e);
        }
    }

    /**
     * Get the appropriate Kerberos Auth module for JAAS entries
     * for this JVM.
     * @return a JVM-specific kerberos login module classname.
     */
    public static String getKerberosAuthModuleForJVM() {
        if (System.getProperty("java.vendor").contains("IBM")) {
            return "com.ibm.security.auth.module.Krb5LoginModule";
        } else {
            return "com.sun.security.auth.module.Krb5LoginModule";
        }
    }

    /**
     * JAAS template: {@value}
     * Note the semicolon on the last entry
     */
    private static final String JAAS_ENTRY = "%s { %n" + " %s required%n"
    // kerberos module
            + " keyTab=\"%s\"%n" + " debug=true%n" + " principal=\"%s\"%n" + " useKeyTab=true%n"
            + " useTicketCache=false%n" + " doNotPrompt=true%n" + " storeKey=true;%n" + "}; %n";

    /**
     * Create a JAAS entry for insertion
     * @param context context of the entry
     * @param principal kerberos principal
     * @param keytab keytab
     * @return a context
     */
    public String createJAASEntry(String context, String principal, File keytab) {
        Preconditions.checkArgument(StringUtils.isNotEmpty(principal), "invalid principal");
        Preconditions.checkArgument(StringUtils.isNotEmpty(context), "invalid context");
        Preconditions.checkArgument(keytab != null && keytab.isFile(), "Keytab null or missing: ");
        String keytabpath = keytab.getAbsolutePath();
        // fix up for windows; no-op on unix
        keytabpath = keytabpath.replace('\\', '/');
        return String.format(Locale.ENGLISH, JAAS_ENTRY, context, getKerberosAuthModuleForJVM(), keytabpath,
                principal);
    }

    /**
     * Bind the JVM JAS setting to the specified JAAS file.
     *
     * <b>Important:</b> once a file has been loaded the JVM doesn't pick up
     * changes
     * @param jaasFile the JAAS file
     */
    public static void bindJVMtoJAASFile(File jaasFile) {
        String path = jaasFile.getAbsolutePath();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Binding {} to {}", Environment.JAAS_CONF_KEY, path);
        }
        System.setProperty(Environment.JAAS_CONF_KEY, path);
    }

    /**
     * Set the Zookeeper server property
     * {@link ZookeeperConfigOptions#PROP_ZK_SERVER_SASL_CONTEXT}
     * to the SASL context. When the ZK server starts, this is the context
     * which it will read in
     * @param contextName the name of the context
     */
    public static void bindZKToServerJAASContext(String contextName) {
        System.setProperty(PROP_ZK_SERVER_SASL_CONTEXT, contextName);
    }

    /**
     * Reset any system properties related to JAAS
     */
    public static void clearJaasSystemProperties() {
        System.clearProperty(Environment.JAAS_CONF_KEY);
    }

    /**
     * Resolve the context of an entry. This is an effective test of
     * JAAS setup, because it will relay detected problems up
     * @param context context name
     * @return the entry
     * @throws RuntimeException if there is no context entry found
     */
    public static AppConfigurationEntry[] validateContext(String context) {
        if (context == null) {
            throw new RuntimeException("Null context argument");
        }
        if (context.isEmpty()) {
            throw new RuntimeException("Empty context argument");
        }
        javax.security.auth.login.Configuration configuration = javax.security.auth.login.Configuration
                .getConfiguration();
        AppConfigurationEntry[] entries = configuration.getAppConfigurationEntry(context);
        if (entries == null) {
            throw new RuntimeException(String.format("Entry \"%s\" not found; " + "JAAS config = %s", context,
                    describeProperty(Environment.JAAS_CONF_KEY)));
        }
        return entries;
    }

    /**
     * Apply the security environment to this curator instance. This
     * may include setting up the ZK system properties for SASL
     * @param builder curator builder
     */
    public void applySecurityEnvironment(CuratorFrameworkFactory.Builder builder) {

        if (isSecureRegistry()) {
            switch (access) {
            case anon:
                clearZKSaslClientProperties();
                break;

            case digest:
                // no SASL
                clearZKSaslClientProperties();
                builder.authorization(SCHEME_DIGEST, digestAuthData);
                break;

            case sasl:
                // bind to the current identity and context within the JAAS file
                setZKSaslClientProperties(jaasClientIdentity, jaasClientContext);
            }
        }
    }

    /**
     * Set the client properties. This forces the ZK client into
     * failing if it can't auth.
     * <b>Important:</b>This is JVM-wide.
     * @param username username
     * @param context login context
     * @throws RuntimeException if the context cannot be found in the current
     * JAAS context
     */
    public static void setZKSaslClientProperties(String username, String context) {
        RegistrySecurity.validateContext(context);
        enableZookeeperClientSASL();
        System.setProperty(PROP_ZK_SASL_CLIENT_USERNAME, username);
        System.setProperty(PROP_ZK_SASL_CLIENT_CONTEXT, context);
    }

    /**
     * Clear all the ZK SASL Client properties
     * <b>Important:</b>This is JVM-wide
     */
    public static void clearZKSaslClientProperties() {
        disableZookeeperClientSASL();
        System.clearProperty(PROP_ZK_SASL_CLIENT_CONTEXT);
        System.clearProperty(PROP_ZK_SASL_CLIENT_USERNAME);
    }

    /**
     * Turn ZK SASL on
     * <b>Important:</b>This is JVM-wide
     */
    protected static void enableZookeeperClientSASL() {
        System.setProperty(PROP_ZK_ENABLE_SASL_CLIENT, "true");
    }

    /**
     * Force disable ZK SASL bindings.
     * <b>Important:</b>This is JVM-wide
     */
    public static void disableZookeeperClientSASL() {
        System.setProperty(ZookeeperConfigOptions.PROP_ZK_ENABLE_SASL_CLIENT, "false");
    }

    /**
     * Is the system property enabling the SASL client set?
     * @return true if the SASL client system property is set.
     */
    public static boolean isClientSASLEnabled() {
        return Boolean.parseBoolean(System.getProperty(ZookeeperConfigOptions.PROP_ZK_ENABLE_SASL_CLIENT, "true"));
    }

    /**
     * Log details about the current Hadoop user at INFO.
     * Robust against IOEs when trying to get the current user
     */
    public void logCurrentHadoopUser() {
        try {
            UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
            LOG.info("Current user = {}", currentUser);
            UserGroupInformation realUser = currentUser.getRealUser();
            LOG.info("Real User = {}", realUser);
        } catch (IOException e) {
            LOG.warn("Failed to get current user {}, {}", e);
        }
    }

    /**
     * Stringify a list of ACLs for logging. Digest ACLs have their
     * digest values stripped for security.
     * @param acls ACL list
     * @return a string for logs, exceptions, ...
     */
    public static String aclsToString(List<ACL> acls) {
        StringBuilder builder = new StringBuilder();
        if (acls == null) {
            builder.append("null ACL");
        } else {
            builder.append('\n');
            for (ACL acl : acls) {
                builder.append(aclToString(acl)).append(" ");
            }
        }
        return builder.toString();
    }

    /**
     * Convert an ACL to a string, with any obfuscation needed
     * @param acl ACL
     * @return ACL string value
     */
    public static String aclToString(ACL acl) {
        return String.format(Locale.ENGLISH, "0x%02x: %s", acl.getPerms(), idToString(acl.getId()));
    }

    /**
     * Convert an ID to a string, stripping out all but the first few characters
     * of any digest auth hash for security reasons
     * @param id ID
     * @return a string description of a Zookeeper ID
     */
    public static String idToString(Id id) {
        String s;
        if (id.getScheme().equals(SCHEME_DIGEST)) {
            String ids = id.getId();
            int colon = ids.indexOf(':');
            if (colon > 0) {
                ids = ids.substring(colon + 3);
            }
            s = SCHEME_DIGEST + ": " + ids;
        } else {
            s = id.toString();
        }
        return s;
    }

    /**
     * Build up low-level security diagnostics to aid debugging
     * @return a string to use in diagnostics
     */
    public String buildSecurityDiagnostics() {
        StringBuilder builder = new StringBuilder();
        builder.append(secureRegistry ? "secure registry; " : "insecure registry; ");
        builder.append("Curator service access policy: ").append(access);

        builder.append("; System ACLs: ").append(aclsToString(systemACLs));
        builder.append("User: ").append(UgiInfo.fromCurrentUser());
        builder.append("; Kerberos Realm: ").append(kerberosRealm);
        builder.append(describeProperty(Environment.JAAS_CONF_KEY));
        String sasl = System.getProperty(PROP_ZK_ENABLE_SASL_CLIENT, DEFAULT_ZK_ENABLE_SASL_CLIENT);
        boolean saslEnabled = Boolean.parseBoolean(sasl);
        builder.append(describeProperty(PROP_ZK_ENABLE_SASL_CLIENT, DEFAULT_ZK_ENABLE_SASL_CLIENT));
        if (saslEnabled) {
            builder.append("; JAAS Client Identity").append("=").append(jaasClientIdentity).append("; ");
            builder.append(KEY_REGISTRY_CLIENT_JAAS_CONTEXT).append("=").append(jaasClientContext).append("; ");
            builder.append(describeProperty(PROP_ZK_SASL_CLIENT_USERNAME));
            builder.append(describeProperty(PROP_ZK_SASL_CLIENT_CONTEXT));
        }
        builder.append(describeProperty(PROP_ZK_ALLOW_FAILED_SASL_CLIENTS, "(undefined but defaults to true)"));
        builder.append(describeProperty(PROP_ZK_SERVER_MAINTAIN_CONNECTION_DESPITE_SASL_FAILURE));
        return builder.toString();
    }

    private static String describeProperty(String name) {
        return describeProperty(name, "(undefined)");
    }

    private static String describeProperty(String name, String def) {
        return "; " + name + "=" + System.getProperty(name, def);
    }

    /**
     * Get the default kerberos realm returning "" if there
     * is no realm or other problem
     * @return the default realm of the system if it
     * could be determined
     */
    public static String getDefaultRealmInJVM() {
        try {
            return KerberosUtil.getDefaultRealm();
            // JDK7
        } catch (ClassNotFoundException ignored) {
            // ignored
        } catch (NoSuchMethodException ignored) {
            // ignored
        } catch (IllegalAccessException ignored) {
            // ignored
        } catch (InvocationTargetException ignored) {
            // ignored
        }
        return "";
    }

    /**
     * Create an ACL For a user.
     * @param ugi User identity
     * @return the ACL For the specified user. Ifthe username doesn't end
     * in "@" then the realm is added
     */
    public ACL createACLForUser(UserGroupInformation ugi, int perms) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Creating ACL For ", new UgiInfo(ugi));
        }
        if (!secureRegistry) {
            return ALL_READWRITE_ACCESS;
        } else {
            return createACLfromUsername(ugi.getUserName(), perms);
        }
    }

    /**
     * Given a user name (short or long), create a SASL ACL
     * @param username user name; if it doesn't contain an "@" symbol, the
     * service's kerberos realm is added
     * @param perms permissions
     * @return an ACL for the user
     */
    public ACL createACLfromUsername(String username, int perms) {
        if (!username.contains("@")) {
            username = username + "@" + kerberosRealm;
            if (LOG.isDebugEnabled()) {
                LOG.debug("Appending kerberos realm to make {}", username);
            }
        }
        return new ACL(perms, new Id(SCHEME_SASL, username));
    }

    /**
     * On demand string-ifier for UGI with extra details
     */
    public static class UgiInfo {

        public static UgiInfo fromCurrentUser() {
            try {
                return new UgiInfo(UserGroupInformation.getCurrentUser());
            } catch (IOException e) {
                LOG.info("Failed to get current user {}", e, e);
                return new UgiInfo(null);
            }
        }

        private final UserGroupInformation ugi;

        public UgiInfo(UserGroupInformation ugi) {
            this.ugi = ugi;
        }

        @Override
        public String toString() {
            if (ugi == null) {
                return "(null ugi)";
            }
            StringBuilder builder = new StringBuilder();
            builder.append(ugi.getUserName()).append(": ");
            builder.append(ugi.toString());
            builder.append(" hasKerberosCredentials=").append(ugi.hasKerberosCredentials());
            builder.append(" isFromKeytab=").append(ugi.isFromKeytab());
            builder.append(" kerberos is enabled in Hadoop =").append(UserGroupInformation.isSecurityEnabled());
            return builder.toString();
        }

    }

    /**
     * on-demand stringifier for a list of ACLs
     */
    public static class AclListInfo {
        public final List<ACL> acls;

        public AclListInfo(List<ACL> acls) {
            this.acls = acls;
        }

        @Override
        public String toString() {
            return aclsToString(acls);
        }
    }
}