| // Copyright 2013 Google Inc. All Rights Reserved. |
| // |
| // 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 com.google.enterprise.adaptor.ad; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.enterprise.adaptor.AbstractAdaptor; |
| import com.google.enterprise.adaptor.AdaptorContext; |
| import com.google.enterprise.adaptor.Config; |
| import com.google.enterprise.adaptor.DocIdPusher; |
| import com.google.enterprise.adaptor.GroupPrincipal; |
| import com.google.enterprise.adaptor.PollingIncrementalLister; |
| import com.google.enterprise.adaptor.Principal; |
| import com.google.enterprise.adaptor.Request; |
| import com.google.enterprise.adaptor.Response; |
| import com.google.enterprise.adaptor.UserPrincipal; |
| |
| import java.io.IOException; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.concurrent.locks.ReentrantLock; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| import javax.naming.InterruptedNamingException; |
| import javax.naming.NamingException; |
| |
| /** Adaptor for Active Directory. */ |
| public class AdAdaptor extends AbstractAdaptor |
| implements PollingIncrementalLister { |
| private static final Logger log |
| = Logger.getLogger(AdAdaptor.class.getName()); |
| private static final boolean CASE_SENSITIVITY = false; |
| /** |
| * Only one crawl (full or incremental) is done at a time, however: |
| * when a full crawl is invoked, we wait until the lock is available; |
| * when an incremental crawl is invoked, we immediately return if the lock |
| * isn't available. |
| */ |
| private final ReentrantLock mutex = new ReentrantLock(); |
| |
| private String namespace; |
| private String defaultUser; // used if an AD doesn't override |
| private String defaultPassword; // likewise |
| private List<AdServer> servers = new ArrayList<AdServer>(); |
| private Map<String, String> localizedStrings; |
| private boolean feedBuiltinGroups; |
| private GroupCatalog lastCompleteGroupCatalog = null; |
| private String ldapTimeoutInMillis; |
| |
| @Override |
| public void initConfig(Config config) { |
| config.addKey("ad.servers", null); |
| config.addKey("adaptor.namespace", "Default"); |
| config.addKey("ad.defaultUser", ""); |
| config.addKey("ad.defaultPassword", ""); |
| config.addKey("ad.localized.Everyone", "Everyone"); |
| config.addKey("ad.localized.NTAuthority", "NT Authority"); |
| config.addKey("ad.localized.Interactive", "Interactive"); |
| config.addKey("ad.localized.AuthenticatedUsers", "Authenticated Users"); |
| config.addKey("ad.localized.Builtin", "BUILTIN"); |
| config.addKey("ad.feedBuiltinGroups", "false"); |
| config.addKey("ad.ldapReadTimeoutSecs", "90"); |
| } |
| |
| @Override |
| public void init(AdaptorContext context) throws Exception { |
| Config config = context.getConfig(); |
| namespace = config.getValue("adaptor.namespace"); |
| log.config("common namespace: " + namespace); |
| defaultUser = config.getValue("ad.defaultUser"); |
| defaultPassword = context.getSensitiveValueDecoder().decodeValue( |
| config.getValue("ad.defaultPassword")); |
| feedBuiltinGroups = Boolean.parseBoolean( |
| config.getValue("ad.feedBuiltinGroups")); |
| ldapTimeoutInMillis = parseLdapTimeoutInMillis( |
| config.getValue("ad.ldapReadTimeoutSecs")); |
| // register for incremental pushes |
| context.setPollingIncrementalLister(this); |
| List<Map<String, String>> serverConfigs |
| = config.getListOfConfigs("ad.servers"); |
| servers.clear(); // in case init gets called again |
| for (Map<String, String> singleServerConfig : serverConfigs) { |
| String host = singleServerConfig.get("host"); |
| int port = 389; |
| if (singleServerConfig.containsKey("port")) { |
| port = Integer.parseInt(singleServerConfig.get("port")); |
| } |
| Method method = Method.STANDARD; |
| if (singleServerConfig.containsKey("method")) { |
| String methodStr = singleServerConfig.get("method").toLowerCase(); |
| if ("ssl".equals(methodStr)) { |
| method = Method.SSL; |
| } else if (!"standard".equals(methodStr)) { |
| throw new IllegalArgumentException("invalid method: " + methodStr); |
| } |
| } |
| String principal = singleServerConfig.get("user"); |
| if (null == principal) { |
| principal = defaultUser; |
| } |
| if (principal.isEmpty()) { |
| throw new IllegalStateException("user not specified for host " + host); |
| } |
| String passwd = context.getSensitiveValueDecoder().decodeValue( |
| singleServerConfig.get("password")); |
| if (null == passwd) { |
| passwd = defaultPassword; |
| } |
| if (passwd.isEmpty()) { |
| throw new IllegalStateException("password not specified for host " |
| + host); |
| } |
| AdServer adServer = newAdServer(method, host, port, principal, passwd, |
| ldapTimeoutInMillis); |
| adServer.initialize(); |
| servers.add(adServer); |
| Map<String, String> dup = new TreeMap<String, String>(singleServerConfig); |
| dup.put("password", "XXXXXX"); // hide password |
| log.log(Level.CONFIG, "AD server spec: {0}", dup); |
| } |
| localizedStrings = config.getValuesWithPrefix("ad.localized."); |
| } |
| |
| /** |
| * This method exists specifically to be overwritten in the test class, in |
| * order to inject a version of AdServer that uses mocks. |
| */ |
| @VisibleForTesting |
| AdServer newAdServer(Method method, String host, int port, |
| String principal, String passwd, String ldapTimeoutInMillis) { |
| return new AdServer(method, host, port, principal, passwd, |
| ldapTimeoutInMillis); |
| } |
| |
| private static String parseLdapTimeoutInMillis(String timeInSeconds) { |
| if (timeInSeconds.equals("0") || timeInSeconds.trim().equals("")) { |
| timeInSeconds = "90"; |
| log.log(Level.CONFIG, "ad.ldapReadTimeoutSecs set to default of 90 sec."); |
| } |
| try { |
| return String.valueOf(1000L * Integer.parseInt(timeInSeconds)); |
| } catch (NumberFormatException e) { |
| throw new IllegalArgumentException("invalid ad.ldapReadTimeoutSecs: " + |
| timeInSeconds); |
| } |
| } |
| |
| /** This adaptor does not serve documents. */ |
| @Override |
| public void getDocContent(Request req, Response resp) throws IOException { |
| resp.respondNotFound(); |
| } |
| |
| /** Call default main for adaptors. */ |
| public static void main(String[] args) { |
| AbstractAdaptor.main(new AdAdaptor(), args); |
| } |
| |
| /** Crawls/pushes all groups from all AdServers. */ |
| |
| @Override |
| public void getDocIds(DocIdPusher pusher) throws InterruptedException, |
| IOException { |
| log.log(Level.FINER, "getDocIds invoked - waiting for lock."); |
| mutex.lock(); |
| try { |
| clearLastCompleteGroupCatalog(); |
| GroupCatalog cumulativeCatalog = makeFullCatalog(); |
| // all servers were able to successfully populate the catalog: do a push |
| cumulativeCatalog.resolveForeignSecurityPrincipals(); |
| Map<GroupPrincipal, List<Principal>> groups = |
| cumulativeCatalog.makeDefs(); |
| pusher.pushGroupDefinitions(groups, CASE_SENSITIVITY); |
| // TODO(myk): clear membership information from cache - retain only the |
| // entities in bySid, byDn, and wellKnownMembership. |
| lastCompleteGroupCatalog = cumulativeCatalog; |
| } finally { |
| mutex.unlock(); |
| log.log(Level.FINE, "getDocIds ending - lock released."); |
| } |
| } |
| |
| private GroupCatalog makeFullCatalog() throws InterruptedException, |
| IOException { |
| GroupCatalog cumulativeCatalog = new GroupCatalog(localizedStrings, |
| namespace, feedBuiltinGroups); |
| for (AdServer server : servers) { |
| try { |
| server.ensureConnectionIsCurrent(); |
| GroupCatalog catalog = new GroupCatalog(localizedStrings, namespace, |
| feedBuiltinGroups); |
| catalog.readEverythingFrom(server); |
| cumulativeCatalog.add(catalog); |
| } catch (NamingException ne) { |
| String host = server.getHostName(); |
| throw new IOException("could not get entities from " + host, ne); |
| } |
| } |
| return cumulativeCatalog; |
| } |
| |
| /** |
| * Attempts an incremental push of updated groups from all AdServers. |
| * <p> |
| * When a server cannot do an incremental push, it does a full crawl without |
| * doing a push afterwards -- this sets up its state so that subsequent |
| * incremental pushes can work. A future version of this method will do the |
| * full crawl ignoring the "member" attribute. |
| */ |
| @Override |
| public void getModifiedDocIds(DocIdPusher pusher) throws InterruptedException, |
| IOException { |
| if (!mutex.tryLock()) { |
| log.log(Level.FINE, "getModifiedDocIds could not acquire lock; " + |
| "will retry later."); |
| return; |
| } |
| try { |
| log.log(Level.FINE, "getModifiedDocIds starting - acquired lock."); |
| |
| for (AdServer server : servers) { |
| String previousServiceName = server.getDsServiceName(); |
| String previousInvocationId = server.getInvocationID(); |
| long previousHighestUSN = server.getHighestCommittedUSN(); |
| try { |
| server.ensureConnectionIsCurrent(); |
| // TODO(myk): combine each server's resulting Entities into one Set |
| lastCompleteGroupCatalog.readUpdatesFrom(server, previousServiceName, |
| previousInvocationId, previousHighestUSN); |
| } catch (NamingException ne) { |
| // invalidate the saved group catalog |
| clearLastCompleteGroupCatalog(); |
| String host = server.getHostName(); |
| throw new IOException("could not get entities from " + host, ne); |
| } |
| } |
| |
| // all servers were able to successfully update the catalog: do a push |
| lastCompleteGroupCatalog.resolveForeignSecurityPrincipals(); |
| Map<GroupPrincipal, List<Principal>> groups = |
| lastCompleteGroupCatalog.makeDefs(); |
| // TODO(myk): pass on (only) new/modified entities to resolve/makeDefs, |
| // so that we're only pushing the new/modified groups below. |
| pusher.pushGroupDefinitions(groups, CASE_SENSITIVITY); |
| // TODO(myk): clear membership information from cache - retain only the |
| // entities in bySid, byDn, and wellKnownMembership. |
| } finally { |
| mutex.unlock(); |
| log.log(Level.FINE, "getModifiedDocIds ending - lock released."); |
| } |
| } |
| |
| // don't expose the <code>lastCompleteGroupCatalog</code> field, but do allow |
| // tests to clear it |
| @VisibleForTesting |
| void clearLastCompleteGroupCatalog() { |
| lastCompleteGroupCatalog = null; |
| } |
| |
| // Space for all group info, organized in different ways |
| @VisibleForTesting |
| static class GroupCatalog { |
| Map<String, String> localizedStrings; |
| String namespace; |
| boolean feedBuiltinGroups; |
| Set<AdEntity> entities = new HashSet<AdEntity>(); |
| Map<AdEntity, Set<String>> members = new HashMap<AdEntity, Set<String>>(); |
| |
| Map<String, AdEntity> bySid = new HashMap<String, AdEntity>(); |
| Map<String, AdEntity> byDn = new HashMap<String, AdEntity>(); |
| Map<AdEntity, String> domain = new HashMap<AdEntity, String>(); |
| |
| final AdEntity everyone; |
| final AdEntity interactive; |
| final AdEntity authenticatedUsers; |
| final Map<AdEntity, Set<String>> wellKnownMembership; |
| |
| public GroupCatalog(Map<String, String> localizedStrings, String namespace, |
| boolean feedBuiltinGroups) { |
| this.localizedStrings = localizedStrings; |
| this.namespace = namespace; |
| this.feedBuiltinGroups = feedBuiltinGroups; |
| everyone = new AdEntity("S-1-1-0", |
| MessageFormat.format("CN={0}", |
| localizedStrings.get("Everyone"))); |
| interactive = new AdEntity("S-1-5-4", |
| MessageFormat.format("CN={0},DC={1}", |
| localizedStrings.get("Interactive"), |
| localizedStrings.get("NTAuthority"))); |
| authenticatedUsers = new AdEntity("S-1-5-11" , |
| MessageFormat.format("CN={0},DC={1}", |
| localizedStrings.get("AuthenticatedUsers"), |
| localizedStrings.get("NTAuthority"))); |
| wellKnownMembership = new HashMap<AdEntity, Set<String>>(); |
| wellKnownMembership.put(everyone, new TreeSet<String>()); |
| wellKnownMembership.put(interactive, new TreeSet<String>()); |
| wellKnownMembership.put(authenticatedUsers, new TreeSet<String>()); |
| |
| // To save space on GSA onboard groups database, we add "everyone" as a |
| // member to "Interactive" and "authenticated users" groups. |
| // Each user from domain will be added as member of "everyone" group |
| // and user will be indirect member for |
| // "Interactive" and "authenticated users" groups. |
| wellKnownMembership.get(interactive).add(everyone.getDn()); |
| wellKnownMembership.get(authenticatedUsers).add(everyone.getDn()); |
| |
| entities.add(everyone); |
| entities.add(interactive); |
| entities.add(authenticatedUsers); |
| |
| bySid.put(everyone.getSid(), everyone); |
| byDn.put(everyone.getDn(), everyone); |
| |
| bySid.put(interactive.getSid(), interactive); |
| byDn.put(interactive.getDn(), interactive); |
| domain.put(interactive, localizedStrings.get("NTAuthority")); |
| |
| bySid.put(authenticatedUsers.getSid(), authenticatedUsers); |
| byDn.put(authenticatedUsers.getDn(), authenticatedUsers); |
| domain.put(authenticatedUsers, localizedStrings.get("NTAuthority")); |
| } |
| |
| @VisibleForTesting |
| GroupCatalog(Map<String, String> localizedStrings, String namespace, |
| boolean feedBuiltinGroups, Set<AdEntity> entities, |
| Map<AdEntity, Set<String>> members, |
| Map<String, AdEntity> bySid, |
| Map<String, AdEntity> byDn, |
| Map<AdEntity, String> domain) { |
| this(localizedStrings, namespace, feedBuiltinGroups); |
| this.entities.clear(); |
| this.entities.addAll(entities); |
| this.members.putAll(members); |
| this.bySid.putAll(bySid); |
| this.byDn.putAll(byDn); |
| this.domain.putAll(domain); |
| } |
| |
| @VisibleForTesting |
| void readEverythingFrom(AdServer server) throws InterruptedNamingException { |
| // TODO(myk): Phase II: indicate whether or not this search should |
| // include members |
| log.log(Level.FINE, "Starting full crawl."); |
| // LDAP_MATCHING_RULE_BIT_AND = 1.2.840.113556.1.4.803 |
| // and ADS_GROUP_TYPE_SECURITY_ENABLED = 2147483648. |
| entities = server.search("(|(&(objectClass=group)" |
| + "(groupType:1.2.840.113556.1.4.803:=2147483648))" |
| + "(&(objectClass=user)(objectCategory=person)))", |
| /*deleted=*/ false, |
| new String[] { "uSNChanged", "sAMAccountName", "objectGUID;binary", |
| "objectSid;binary", "userPrincipalName", "primaryGroupId", |
| "member", "userAccountControl" }); |
| // disabled groups handled later, in makeDefs() |
| log.log(Level.FINE, "Ending full crawl - now starting processing."); |
| processEntities(entities, server.getnETBIOSName()); |
| } |
| |
| /** |
| * 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>. |
| */ |
| /* TODO(myk): add |
| * @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 |
| void 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); |
| return; |
| //TODO(myk): 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); |
| return; |
| //TODO(myk): return Collections.emptySet(); |
| } |
| if (currentHighestUSN == previousHighestUSN) { |
| log.log(Level.INFO, "No updates on server {0} -- no crawl invoked.", |
| server); |
| return; |
| //TODO(myk): return Collections.emptySet(); |
| } |
| log.log(Level.INFO, "Attempting incremental crawl."); |
| incrementalCrawl(server, previousHighestUSN, currentHighestUSN); |
| } |
| |
| private void processEntities(Set<AdEntity> entities, String nETBIOSName) { |
| log.log(Level.FINE, "received {0} entities from server", entities.size()); |
| for (AdEntity e : entities) { |
| bySid.put(e.getSid(), e); |
| byDn.put(e.getDn(), e); |
| // TODO(pjo): Have AdServer put domain into AdEntity during search |
| domain.put(e, e.getSid().startsWith("S-1-5-32-") ? |
| localizedStrings.get("Builtin") : nETBIOSName); |
| } |
| initializeMembers(entities); |
| resolvePrimaryGroups(entities); |
| log.log(Level.FINE, "Ending processing of {0} entities", entities.size()); |
| } |
| |
| @VisibleForTesting |
| Set<AdEntity> incrementalCrawl(AdServer server, long previousHighestUSN, |
| long currentHighestUSN) throws InterruptedNamingException { |
| log.log(Level.FINE, "Starting incremental crawl."); |
| // LDAP_MATCHING_RULE_BIT_AND = 1.2.840.113556.1.4.803 |
| // and ADS_GROUP_TYPE_SECURITY_ENABLED = 2147483648. |
| // TODO(myk): Phase II: indicate that this search should exclude members |
| Set<AdEntity> newOrModifiedEntities = server.search("(&(uSNChanged>=" |
| + (previousHighestUSN + 1) + ")(|(&(objectClass=group)" |
| + "(groupType:1.2.840.113556.1.4.803:=2147483648))" |
| + "(&(objectClass=user)(objectCategory=person))))", |
| /*deleted=*/ false, |
| new String[] { "uSNChanged", "sAMAccountName", "objectGUID;binary", |
| "objectSid;binary", "userPrincipalName", "primaryGroupId", |
| "member", "userAccountControl" }); |
| // 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) { |
| entities.remove(oldEntity); |
| byDn.remove(oldEntity.getDn()); |
| members.remove(oldEntity); |
| wellKnownMembership.get(everyone).remove(oldEntity.getDn()); |
| } |
| } |
| // add the new-or-modified entries to our catalog |
| entities.addAll(newOrModifiedEntities); |
| processEntities(newOrModifiedEntities, server.getnETBIOSName()); |
| log.log(Level.FINE, "Ending incremental crawl."); |
| return newOrModifiedEntities; |
| } |
| |
| /** |
| * Correctly specify each group's members in the "members" data store |
| */ |
| private void initializeMembers(Set<AdEntity> entities) { |
| for (AdEntity entity : entities) { |
| if (entity.isGroup()) { |
| members.put(entity, new TreeSet<String>(entity.getMembers())); |
| } |
| } |
| } |
| |
| /** |
| * Make sure that each non-group entity's "primary" group exists in bySid |
| * |
| * and contains that entity in the <code>members</code> data store. |
| */ |
| private void resolvePrimaryGroups(Set<AdEntity> entities) { |
| int nadds = 0; |
| int missingGroups = 0; |
| for (AdEntity e : entities) { |
| if (e.isGroup()) { |
| continue; |
| } |
| AdEntity user = e; |
| AdEntity primaryGroup = bySid.get(user.getPrimaryGroupSid()); |
| if (primaryGroup == null) { |
| missingGroups++; |
| log.log(Level.WARNING, |
| "Group {0} -- primary group for user {1} -- not found", |
| new Object[]{user.getPrimaryGroupSid(), user}); |
| continue; |
| } |
| if (!members.containsKey(primaryGroup)) { |
| members.put(primaryGroup, new TreeSet<String>()); |
| } |
| members.get(primaryGroup).add(user.getDn()); |
| wellKnownMembership.get(everyone).add(user.getDn()); |
| nadds++; |
| } |
| log.log(Level.FINE, "# primary groups: {0}", members.keySet().size()); |
| if (missingGroups > 0) { |
| log.log(Level.FINE, "# missing primary groups: {0}", missingGroups); |
| } |
| log.log(Level.FINE, "# users added to all primary groups: {0}", nadds); |
| } |
| |
| void resolveForeignSecurityPrincipals() { |
| //TODO(myk) Phase II: pass in only the entities just read in (for an |
| // incremental search to only consider those entities) |
| int nGroups = 0; |
| int nNullSid = 0; |
| int nNullResolution = 0; |
| int nResolved = 0; |
| for (AdEntity entity : entities) { |
| if (!entity.isGroup() || entity.isWellKnown()) { |
| continue; |
| } |
| nGroups++; |
| Set<String> resolvedMembers = new HashSet<String>(); |
| for (String member : members.get(entity)) { |
| String sid = AdEntity.parseForeignSecurityPrincipal(member); |
| if (null == sid) { |
| resolvedMembers.add(member); |
| nNullSid++; |
| } else { |
| AdEntity resolved = bySid.get(sid); |
| if (null == resolved) { |
| log.info("unable to resolve foreign principal [" |
| + member + "]; member of [" + entity.getDn()); |
| nNullResolution++; |
| } else { |
| resolvedMembers.add(resolved.getDn()); |
| nResolved++; |
| } |
| } |
| } |
| members.put(entity, resolvedMembers); |
| } |
| log.log(Level.FINE, "#groups: {0}", nGroups); |
| log.log(Level.FINE, "#null-SID: {0}", nNullSid); |
| log.log(Level.FINE, "#null-resolve: {0}", nNullResolution); |
| log.log(Level.FINE, "#resolved: {0}", nResolved); |
| } |
| |
| Map<GroupPrincipal, List<Principal>> makeDefs() { |
| // Merge members with well known group members |
| //TODO(myk) Phase II: pass in only the entities just read in (for an |
| // incremental search to only consider those entities) |
| Map<AdEntity, Set<String>> allMembers |
| = new HashMap<AdEntity, Set<String>>(members); |
| allMembers.putAll(wellKnownMembership); |
| Map<GroupPrincipal, List<Principal>> groups |
| = new HashMap<GroupPrincipal, List<Principal>>(); |
| for (AdEntity entity : entities) { |
| if (!entity.isGroup()) { |
| continue; |
| } |
| |
| if (!allMembers.containsKey(entity)) { |
| continue; |
| } |
| |
| String groupName = getPrincipalName(entity); |
| GroupPrincipal group; |
| try { |
| group = new GroupPrincipal(groupName, namespace); |
| } catch (IllegalArgumentException iae) { |
| log.log(Level.WARNING, "Skipping over badly-named group", iae); |
| continue; |
| } |
| List<Principal> def = new ArrayList<Principal>(); |
| |
| if (!feedBuiltinGroups |
| && entity.getSid().startsWith("S-1-5-32-")) { |
| log.log(Level.FINER, "Sending empty BUILTIN Group {0}", entity); |
| groups.put(group, def); |
| continue; |
| } |
| |
| if (entity.isDisabled()) { |
| log.log(Level.FINE, "Skipping {0} members from disabled group {1}", |
| new Object[]{entity.getMembers().size(), group}); |
| groups.put(group, def); |
| continue; |
| } |
| for (String memberDn : allMembers.get(entity)) { |
| AdEntity member = byDn.get(memberDn); |
| if (member == null) { |
| log.info("Unknown member [" + memberDn + "] of group [" |
| + entity.getDn()); |
| continue; |
| } |
| Principal p; |
| String memberName = getPrincipalName(member); |
| if (member.isGroup()) { |
| try { |
| p = new GroupPrincipal(memberName, namespace); |
| } catch (IllegalArgumentException iae) { |
| String warning = "Skipping badly-named group \"" + memberName |
| + "\" from group \"" + groupName + "\"."; |
| log.log(Level.WARNING, warning, iae); |
| continue; |
| } |
| } else { |
| try { |
| p = new UserPrincipal(memberName, namespace); |
| } catch (IllegalArgumentException iae) { |
| String warning = "Skipping badly-named user \"" + memberName |
| + "\" from group \"" + groupName + "\"."; |
| log.log(Level.WARNING, warning, iae); |
| continue; |
| } |
| } |
| def.add(p); |
| } |
| if (entity.isWellKnown()) { |
| log.log(Level.FINE, "Well known group {0} with # members {1}", |
| new Object[]{group, def.size()}); |
| } |
| groups.put(group, def); |
| } |
| log.log(Level.FINE, "number of groups defined: {0}", |
| groups.keySet().size()); |
| if (log.isLoggable(Level.FINER)) { |
| int numGroups = groups.keySet().size(); |
| int totalMembers = 0; |
| for (List<Principal> def : groups.values()) { |
| totalMembers += def.size(); |
| } |
| if (0 != numGroups) { |
| double mean = ((double) totalMembers) / numGroups; |
| log.finer("mean size of defined group: " + mean); |
| } |
| } |
| return groups; |
| } |
| |
| /* |
| * returns principal name for ADEntity object. if domain is available return |
| * principal name as samaccountname@domain else just use samaccountname as |
| * principal name. |
| */ |
| String getPrincipalName(AdEntity e) { |
| return domain.get(e) != null ? |
| e.getSAMAccountName() + "@" + domain.get(e) : e.getSAMAccountName(); |
| } |
| |
| /* Combines info of another catalog with this one. */ |
| void add(GroupCatalog other) { |
| entities.addAll(other.entities); |
| members.putAll(other.members); |
| bySid.putAll(other.bySid); |
| byDn.putAll(other.byDn); |
| domain.putAll(other.domain); |
| for (Object o : wellKnownMembership.keySet()) { |
| wellKnownMembership.get(o).addAll(other.wellKnownMembership.get(o)); |
| } |
| } |
| |
| void clear() { |
| entities.clear(); |
| members.clear(); |
| bySid.clear(); |
| byDn.clear(); |
| domain.clear(); |
| wellKnownMembership.clear(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Arrays.hashCode( |
| new Object[] {entities, members, bySid, byDn, domain}); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof GroupCatalog)) { |
| return false; |
| } |
| GroupCatalog gc = (GroupCatalog) o; |
| return entities.equals(gc.entities) |
| && members.equals(gc.members) |
| && bySid.equals(gc.bySid) |
| && byDn.equals(gc.byDn) |
| && domain.equals(gc.domain) |
| && wellKnownMembership.equals(gc.wellKnownMembership); |
| } |
| } |
| } |