/* * ==================== * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of the Common Development * and Distribution License("CDDL") (the "License"). You may not use this file * except in compliance with the License. * * You can obtain a copy of the License at * http://IdentityConnectors.dev.java.net/legal/license.txt * See the License for the specific language governing permissions and limitations * under the License. * * When distributing the Covered Code, include this CDDL Header Notice in each file * and include the License file at identityconnectors/legal/license.txt. * If applicable, add the following below this CDDL Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * ==================== */ package org.identityconnectors.ldap; import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableSet; import static org.identityconnectors.common.CollectionUtil.newCaseInsensitiveSet; import static org.identityconnectors.common.StringUtil.isNotBlank; import static org.identityconnectors.ldap.LdapUtil.getStringAttrValue; import static org.identityconnectors.ldap.LdapUtil.getStringAttrValues; import static org.identityconnectors.ldap.LdapUtil.nullAsEmpty; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Set; import javax.naming.AuthenticationException; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.Attributes; import javax.naming.ldap.Control; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.LdapContext; import org.identityconnectors.common.Pair; import org.identityconnectors.common.logging.Log; import org.identityconnectors.common.security.GuardedString; import org.identityconnectors.common.security.GuardedString.Accessor; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.ConnectorSecurityException; import org.identityconnectors.framework.common.exceptions.PasswordExpiredException; import org.identityconnectors.ldap.schema.LdapSchemaMapping; import com.sun.jndi.ldap.ctl.PasswordExpiredResponseControl; public class LdapConnection { // TODO: SASL authentication, "dn:entryDN" user name. // The LDAP attributes with a byte array syntax. private static final Set<String> LDAP_BINARY_SYNTAX_ATTRS; // The LDAP attributes which require the binary option for transfer. private static final Set<String> LDAP_BINARY_OPTION_ATTRS; static { // Cf. http://java.sun.com/products/jndi/tutorial/ldap/misc/attrs.html. LDAP_BINARY_SYNTAX_ATTRS = newCaseInsensitiveSet(); LDAP_BINARY_SYNTAX_ATTRS.add("audio"); LDAP_BINARY_SYNTAX_ATTRS.add("jpegPhoto"); LDAP_BINARY_SYNTAX_ATTRS.add("photo"); LDAP_BINARY_SYNTAX_ATTRS.add("personalSignature"); LDAP_BINARY_SYNTAX_ATTRS.add("userPassword"); LDAP_BINARY_SYNTAX_ATTRS.add("userCertificate"); LDAP_BINARY_SYNTAX_ATTRS.add("caCertificate"); LDAP_BINARY_SYNTAX_ATTRS.add("authorityRevocationList"); LDAP_BINARY_SYNTAX_ATTRS.add("deltaRevocationList"); LDAP_BINARY_SYNTAX_ATTRS.add("certificateRevocationList"); LDAP_BINARY_SYNTAX_ATTRS.add("crossCertificatePair"); LDAP_BINARY_SYNTAX_ATTRS.add("x500UniqueIdentifier"); LDAP_BINARY_SYNTAX_ATTRS.add("supportedAlgorithms"); // Java serialized objects. LDAP_BINARY_SYNTAX_ATTRS.add("javaSerializedData"); // These seem to only be present in Active Directory. LDAP_BINARY_SYNTAX_ATTRS.add("thumbnailPhoto"); LDAP_BINARY_SYNTAX_ATTRS.add("thumbnailLogo"); // Cf. RFC 4522 and RFC 4523. LDAP_BINARY_OPTION_ATTRS = newCaseInsensitiveSet(); LDAP_BINARY_OPTION_ATTRS.add("userCertificate"); LDAP_BINARY_OPTION_ATTRS.add("caCertificate"); LDAP_BINARY_OPTION_ATTRS.add("authorityRevocationList"); LDAP_BINARY_OPTION_ATTRS.add("deltaRevocationList"); LDAP_BINARY_OPTION_ATTRS.add("certificateRevocationList"); LDAP_BINARY_OPTION_ATTRS.add("crossCertificatePair"); LDAP_BINARY_OPTION_ATTRS.add("supportedAlgorithms"); } private static final String LDAP_CTX_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; private static final Log log = Log.getLog(LdapConnection.class); private final LdapConfiguration config; private final LdapSchemaMapping schemaMapping; private LdapContext initCtx; private Set<String> supportedControls; private ServerType serverType; public LdapConnection(LdapConfiguration config) { this.config = config; schemaMapping = new LdapSchemaMapping(this); } public String format(String key, String dflt, Object... args) { return config.getConnectorMessages().format(key, dflt, args); } public LdapConfiguration getConfiguration() { return config; } public LdapContext getInitialContext() { if (initCtx != null) { return initCtx; } initCtx = connect(config.getPrincipal(), config.getCredentials()); return initCtx; } private LdapContext connect(String principal, GuardedString credentials) { Pair<AuthenticationResult, LdapContext> pair = createContext(principal, credentials); if (pair.first.getType().equals(AuthenticationResultType.SUCCESS)) { return pair.second; } pair.first.propagate(); throw new IllegalStateException("Should never get here"); } private Pair<AuthenticationResult, LdapContext> createContext(String principal, GuardedString credentials) { final List<Pair<AuthenticationResult, LdapContext>> result = new ArrayList<Pair<AuthenticationResult, LdapContext>>(1); final Hashtable<Object, Object> env = new Hashtable<Object, Object>(); env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_CTX_FACTORY); env.put(Context.PROVIDER_URL, getLdapUrls()); env.put(Context.REFERRAL, "follow"); if (config.isSsl()) { env.put(Context.SECURITY_PROTOCOL, "ssl"); } String authentication = isNotBlank(principal) ? "simple" : "none"; env.put(Context.SECURITY_AUTHENTICATION, authentication); if (isNotBlank(principal)) { env.put(Context.SECURITY_PRINCIPAL, principal); if (credentials != null) { credentials.access(new Accessor() { public void access(char[] clearChars) { env.put(Context.SECURITY_CREDENTIALS, clearChars); // Connect while in the accessor, otherwise clearChars will be cleared. result.add(createContext(env)); } }); assert result.size() > 0; } else { result.add(createContext(env)); } } else { result.add(createContext(env)); } return result.get(0); } private Pair<AuthenticationResult, LdapContext> createContext(Hashtable<?, ?> env) { AuthenticationResult authnResult = null; InitialLdapContext context = null; try { context = new InitialLdapContext(env, null); if (config.isRespectResourcePasswordPolicyChangeAfterReset()) { if (hasPasswordExpiredControl(context.getResponseControls())) { authnResult = new AuthenticationResult(AuthenticationResultType.PASSWORD_EXPIRED); } } // TODO: process Password Policy control. } catch (AuthenticationException e) { String message = e.getMessage().toLowerCase(); if (message.contains("password expired")) { // Sun DS. authnResult = new AuthenticationResult(AuthenticationResultType.PASSWORD_EXPIRED, e); } else if (message.contains("password has expired")) { // RACF. authnResult = new AuthenticationResult(AuthenticationResultType.PASSWORD_EXPIRED, e); } else { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, e); } } catch (NamingException e) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, e); } if (authnResult == null) { assert context != null; authnResult = new AuthenticationResult(AuthenticationResultType.SUCCESS); } return new Pair<AuthenticationResult, LdapContext>(authnResult, context); } private static boolean hasPasswordExpiredControl(Control[] controls) { if (controls != null) { for (Control control : controls) { if (control instanceof PasswordExpiredResponseControl) { return true; } } } return false; } private String getLdapUrls() { StringBuilder builder = new StringBuilder(); builder.append("ldap://"); builder.append(config.getHost()); builder.append(':'); builder.append(config.getPort()); for (String failover : nullAsEmpty(config.getFailover())) { builder.append(' '); builder.append(failover); } return builder.toString(); } public void close() { try { quietClose(initCtx); } finally { initCtx = null; } } private static void quietClose(LdapContext ctx) { try { if (ctx != null) { ctx.close(); } } catch (NamingException e) { log.warn(e, null); } } public LdapSchemaMapping getSchemaMapping() { return schemaMapping; } public LdapNativeSchema createNativeSchema() { try { if (config.isReadSchema()) { return new ServerNativeSchema(this); } else { return new StaticNativeSchema(); } } catch (NamingException e) { throw new ConnectorException(e); } } public AuthenticationResult authenticate(String entryDN, GuardedString password) { assert entryDN != null; log.ok("Attempting to authenticate {0}", entryDN); Pair<AuthenticationResult, LdapContext> pair = createContext(entryDN, password); if (pair.second != null) { quietClose(pair.second); } log.ok("Authentication result: {0}", pair.first); return pair.first; } public void test() { checkAlive(); } public void checkAlive() { try { Attributes attrs = getInitialContext().getAttributes("", new String[] { "subschemaSubentry" } ); attrs.get("subschemaSubentry"); } catch (NamingException e) { throw new ConnectorException(e); } } /** * Returns {@code} true if the control with the given OID is supported by * the server. */ public boolean supportsControl(String oid) { return getSupportedControls().contains(oid); } private Set<String> getSupportedControls() { if (supportedControls == null) { try { Attributes attrs = getInitialContext().getAttributes("", new String[] { "supportedControl" }); supportedControls = unmodifiableSet(getStringAttrValues(attrs, "supportedControl")); } catch (NamingException e) { log.warn(e, "Exception while retrieving the supported controls"); supportedControls = emptySet(); } } return supportedControls; } public ServerType getServerType() { if (serverType == null) { serverType = detectServerType(); } return serverType; } private ServerType detectServerType() { try { Attributes attrs = getInitialContext().getAttributes("", new String[] { "vendorVersion" }); String vendorVersion = getStringAttrValue(attrs, "vendorVersion"); if (vendorVersion != null) { vendorVersion = vendorVersion.toLowerCase(); if (vendorVersion.contains("opends") || vendorVersion.contains("opendj")) { return ServerType.OPENDS; } if (vendorVersion.contains("sun") && vendorVersion.contains("directory")) { return ServerType.SUN_DSEE; } } } catch (NamingException e) { log.warn(e, "Exception while detecting the server type"); } return ServerType.UNKNOWN; } public boolean needsBinaryOption(String attrName) { return LDAP_BINARY_OPTION_ATTRS.contains(attrName); } public boolean isBinarySyntax(String attrName) { return LDAP_BINARY_SYNTAX_ATTRS.contains(attrName); } public enum AuthenticationResultType { SUCCESS { @Override public void propagate(Exception cause) { } }, PASSWORD_EXPIRED { @Override public void propagate(Exception cause) { throw new PasswordExpiredException(cause); } }, FAILED { @Override public void propagate(Exception cause) { throw new ConnectorSecurityException(cause); } }; public abstract void propagate(Exception cause); } public static class AuthenticationResult { private final AuthenticationResultType type; private final Exception cause; public AuthenticationResult(AuthenticationResultType type) { this(type, null); } public AuthenticationResult(AuthenticationResultType type, Exception cause) { assert type != null; this.type = type; this.cause = cause; } public void propagate() { type.propagate(cause); } public AuthenticationResultType getType() { return type; } @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("AuthenticationResult[type: " + type); if (cause != null) { result.append("; cause: " + cause.getMessage()); } result.append(']'); return result.toString(); } } public enum ServerType { SUN_DSEE, OPENDS, UNKNOWN } }