/* * Copyright 2011 Future Systems * * Licensed 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.krakenapps.ldap.impl; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.UnknownHostException; import java.security.KeyStore; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.StringTokenizer; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Provides; import org.apache.felix.ipojo.annotations.Requires; import org.krakenapps.confdb.Config; import org.krakenapps.confdb.ConfigDatabase; import org.krakenapps.confdb.ConfigService; import org.krakenapps.confdb.Predicates; import org.krakenapps.ldap.LdapOrgUnit; import org.krakenapps.ldap.LdapUser; import org.krakenapps.ldap.LdapProfile; import org.krakenapps.ldap.LdapServerType; import org.krakenapps.ldap.LdapService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.novell.ldap.LDAPAttribute; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPEntry; import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPJSSESecureSocketFactory; import com.novell.ldap.LDAPModification; import com.novell.ldap.LDAPReferralException; import com.novell.ldap.LDAPSearchConstraints; import com.novell.ldap.LDAPSearchResults; import com.novell.ldap.LDAPSocketFactory; @Component(name = "ldap-service") @Provides public class JLdapService implements LdapService { private static final int DEFAULT_TIMEOUT = 5000; private final Logger logger = LoggerFactory.getLogger(JLdapService.class); @Requires private ConfigService conf; private ConfigDatabase getDatabase() { return conf.ensureDatabase("kraken-ldap"); } @Override public Collection<LdapProfile> getProfiles() { return getDatabase().findAll(LdapProfile.class).getDocuments(LdapProfile.class); } @Override public LdapProfile getProfile(String name) { Config c = getDatabase().findOne(LdapProfile.class, Predicates.field("name", name)); if (c == null) return null; return c.getDocument(LdapProfile.class); } @Override public void createProfile(LdapProfile profile) { if (getProfile(profile.getName()) != null) throw new IllegalStateException("already exist"); getDatabase().add(profile); } @Override public void updateProfile(LdapProfile profile) { ConfigDatabase db = getDatabase(); Config c = db.findOne(LdapProfile.class, Predicates.field("name", profile.getName())); if (c == null) throw new IllegalStateException("not exist"); db.update(c, profile); } @Override public void removeProfile(String name) { ConfigDatabase db = getDatabase(); Config c = db.findOne(LdapProfile.class, Predicates.field("name", name)); if (c == null) throw new IllegalStateException("not exist"); db.remove(c); } @Override public Collection<LdapUser> getUsers(LdapProfile profile) { List<LdapUser> users = new ArrayList<LdapUser>(); LDAPConnection lc = openLdapConnection(profile, null); int count = 0; try { String filter = "(&(userPrincipalName=*))"; if (profile.getServerType() != LdapServerType.ActiveDirectory) filter = "(&(objectClass=inetOrgPerson))"; String idAttr = profile.getIdAttr() == null ? "uid" : profile.getIdAttr(); LDAPSearchConstraints cons = new LDAPSearchConstraints(); cons.setTimeLimit(20000); cons.setMaxResults(0); LDAPSearchResults r = lc.search(buildBaseDN(profile), LDAPConnection.SCOPE_SUB, filter, null, false, cons); while (r.hasMore()) { try { LDAPEntry entry = r.next(); logger.debug("kraken-ldap: fetch entry [{}]", entry.getDN()); users.add(new LdapUser(entry, idAttr)); count++; } catch (LDAPReferralException e) { } } logger.info("kraken-ldap: profile [{}], total {} ldap entries", profile.getName(), count); } catch (LDAPException e) { if (e.getResultCode() == LDAPException.SIZE_LIMIT_EXCEEDED) logger.error("kraken-ldap: profile [{}], size limit, fetched only {} ldap entries", profile.getName(), count); throw new IllegalStateException(e); } catch (Exception e) { logger.error("kraken-ldap: cannot fetch domain users", e); throw new IllegalStateException(e); } finally { try { if (lc != null && lc.isConnected()) lc.disconnect(); } catch (LDAPException e) { logger.error("kraken ldap: disconnect failed", e); } } return users; } @Override public LdapUser findUser(LdapProfile profile, String uid) { LDAPConnection lc = openLdapConnection(profile, null); try { String filter = "(&(sAMAccountName=" + uid + "))"; if (profile.getServerType() != LdapServerType.ActiveDirectory) filter = buildUserFilter(profile, uid); String idAttr = profile.getIdAttr() == null ? "uid" : profile.getIdAttr(); LDAPSearchConstraints cons = new LDAPSearchConstraints(); cons.setTimeLimit(20000); cons.setMaxResults(1); LDAPSearchResults r = lc.search(buildBaseDN(profile), LDAPConnection.SCOPE_SUB, filter, null, false, cons); if (r.hasMore()) { try { LDAPEntry entry = r.next(); if (logger.isDebugEnabled()) logger.debug("kraken-ldap: fetch entry [{}]", entry); return new LdapUser(entry, idAttr); } catch (LDAPReferralException e) { } } } catch (Exception e) { logger.error("kraken-ldap: cannot fetch domain users", e); throw new IllegalStateException(e); } finally { try { if (lc != null && lc.isConnected()) lc.disconnect(); } catch (LDAPException e) { logger.error("kraken ldap: disconnect failed", e); } } return null; } @Override public Collection<LdapOrgUnit> getOrgUnits(LdapProfile profile) { List<LdapOrgUnit> ous = new ArrayList<LdapOrgUnit>(); LDAPConnection lc = openLdapConnection(profile, null); try { String filter = "(objectClass=organizationalUnit)"; LDAPSearchResults r = lc.search(buildBaseDN(profile), LDAPConnection.SCOPE_SUB, filter, null, false); while (r.hasMore()) { try { LDAPEntry entry = r.next(); logger.debug("kraken-ldap: fetch org unit entry [{}]", entry.getDN()); ous.add(new LdapOrgUnit(entry)); } catch (LDAPReferralException e) { } } } catch (Exception e) { logger.error("kraken-ldap: cannot fetch domain users"); throw new IllegalStateException(e); } finally { try { if (lc.isConnected()) lc.disconnect(); } catch (LDAPException e) { logger.error("kraken ldap: disconnect failed", e); } } return ous; } @Override public boolean verifyPassword(LdapProfile profile, String uid, String password) { return verifyPassword(profile, uid, password, 0); } @Override public boolean verifyPassword(LdapProfile profile, String uid, String password, int timeout) { boolean bindStatus = false; if (password == null || password.isEmpty()) return false; LDAPConnection lc = openLdapConnection(profile, timeout); try { String filter = null; if (profile.getServerType() == LdapServerType.ActiveDirectory) { filter = "(sAMAccountName=" + uid + ")"; } else { filter = buildUserFilter(profile, uid); } String baseDn = buildBaseDN(profile); LDAPSearchResults r = lc.search(baseDn, LDAPConnection.SCOPE_SUB, filter, null, false); bindStatus = true; // query for verification LDAPEntry entry = r.next(); logger.trace("kraken ldap: verify password for {}", entry); // try bind logger.trace("kraken ldap: trying to bind using dn [{}]", entry.getDN()); lc.bind(LDAPConnection.LDAP_V3, entry.getDN(), password.getBytes("utf-8")); return true; } catch (Exception e) { if (!bindStatus) throw new IllegalArgumentException("check ldap profile: " + profile.getName(), e); return false; } finally { if (lc.isConnected()) { try { lc.disconnect(); } catch (LDAPException e) { logger.error("kraken ldap: disconnect failed", e); } } } } private String buildUserFilter(LdapProfile profile, String uid) { String filter; String idAttr = "uid"; if (profile.getIdAttr() != null) idAttr = profile.getIdAttr(); filter = "(" + idAttr + "=" + uid + ")"; return filter; } @Override public void testLdapConnection(LdapProfile profile, Integer timeout) { if (timeout == null) timeout = DEFAULT_TIMEOUT; LDAPConnection conn = null; try { conn = openLdapConnection(profile, timeout); } finally { if (conn != null && conn.isConnected()) try { conn.disconnect(); } catch (LDAPException e) { } } } @Override public void changePassword(LdapProfile profile, String uid, String newPassword) { changePassword(profile, uid, newPassword, DEFAULT_TIMEOUT); } @Override public void changePassword(LdapProfile profile, String uid, String newPassword, int timeout) { // newPassword null check if (newPassword == null || newPassword.isEmpty()) throw new IllegalArgumentException("password should be not null and not empty"); // connection server LDAPConnection lc = openLdapConnection(profile, timeout); try { // set filter String filter = "(sAMAccountName=" + uid + ")"; if (profile.getServerType() != LdapServerType.ActiveDirectory) filter = buildUserFilter(profile, uid); String baseDn = buildBaseDN(profile); LDAPSearchResults r = lc.search(baseDn, LDAPConnection.SCOPE_SUB, filter, null, false); // query for verification LDAPEntry entry = r.next(); logger.trace("kraken ldap: change password for {}", entry); // set mod LDAPModification mod = null; if (profile.getServerType() == LdapServerType.ActiveDirectory) { // ActiveDirectory - newPassword enclosed in quotation marks and // UTF-16LE encoding byte[] quotedPasswordBytes = null; String tmpPassword = ldapPasswordEscape(newPassword); try { String quotedPassword = '"' + tmpPassword + '"'; quotedPasswordBytes = quotedPassword.getBytes("UTF-16LE"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } mod = new LDAPModification(LDAPModification.REPLACE, new LDAPAttribute("unicodePwd", quotedPasswordBytes)); logger.debug("kraken ldap: active directory modify request [{}] for dn [{}]", mod.toString(), entry.getDN()); } else { mod = new LDAPModification(LDAPModification.REPLACE, new LDAPAttribute("userPassword", newPassword)); logger.debug("kraken ldap: sun one modify request [{}] for dn [{}]", mod.toString(), entry.getDN()); } // modify lc.modify(entry.getDN(), mod); } catch (LDAPException e) { throw new IllegalStateException("cannot change password, profile=" + profile.getName() + ", uid=" + uid, e); } finally { if (lc.isConnected()) { try { lc.disconnect(); } catch (LDAPException e) { logger.error("kraken ldap: disconnect failed", e); } } } } public static String ldapPasswordEscape(String s) { for (int i = 0; i < s.length(); i++) { s.replaceAll("\"", "\\\""); } return s; } @Override public LDAPConnection openLdapConnection(LdapProfile profile, Integer timeout) { if (profile.getDc() == null) throw new IllegalArgumentException("ldap domain controller should be not null"); if (profile.getPort() == null) throw new IllegalArgumentException("ldap port should be not null"); if (profile.getAccount() == null) throw new IllegalArgumentException("ldap account should be not null"); if (profile.getPassword() == null) throw new IllegalArgumentException("ldap password should be not null"); try { X509Certificate cert = profile.getX509Certificate(); KeyStore ks = null; if (cert != null) { ks = KeyStore.getInstance("JKS"); ks.load(null, null); ks.setCertificateEntry("mykey", cert); } if (ks != null) { SSLContext ctx = SSLContext.getInstance("SSL"); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(ks); ctx.init(null, tmf.getTrustManagers(), new SecureRandom()); LDAPConnection.setSocketFactory(new LDAPJSSESecureSocketFactory(ctx.getSocketFactory())); } else LDAPConnection.setSocketFactory(new JLDAPSocketFactory(timeout)); logger.trace("kraken ldap: connect to {}:{}, user [{}]", new Object[] { profile.getDc(), profile.getPort(), profile.getAccount() }); LDAPConnection conn = new LDAPConnection(); conn.connect(profile.getDc(), profile.getPort()); conn.bind(LDAPConnection.LDAP_V3, profile.getAccount(), profile.getPassword().getBytes("utf-8")); return conn; } catch (UnsupportedEncodingException e) { logger.error("kraken ldap: JVM unsupported utf-8 encoding"); throw new IllegalStateException("invalid profile [" + profile.getName() + "]", e); } catch (Exception e) { logger.error("kraken ldap: ldap profile [" + profile.getName() + "] connection failed", e); throw new IllegalStateException("invalid profile [" + profile.getName() + "]", e); } } private String buildBaseDN(LdapProfile profile) { if (profile.getBaseDn() != null) return profile.getBaseDn(); String domain = profile.getDc(); StringTokenizer t = new StringTokenizer(domain, "."); String dn = ""; int i = 0; while (t.hasMoreTokens()) { if (i++ != 0) dn += ","; dn += "dc=" + t.nextToken(); } return dn; } private static class JLDAPSocketFactory implements LDAPSocketFactory { private Integer timeout; public JLDAPSocketFactory(Integer timeout) { this.timeout = timeout; } @Override public Socket createSocket(String host, int port) throws IOException, UnknownHostException { Socket socket = new Socket(host, port); if (timeout != null) socket.setSoTimeout(timeout); return socket; } } }