package com.google.enterprise.adaptor.sharepoint; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.io.IOException; import java.util.Hashtable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javax.naming.CommunicationException; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.LdapContext; /* * ActiveDirectory client to convert SID to corresponding domain\\accountname * format. */ public class ActiveDirectoryClient { private static final Logger log = Logger.getLogger(ActiveDirectoryClient.class.getName()); private final ADServer adServer; private final LoadingCache<String, String> cache = CacheBuilder.newBuilder() // Cache will auto expire in 30 minutes after initial write or update. .expireAfterWrite(30, TimeUnit.MINUTES) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws IOException { log.log(Level.FINE, "Performing SID lookup for {0}", key); String resolved = adServer.getUserAccountBySid(key); log.log(Level.FINE, "SID {0} resolved to {1}", new Object[] {key, resolved}); if (resolved == null) { // CacheBuilder doesn't allow to return null here. // Throwing IOEXception will result in repeated attempts // to resolve unknown SID. To avoid repeated attempts to resolve // SID, returning empty string here. log.log(Level.WARNING, "Could not resolve SID {0}." + "Returning empty string", key); return ""; } return resolved; } }); public String getUserAccountBySid(String sid) throws IOException { Preconditions.checkNotNull(sid); Preconditions.checkArgument(sid.startsWith("S-1-") || sid.startsWith("s-1-"), "Invalid SID: %s", sid); try { String domainSid = sid.substring(0, sid.lastIndexOf("-")); String domain = cache.get(domainSid); if ("".equals(domain)) { log.log(Level.WARNING, "Could not resolve domain for domain SID {0}." + " Returning null as account name for SID {1}", new Object[] {domainSid, sid}); return null; } String accountname = cache.get(sid); if ("".equals(accountname)) { log.log(Level.WARNING, "Could not resolve accountname for SID {0}." + " Returning null as account name.", sid); return null; } String logonName = domain + "\\" + accountname; log.log(Level.FINE, "Returning logon name as {0} for SID {1}", new Object[] {logonName, sid}); return logonName; } catch (ExecutionException e) { throw new IOException(e); } } public static ActiveDirectoryClient getInstance(String host, int port, String username, String password, String method) throws IOException { return new ActiveDirectoryClient(new ADServerImpl( host, port, username, password, method)); } @VisibleForTesting ActiveDirectoryClient(ADServer adServer) throws IOException { Preconditions.checkNotNull(adServer); this.adServer = adServer; adServer.start(); } interface ADServer { /* * Resolves input SID to user account name. Returns null if SID is not * available. */ public String getUserAccountBySid(String sid) throws IOException; /* * Initializes LDAP Context and verifies that successful connection can * established with AD server using provided connection properties. */ public void start() throws IOException; } static class ADServerImpl implements ADServer { private final String host; private final int port; private final String username; private final String password; private final String protocol; private final SearchControls searchCtls; private final String[] attributes = new String[] { "sAMAccountName", "name" }; private volatile LdapContext context; private String dn; ADServerImpl(String host, int port, String username, String password, String method) { Preconditions.checkNotNull(host); Preconditions.checkArgument(!("".equals(host))); Preconditions.checkNotNull(username); Preconditions.checkArgument(!("".equals(username))); Preconditions.checkNotNull(password); Preconditions.checkArgument(!("".equals(password))); Preconditions.checkArgument(port > 0); this.host = host; this.port = port; this.username = username; this.password = password; this.protocol = "ssl".equalsIgnoreCase(method) ? "ldaps" : "ldap"; this.searchCtls = new SearchControls(); searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); searchCtls.setReturningAttributes(attributes); } @Override public String getUserAccountBySid(String sid) throws IOException { Preconditions.checkNotNull(sid); Preconditions.checkArgument(sid.startsWith("S-1-") || sid.startsWith("s-1-"), "Invalid SID: %s", sid); refreshConnection(); String query = String.format("(objectSid=%s)", sid); String searchBase = (port == 389 || port == 636) ? dn : ""; log.log(Level.FINE, "Querying host {0} on port {1} with query {2} and" + " search base {3}", new Object[] {host, port, query, searchBase}); try { NamingEnumeration<SearchResult> results = context.search(searchBase, query, searchCtls); if (!results.hasMoreElements()) { log.log(Level.WARNING, "No result found on host {0} on port {1}" + " with query {2} and search base {3}. Returing null.", new Object[] {host, port, query, searchBase}); return null; } SearchResult sr = results.next(); Attributes attrbs = sr.getAttributes(); // use sAMAccountName when available String sAMAccountName = (String) getAttribute(attrbs, "sAMAccountName"); if (!Strings.isNullOrEmpty(sAMAccountName)) { return sAMAccountName; } log.log(Level.FINER, "sAMAccountName is null for SID {0}. This might" + " be domain object.", sid); String name = (String) getAttribute(attrbs, "name"); if (!Strings.isNullOrEmpty(name)) { return name; } log.log(Level.WARNING, "name is null for SID {0}. Returing null.", sid); return null; } catch (NamingException ne) { throw new IOException(ne); } } @Override public void start() throws IOException { initializeContext(); refreshConnection(); } private synchronized void initializeContext() throws IOException { // Check if current context is still useful by calling // context.getAttributes. if (context != null) { try { context.getAttributes(""); return; } catch (NamingException ignore) { ignore = null; } } Hashtable<String, String> env = new Hashtable<String, String>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put("com.sun.jndi.ldap.read.timeout", "90000"); env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, username); env.put(Context.SECURITY_CREDENTIALS, password); String ldapUrl = String.format("%s://%s:%d", protocol, host, port); env.put(Context.PROVIDER_URL, ldapUrl); try { context = new InitialLdapContext(env, null); } catch (NamingException ne) { throw new IOException(ne); } } private void refreshConnection() throws IOException { refreshConnection(true); } private void refreshConnection(boolean retry) throws IOException { if (context == null) { throw new IOException("LDAP Context not initialized."); } try { Attributes attributes = context.getAttributes(""); dn = (String) getAttribute(attributes, "defaultNamingContext"); } catch (CommunicationException ce) { if (retry) { log.log(Level.INFO, "Error refreshing LDAP connection to host {0}" + " on port {1} for SID lookup. Retrying.", new Object[] {host, port}); initializeContext(); refreshConnection(false); } else { throw new IOException(ce); } } catch (NamingException ne) { if (retry) { log.log(Level.INFO, "Error refreshing LDAP connection to host {0}" + " on port {1} for SID lookup. Retrying.", new Object[] {host, port}); initializeContext(); refreshConnection(false); } else { throw new IOException(ne); } } } private Object getAttribute(Attributes attributes, String name) throws NamingException { Attribute attribute = attributes.get(name); if (attribute != null) { return attribute.get(0); } else { return null; } } } }