/* * ==================== * 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]" * ==================== * * Portions Copyrighted 2013-2014 ForgeRock AS * Portions Copyrighted 2014 Evolveum */ 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.io.IOException; 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.InitialLdapContext; import javax.naming.ldap.LdapContext; import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsResponse; import javax.naming.ldap.Control; 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.ConnectionFailedException; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.PasswordExpiredException; import org.identityconnectors.framework.common.exceptions.ConnectionFailedException; import org.identityconnectors.framework.common.exceptions.InvalidCredentialException; import org.identityconnectors.ldap.schema.LdapSchemaMapping; 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"); // MS-AD ObjectGUID LDAP_BINARY_SYNTAX_ATTRS.add(LdapConstants.MS_GUID_ATTR); } private static final String LDAP_CTX_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; public static final String SASL_GSSAPI = "SASL-GSSAPI"; public static final String PASSWORD_EXPIRED_OID = "2.16.840.1.113730.3.4.4"; 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; } private Hashtable getDefaultContextEnv(){ final Hashtable env = new Hashtable(11); env.put("java.naming.ldap.attributes.binary", LdapConstants.MS_GUID_ATTR); env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_CTX_FACTORY); env.put(Context.PROVIDER_URL, getLdapUrls()); env.put(Context.REFERRAL, "follow"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); if (config.isSsl()) { env.put(Context.SECURITY_PROTOCOL, "ssl"); } return env; } private LdapContext getAnonymousContext() throws NamingException { InitialLdapContext ctx = null; return new InitialLdapContext(getDefaultContextEnv(), null); } public LdapContext getInitialContext() { if (initCtx != null) { return initCtx; } initCtx = connect(config.getPrincipal(), config.getCredentials()); return initCtx; } public LdapContext getRunAsContext(String principal, GuardedString credentials) { return connect(principal, credentials); } 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 Hashtable<Object, Object> env = new Hashtable<Object, Object>(); env.put("java.naming.ldap.attributes.binary", LdapConstants.MS_GUID_ATTR); env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_CTX_FACTORY); env.put(Context.PROVIDER_URL, getLdapUrls()); env.put(Context.REFERRAL, config.getReferralsHandling()); if (config.isSsl()) { env.put(Context.SECURITY_PROTOCOL, "ssl"); } InitialLdapContext context; try { context = new InitialLdapContext(env, null); } catch (NamingException e) { // TODO better analysis of the cause and appropriate exception throw new ConnectionFailedException(e.getMessage(), e); } if (config.isStartTls()) { StartTlsResponse tls; try { tls = (StartTlsResponse) context.extendedOperation(new StartTlsRequest()); } catch (NamingException e) { throw new ConnectionFailedException(e.getMessage(), e); } try { tls.negotiate(); } catch (IOException e) { throw new ConnectionFailedException(e.getMessage(), e); } } AuthenticationResult authenticationResult = authenticateContext(context, principal, credentials); return new Pair<AuthenticationResult, LdapContext>(authenticationResult, context); } private AuthenticationResult authenticateContext(final InitialLdapContext context, String principal, GuardedString credentials) { AuthenticationResult authnResult = null; try { String authentication; if (SASL_GSSAPI.equalsIgnoreCase(config.getAuthType())) { authentication = "GSSAPI"; } else { authentication = isNotBlank(principal) ? "simple" : "none"; } context.addToEnvironment(Context.SECURITY_AUTHENTICATION, authentication); if (isNotBlank(principal)) { context.addToEnvironment(Context.SECURITY_PRINCIPAL, principal); try { credentials.access(new Accessor() { public void access(char[] clearChars) { try { context.addToEnvironment(Context.SECURITY_CREDENTIALS, new String(clearChars)); } catch (NamingException e) { new RuntimeException(e); } } }); } catch (RuntimeException e) { // Magic to tunnel NamingException out of the "closure". Throwable cause = e.getCause(); if (cause instanceof NamingException) { throw (NamingException)cause; } else { throw e; } } }; if (config.isRespectResourcePasswordPolicyChangeAfterReset()) { if (hasPasswordExpiredControl(context.getResponseControls())) { authnResult = new AuthenticationResult(AuthenticationResultType.PASSWORD_EXPIRED); } } } catch (AuthenticationException e) { String message = e.getMessage().toLowerCase(); //SUN_DSEE, OPENDS, OPENDJ, IBM, MSAD, MSAD_LDS, MSAD_GC, NOVELL, UNBOUNDID, OPENLDAP, UNKNOWN switch (getServerType()) { case MSAD: case MSAD_GC: case MSAD_LDS: if (message.contains("ldap: error code 49 ")) { if (message.contains("data 525,")) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, new AuthenticationException("User not found")); } else if (message.contains("data 52e,")) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, new AuthenticationException("Invalid credentials")); } else if (message.contains("data 530,")) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, new AuthenticationException("Not permitted to logon at this time")); } else if (message.contains("data 531,")) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, new AuthenticationException("Not permitted to logon at this workstation")); } else if (message.contains("data 532,")) { authnResult = new AuthenticationResult(AuthenticationResultType.PASSWORD_EXPIRED, new AuthenticationException("Password expired")); } else if (message.contains("data 533,")) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, new AuthenticationException("Account disabled")); } else if (message.contains("data 701,")) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, new AuthenticationException("Account expired")); } else if (message.contains("data 773,")) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, new AuthenticationException("User must reset password")); } else if (message.contains("data 775,")) { authnResult = new AuthenticationResult(AuthenticationResultType.FAILED, new AuthenticationException("User account locked")); } else { } } break; case SUN_DSEE: if (message.contains("password expired")) { // Sun DS. authnResult = new AuthenticationResult(AuthenticationResultType.PASSWORD_EXPIRED, e); } break; case UNKNOWN: if (message.contains("password has expired")) { // RACF. authnResult = new AuthenticationResult(AuthenticationResultType.PASSWORD_EXPIRED, e); } break; case OPENDJ: case OPENLDAP: default: 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 authnResult; } private static boolean hasPasswordExpiredControl(Control[] controls) { if (controls != null) { for (Control control : controls) { if (PASSWORD_EXPIRED_OID.equalsIgnoreCase(control.getID())) 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() { LdapContext anonymousContext = null; try { anonymousContext = getAnonymousContext(); Attributes attrs = anonymousContext.getAttributes("", new String[]{"vendorVersion", "vendorName", "highestCommittedUSN", "rootDomainNamingContext", "structuralObjectClass"}); String vendorName = getStringAttrValue(attrs, "vendorName"); if (null != vendorName) { vendorName = vendorName.toLowerCase(); if (vendorName.contains("ibm")) { log.info("IBM Directory server has been detected"); return ServerType.IBM; } if (vendorName.contains("novell")) { log.info("Novell eDirectory server has been detected"); return ServerType.NOVELL; } if (vendorName.contains("unboundid")) { log.info("UnboundID Directory server has been detected"); return ServerType.UNBOUNDID; } } String vendorVersion = getStringAttrValue(attrs, "vendorVersion"); if (vendorVersion != null) { vendorVersion = vendorVersion.toLowerCase(); if (vendorVersion.contains("opends")) { log.info("OpenDS Directory server has been detected"); return ServerType.OPENDS; } if (vendorVersion.contains("opendj")) { log.info("ForgeRock OpenDJ Directory server has been detected"); return ServerType.OPENDJ; } if (vendorVersion.contains("sun") && vendorVersion.contains("directory")) { log.info("Sun DSEE Directory server has been detected"); return ServerType.SUN_DSEE; } } else { String hUSN = getStringAttrValue(attrs, "highestCommittedUSN"); String rDC = getStringAttrValue(attrs, "rootDomainNamingContext"); String sOC = getStringAttrValue(attrs, "structuralObjectClass"); if (hUSN != null) { // Windows Active Directory if (rDC != null) { // Only DCs and GCs have the rootDomainNamingContext // We check the port number as well. DC is using the standard 389|636 pair. if ((config.getPort() != 389) && (config.getPort() != 636)) { log.info("MS Active Directory Global Catalog server has been detected"); return ServerType.MSAD_GC; } else { log.info("MS Active Directory server has been detected"); return ServerType.MSAD; } } // ADLDS does not have the rootDomainNamingContext... log.info("MS Active Directory Lightweight Directory Services server has been detected"); return ServerType.MSAD_LDS; } else if (sOC != null && sOC.equalsIgnoreCase("OpenLDAProotDSE")) { return ServerType.OPENLDAP; } } } catch (NamingException e) { log.warn("Exception while detecting the server type: {0}", e.getExplanation()); } finally { if (null != anonymousContext) { try { anonymousContext.close(); } catch (NamingException ex) { log.ok(ex, "Exception while detecting the server type"); } } } log.info("Directory server type is unknown"); 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 InvalidCredentialException(cause.getMessage(), 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, OPENDJ, IBM, MSAD, MSAD_LDS, MSAD_GC, NOVELL, UNBOUNDID, OPENLDAP, UNKNOWN } }