/** * Fetches one specific LDAP Person from the repository * @param dn the Person-specific DN * @return the LDAP Person at the specified DN, or <code>null</code> if * not found. */ public LdapPerson fetchOne(String dn) throws InterruptedNamingException { log.entering("LdapServer", "fetchOne", new Object[] { nickName, dn }); Set<LdapPerson> results = this.search(dn, userFilter, attributes.split(","), false); if (results.size() == 0) { log.exiting("LdapServer", "fetchOne", 0); return null; } else if (results.size() == 1) { LdapPerson person = results.iterator().next(); log.exiting("LdapServer", "fetchOne", person); if (person == null) { throw new NullPointerException("Null LdapPerson found at " + dn); } else { return person; } } else { log.exiting("LdapServer", "fetchOne", 2); throw new IllegalArgumentException("More than one person found at " + dn + " : " + results.size() + " results."); } }
@Test public void testFetchOneNullLdapPersonResult() throws Exception { MockLdapContext ldapContext = new MockLdapContext(); LdapServer ldapServer = new LdapServer("localhost", "nickname", "ou=basedn", "userFilter", "attr1,cn,dn" /* attributes */, 1000 /* traversalRate */, "dn={dn}, cn={cn}", ldapContext) { @Override protected Set<LdapPerson> search(String baseDN, String filter, String[] attributes, boolean validateAttributes) throws InterruptedNamingException { return Sets.newHashSet((LdapPerson) null); } }; thrown.expect(NullPointerException.class); thrown.expectMessage("Null LdapPerson found at ou=basedn"); LdapPerson fetched = ldapServer.fetchOne("ou=basedn"); }
/** * Retrieves a PooledConnection from this list of connections. * Use an existing one if one is idle, or create one if the list's * max size hasn't been reached. If max size has been reached, wait * for a PooledConnection to be returned, or one to be removed (thus * not reaching the max size any longer). * * @param timeout if > 0, msec to wait until connection is available * @param factory creates the PooledConnection if one needs to be created * * @return A non-null PooledConnection * @throws NamingException PooledConnection cannot be created, because this * thread was interrupted while it waited for an available connection, * or if it timed out while waiting, or the creation of a connection * resulted in an error. */ synchronized PooledConnection get(long timeout, PooledConnectionFactory factory) throws NamingException { PooledConnection conn; long start = (timeout > 0 ? System.currentTimeMillis() : 0); long waittime = timeout; d("get(): before"); while ((conn = getOrCreateConnection(factory)) == null) { if (timeout > 0 && waittime <= 0) { throw new CommunicationException( "Timeout exceeded while waiting for a connection: " + timeout + "ms"); } try { d("get(): waiting"); if (waittime > 0) { wait(waittime); // Wait until one is released or removed } else { wait(); } } catch (InterruptedException e) { throw new InterruptedNamingException( "Interrupted while waiting for a connection"); } // Check whether we timed out if (timeout > 0) { long now = System.currentTimeMillis(); waittime = timeout - (now - start); } } d("get(): after"); return conn; }
@VisibleForTesting void readEverythingFrom(AdServer server, boolean includeMembers) throws InterruptedNamingException { final String[] nonMemberAttributes = new String[] { "uSNChanged", "sAMAccountName", "objectGUID;binary", "objectSid;binary", "userPrincipalName", "primaryGroupId", "userAccountControl" }; final String[] allAttributes = Arrays.copyOf(nonMemberAttributes, nonMemberAttributes.length + 1); allAttributes[nonMemberAttributes.length] = "member"; log.log(Level.FINE, "Starting full crawl."); if (server.getGroupSearchBaseDN().equals(server.getUserSearchBaseDN())) { entities = server.search(server.getUserSearchBaseDN(), generateLdapQuery(server), /*deleted=*/ false, includeMembers ? allAttributes : nonMemberAttributes); } else { entities = server.search(server.getGroupSearchBaseDN(), generateGroupLdapQuery(server), /*deleted=*/ false, includeMembers ? allAttributes : nonMemberAttributes); entities.addAll(server.search(server.getUserSearchBaseDN(), generateUserLdapQuery(server), /*deleted=*/ false, nonMemberAttributes)); } // disabled groups handled later, in makeDefs() log.log(Level.FINE, "Ending full crawl - now starting processing."); processEntities(entities, server.getnETBIOSName(), server.getUserSearchBaseDN(), server.getGroupSearchBaseDN(), server.getUserSearchFilter(), server.getGroupSearchFilter()); }
/** * Do an AD search for only groups/users that have been updated since the * previous full or incremental search. * <p>If either <code>getDsServiceName()</code> or * <code>server.getInvocationID()</code> have changed, the cache is stale * and (only) a full crawl is done, to refresh the cache. If neither have * changed, then only groups/users that have a <code>uSNChanged</code> * attribute newer than the <code>previousHighestUSN</code> parameter are * retrieved and returned. * @param server the Active Directory server to query * @param previousServiceName last-crawled value of * <code>getDsServiceName()</code> * @param previousInvocationId last-crawled value of * <code>server.getInvocationID()</code> * @param previousHighestUSN last-crawled value of * <code>server.getHighestCommittedUSN()</code> * <code>previousHighestUSN</code>. * * @return all instances of <code>AdEntity</code> that are users/groups that * have a <code>uSNChanged</code> attribute newer than, or * <code>Collections.emptySet()</code> when the cache had been stale. */ @VisibleForTesting Set<AdEntity> readUpdatesFrom(AdServer server, String previousServiceName, String previousInvocationId, long previousHighestUSN) throws InterruptedNamingException { // TODO(myk): Determine whether adaptors should include code to get/set // last full sync time, and if exceeding some threshhold should force a // full crawl. String currentServiceName = server.getDsServiceName(); String currentInvocationId = server.getInvocationID(); long currentHighestUSN = server.getHighestCommittedUSN(); if (!currentServiceName.equals(previousServiceName)) { // only log a warning if previous service name was set to something if (previousServiceName != null) { log.log(Level.WARNING, "Directory Controller changed from {0} to {1} " + "-- performing full recrawl. Consider configuring AD server to" + " connect directly to FQDN address of domain controller for " + "partial updates support.", new Object[]{previousServiceName, currentServiceName}); } readEverythingFrom(server, /*includeMembers=*/ false); return Collections.emptySet(); } if (!currentInvocationId.equals(previousInvocationId)) { log.log(Level.WARNING, "Directory Controller {0} has been restored from backup. " + "Performing full recrawl.", currentServiceName); readEverythingFrom(server, /*includeMembers=*/ false); return Collections.emptySet(); } if (currentHighestUSN == previousHighestUSN) { log.log(Level.INFO, "No updates on server {0} -- no crawl invoked.", server); return Collections.emptySet(); } log.log(Level.INFO, "Attempting incremental crawl."); return incrementalCrawl(server, previousHighestUSN, currentHighestUSN); }
@Test public void testSearchThrowsInterruptedNamingException() throws NamingException { AdServer adServer = helperSearchThrowsNamingException( new InterruptedNamingException("foo")); // expect exception to be thrown -- hence, we use try try { Set<AdEntity> resultSet = adServer.search("baseDN", "" /* filter */, false, new String[] { "cn", "primaryGroupId", "objectGUID;binary" }); fail("Did not catch expected exception."); } catch (InterruptedNamingException ine) { assertEquals("foo", ine.getMessage()); } }
@Override void readEverythingFrom(AdServer server, boolean unused) throws InterruptedNamingException { try { Thread.sleep(BRIEF_DELAY_IN_MILLISECONDS * 2); } catch (InterruptedException ex) { throw new InterruptedNamingException(ex.getMessage()); } ranFullCrawl = true; }
@Override Set<AdEntity> incrementalCrawl(AdServer server, long previousHighestUSN, long currentHighestUSN) throws InterruptedNamingException { try { Thread.sleep(BRIEF_DELAY_IN_MILLISECONDS * 2); } catch (InterruptedException ex) { throw new InterruptedNamingException(ex.getMessage()); } ranIncrementalCrawl = true; return Collections.emptySet(); }
/** * Searches LDAP repository and creates LdapPerson on each result found * @return list of entities found */ public Set<LdapPerson> scanAll() throws InterruptedNamingException { //TODO(myk): support incremental scan by also allowing an attribute for // last modification time // Each time we scan all, we update a Status indicator on the Dashboard // to let the admin know if there is some desired Attribute that is not // being fetched. Set<LdapPerson> results = this.search(baseDN, userFilter, attributes.split(","), true); fullScanCompleted = true; return results; }
@Override public void getDocContent(Request req, Response resp) throws IOException { log.entering("LdapAdaptor", "getDocContent", new Object[] {req, resp}); DocId id = req.getDocId(); ParsedDocId parsed = parseDocId(id); if (parsed == null || !id.equals(makeDocId(parsed.serverNumber, parsed.dn))) { log.warning(id + " is not a valid id generated by this adaptor."); resp.respondNotFound(); return; } LdapPerson fetched; try { LdapServer server = servers.get(parsed.serverNumber); fetched = server.fetchOne(parsed.dn); if (null == fetched) { log.finer("No results found for DN " + parsed.dn + ""); resp.respondNotFound(); log.exiting("LdapAdaptor", "getDocContent", 0); return; } for (Entry<String, String> metadatum : fetched.asMetadata().entrySet()) { resp.addMetadata(metadatum.getKey(), metadatum.getValue()); } InputStream input = new ByteArrayInputStream(fetched.asDoc( server.getDisplayTemplate()).getBytes(CHARSET)); resp.setContentType("text/html; charset=" + CHARSET.name() + ""); IOHelper.copyStream(input, resp.getOutputStream()); log.exiting("LdapAdaptor", "getDocContent", 1); } catch (InterruptedNamingException e) { log.exiting("LdapAdaptor", "getDocContent", 2); throw new IOException(e); } }
@Test public void testFetchOneMoreThanOneResult() throws Exception { MockLdapContext ldapContext = new MockLdapContext(); LdapServer ldapServer = new LdapServer("localhost", "nickname", "ou=basedn", "userFilter", "attr1,cn,dn" /* attributes */, 1000 /* traversalRate */, "dn={dn}, cn={cn}", ldapContext) { @Override protected Set<LdapPerson> search(String baseDN, String filter, String[] attributes, boolean validateAttributes) throws InterruptedNamingException { SearchResult sr1 = new SearchResult("user 1", "user 1", new BasicAttributes()); sr1.getAttributes().put("cn", "user1"); sr1.setNameInNamespace("cn=user1"); LdapPerson user1 = new LdapPerson(sr1); SearchResult sr2 = new SearchResult("user 2", "user 2", new BasicAttributes()); sr2.getAttributes().put("cn", "user2"); sr2.setNameInNamespace("cn=user2"); LdapPerson user2 = new LdapPerson(sr2); return Sets.newHashSet(user1, user2); } }; thrown.expect(IllegalArgumentException.class); thrown.expectMessage("More than one person found at ou=basedn"); LdapPerson fetched = ldapServer.fetchOne("ou=basedn"); }
@Test public void testGetDocContentWhenFetchOneThrowsException() throws Exception { thrown.expect(IOException.class); final MockLdapContext ldapContext = new MockLdapContext(); final LdapAdaptor ldapAdaptor = new LdapAdaptor() { @Override LdapServer newLdapServer(String host, String nick, Method method, int port, String principal, String passwd, String baseDN, String userFilter, String attributes, int docsPerMinute, long ldapTimeoutInMillis, String displayTemplate) { return new LdapServer(host, nick, baseDN, userFilter, attributes, docsPerMinute, displayTemplate, ldapContext) { @Override void recreateLdapContext() { // leave ldapContext unchanged } @Override public LdapPerson fetchOne(String dn) throws InterruptedNamingException { throw new InterruptedNamingException("INE"); } }; } }; AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher(); Map<String, String> configEntries = defaultConfigEntriesForOneServer(); pushGroupDefinitions(ldapAdaptor, configEntries, pusher, /*fullPush=*/ true, /*init=*/ true); // the above calls LdapAdaptor.init() with the specified config. assertEquals(0, pusher.getRecords().size()); String expectedDocId = "server=0/cn=name\\ under,basedn"; MockResponse response = new MockResponse(); ldapAdaptor.getDocContent(new MockRequest(new DocId(expectedDocId)), response); }
/** * Reads a reply; waits until one is ready. */ BerDecoder readReply(LdapRequest ldr) throws IOException, NamingException { BerDecoder rber; boolean waited = false; while (((rber = ldr.getReplyBer()) == null) && !waited) { try { // If socket closed, don't even try synchronized (this) { if (sock == null) { throw new ServiceUnavailableException(host + ":" + port + "; socket closed"); } } synchronized (ldr) { // check if condition has changed since our last check rber = ldr.getReplyBer(); if (rber == null) { if (readTimeout > 0) { // Socket read timeout is specified // will be woken up before readTimeout only if reply is // available ldr.wait(readTimeout); waited = true; } else { // no timeout is set so we wait infinitely until // a response is received // https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-ldap.html#PROP ldr.wait(); } } else { break; } } } catch (InterruptedException ex) { throw new InterruptedNamingException( "Interrupted during LDAP operation"); } } if ((rber == null) && waited) { abandonRequest(ldr, null); throw new NamingException("LDAP response read timed out, timeout used:" + readTimeout + "ms." ); } return rber; }
@VisibleForTesting Set<AdEntity> incrementalCrawl(AdServer server, long previousHighestUSN, long currentHighestUSN) throws InterruptedNamingException { log.log(Level.FINE, "Starting incremental crawl."); final String[] attributes = new String[] { "uSNChanged", "member", "sAMAccountName", "objectGUID;binary", "objectSid;binary", "userPrincipalName", "primaryGroupId", "userAccountControl" }; Set<AdEntity> newOrModifiedEntities; String newEntryQuery = "(uSNChanged>=" + (previousHighestUSN + 1) + ")"; if (server.getGroupSearchBaseDN().equals(server.getUserSearchBaseDN())) { newOrModifiedEntities = server.search(server.getUserSearchBaseDN(), "(&" + newEntryQuery + generateLdapQuery(server) + ")", /*deleted=*/ false, attributes); } else { newOrModifiedEntities = server.search(server.getGroupSearchBaseDN(), "(&" + newEntryQuery + generateGroupLdapQuery(server) + ")", /*deleted=*/ false, attributes); newOrModifiedEntities.addAll(server.search(server.getUserSearchBaseDN(), "(&" + newEntryQuery + generateUserLdapQuery(server) + ")", /*deleted=*/ false, attributes)); } // disabled groups handled later, in makeDefs() log.log(Level.FINE, "Ending incremental crawl - now starting " + "processing."); // remove previous value of newly-seen entity, if found for (AdEntity e : newOrModifiedEntities) { AdEntity oldEntity = bySid.get(e.getSid()); if (oldEntity != null) { // b/18028678: remove user from old primary group (if needed) String oldPrimaryGroupSid = oldEntity.getPrimaryGroupSid(); String newPrimaryGroupSid = e.getPrimaryGroupSid(); if (oldPrimaryGroupSid != null && !oldPrimaryGroupSid.equals(newPrimaryGroupSid)) { AdEntity oldPrimaryGroup = bySid.get(oldPrimaryGroupSid); if (oldPrimaryGroup == null) { log.log(Level.WARNING, "Primary group [{0}] for user [{1}] detected in previous " + "crawl not found during current crawl. Not updating " + "group [{0}].", new Object[]{oldPrimaryGroupSid, oldEntity}); } else if (primaryMembers.containsKey(oldPrimaryGroup)) { log.log(Level.FINER, "Removing entity [{0}] from primary members of group [{1}].", new Object[]{oldEntity, oldPrimaryGroup}); primaryMembers.get(oldPrimaryGroup).remove(oldEntity.getDn()); } else { log.log(Level.WARNING, "Could not remove user [{0}] from group [{1}], as that " + "group''s membership was not cached.", new Object[]{oldEntity, oldPrimaryGroup}); } } entities.remove(oldEntity); if (oldEntity.isGroup()) { members.remove(oldEntity); // before removing the oldEntity from the primaryMembers HashMap, // copy its elements (if not null) to the new entry for that group. if (null != primaryMembers.get(oldEntity)) { primaryMembers.put(e, primaryMembers.get(oldEntity)); } primaryMembers.remove(oldEntity); } byDn.remove(oldEntity.getDn()); wellKnownMembership.get(everyone).remove(oldEntity.getDn()); } } // add the new-or-modified entries to our catalog entities.addAll(newOrModifiedEntities); processEntities(newOrModifiedEntities, server.getnETBIOSName(), server.getUserSearchBaseDN(), server.getGroupSearchBaseDN(), server.getUserSearchFilter(), server.getGroupSearchFilter()); log.log(Level.FINE, "Ending incremental crawl."); return newOrModifiedEntities; }
/** * Reads a reply; waits until one is ready. */ BerDecoder readReply(LdapRequest ldr) throws IOException, NamingException { BerDecoder rber; boolean waited = false; while (((rber = ldr.getReplyBer()) == null) && !waited) { try { // If socket closed, don't even try synchronized (this) { if (sock == null) { throw new ServiceUnavailableException(host + ":" + port + "; socket closed"); } } synchronized (ldr) { // check if condition has changed since our last check rber = ldr.getReplyBer(); if (rber == null) { if (readTimeout > 0) { // Socket read timeout is specified // will be woken up before readTimeout only if reply is // available ldr.wait(readTimeout); waited = true; } else { ldr.wait(15 * 1000); // 15 second timeout } } else { break; } } } catch (InterruptedException ex) { throw new InterruptedNamingException( "Interrupted during LDAP operation"); } } if ((rber == null) && waited) { removeRequest(ldr); throw new NamingException("LDAP response read timed out, timeout used:" + readTimeout + "ms." ); } return rber; }
@Test public void testGetDocIdsNamingException() throws Exception { final MockLdapContext ldapContext = new MockLdapContext(); final LdapAdaptor ldapAdaptor = new LdapAdaptor() { @Override LdapServer newLdapServer(String host, String nick, Method method, int port, String principal, String passwd, String baseDN, String userFilter, String attributes, int docsPerMinute, long ldapTimeoutInMillis, String displayTemplate) { return new LdapServer(host, nick, baseDN, userFilter, attributes, docsPerMinute, displayTemplate, ldapContext) { @Override void recreateLdapContext() { // leave ldapContext unchanged } @Override public void ensureConnectionIsCurrent() throws CommunicationException, NamingException { // do nothing } @Override public Set<LdapPerson> scanAll() throws InterruptedNamingException { throw new InterruptedNamingException("INE"); } }; } }; AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher(); Map<String, String> configEntries = defaultConfigEntriesForOneServer(); try { pushGroupDefinitions(ldapAdaptor, configEntries, pusher, /*fullPush=*/ true, /*init=*/ true); fail("Expected an IOException"); } catch (IOException ioe) { assertEquals("Could not get entities from the following server(s): " + "localhost", ioe.getMessage()); assertTrue(ioe.getCause() instanceof NamingException); NamingException ne = (NamingException) ioe.getCause(); assertTrue(ne.getMessage().contains("INE")); } assertEquals(0, pusher.getRecords().size()); }