1   package com.atlassian.user.impl.ldap.adaptor;
2   
3   import com.atlassian.user.EntityException;
4   import com.atlassian.user.Group;
5   import com.atlassian.user.impl.EntityMissingException;
6   import com.atlassian.user.impl.RepositoryException;
7   import com.atlassian.user.impl.ldap.LDAPEntity;
8   import com.atlassian.user.impl.ldap.LDAPGroupFactory;
9   import com.atlassian.user.impl.ldap.properties.LdapSearchProperties;
10  import com.atlassian.user.impl.ldap.repository.LdapContextFactory;
11  import com.atlassian.user.impl.ldap.search.LDAPPagerInfo;
12  import com.atlassian.user.impl.ldap.search.LdapFilterFactory;
13  import com.atlassian.user.impl.ldap.search.page.LDAPEntityPager;
14  import com.atlassian.user.search.page.Pager;
15  import com.atlassian.user.util.LDAPUtils;
16  import com.atlassian.util.profiling.UtilTimerStack;
17  import com.opensymphony.util.TextUtils;
18  import net.sf.ldaptemplate.support.filter.EqualsFilter;
19  import net.sf.ldaptemplate.support.filter.Filter;
20  import org.apache.log4j.Logger;
21  
22  import javax.naming.Context;
23  import javax.naming.NamingEnumeration;
24  import javax.naming.NamingException;
25  import javax.naming.directory.Attributes;
26  import javax.naming.directory.DirContext;
27  import javax.naming.directory.SearchControls;
28  import javax.naming.directory.SearchResult;
29  import java.net.Inet4Address;
30  import java.net.URI;
31  import java.net.URISyntaxException;
32  import java.net.UnknownHostException;
33  import java.text.MessageFormat;
34  
35  public abstract class AbstractLDAPGroupAdaptor implements LDAPGroupAdaptor
36  {
37      protected final Logger log = Logger.getLogger(this.getClass());
38  
39      protected final LdapSearchProperties searchProperties;
40      private final LdapFilterFactory filterFactory;
41  
42      protected final LdapContextFactory repository;
43      protected final LDAPGroupFactory groupFactory;
44  
45      protected AbstractLDAPGroupAdaptor(LdapContextFactory repo, LdapSearchProperties searchProperties,
46          LDAPGroupFactory groupFactory, LdapFilterFactory filterFactory)
47      {
48          this.repository = repo;
49          this.searchProperties = searchProperties;
50          this.filterFactory = filterFactory;
51          this.groupFactory = groupFactory;
52      }
53  
54      public Group getGroup(String name) throws EntityException
55      {
56          DirContext ctx = null;
57          NamingEnumeration<SearchResult> enume;
58          Group group = null;
59  
60          String filter = constructGroupSearchFilter(name).encode();
61          String baseDn = searchProperties.getBaseGroupNamespace();
62  
63          try
64          {
65              ctx = repository.getLDAPContext();
66              if (log.isDebugEnabled())
67                  log.debug("AbstractLDAPGroupAdapter.getGroup:" + filter);
68  
69              enume = ctx.search(baseDn, filter,
70                  LDAPUtils.createSearchControls(new String[]{searchProperties.getGroupnameAttribute()}, searchProperties.isGroupSearchScopeAllDepths(),
71                      searchProperties.getTimeLimitMillis()));
72              if (enume == null) return null;
73  
74              while (enume.hasMoreElements())
75              {
76                  SearchResult result = enume.nextElement();
77                  Attributes attrs = result.getAttributes();
78                  group = groupFactory.getGroup(attrs, result.getName());
79              }
80          }
81          catch (NamingException e)
82          {
83              String msg = "Exception when retrieving LDAP group {0} (base DN: {1}, filter: {2})";
84              throw new RepositoryException(MessageFormat.format(msg, name, baseDn, filter), e);
85          }
86          finally
87          {
88              try
89              {
90                  if (ctx != null) ctx.close();
91              }
92              catch (NamingException e)
93              {
94                  log.warn("Failed to close LDAP connection after search for group: " + name, e);
95              }
96          }
97  
98          return group;
99      }
100 
101     public Pager<Group> getGroups() throws EntityException
102     {
103         LDAPPagerInfo ldapPagerInfo = getGroupEntries();
104 
105         return new LDAPEntityPager<Group>(searchProperties, repository, groupFactory, ldapPagerInfo);
106     }
107 
108     public LDAPPagerInfo getGroupEntries() throws EntityException
109     {
110         return getGroupEntries("*");
111     }
112 
113     public LDAPPagerInfo getGroupEntries(String groupName) throws EntityException
114     {
115         return getGroupEntries(groupName, null, null);
116     }
117 
118     public LDAPPagerInfo getGroupEntries(String[] attributesToReturn, Filter additionalSearchFilter)
119             throws EntityException
120     {
121         return getGroupEntries("*", attributesToReturn, additionalSearchFilter);
122     }
123 
124     /**
125      * @param groupName a String holding an RFC 2254 compliant getGroupEntries parameter to AND to
126      *                  the {@link com.atlassian.user.impl.ldap.repository.LdapContextFactory#GROUP_SEARCH_FILTER}.
127      * @throws RepositoryException
128      */
129     public LDAPPagerInfo getGroupEntries(String groupName, String[] attributesToReturn, Filter additionalSearchFilter)
130             throws RepositoryException
131     {
132         Filter searchFilter = constructGroupSearchFilter(groupName, additionalSearchFilter);
133         return search(searchFilter, attributesToReturn);
134     }
135 
136     /**
137      * @return an {@link LDAPPagerInfo} holding the search query used, the result, and the original base context.
138      * @throws RepositoryException
139      */
140     public LDAPPagerInfo search(Filter searchFilter) throws RepositoryException
141     {
142         return search(searchFilter, null);
143     }
144 
145     /**
146      * @param attributesToReturn - will default to mapped membership attribute available in the {@link com.atlassian.user.impl.ldap.repository.LdapContextFactory}
147      *                           if set to null.
148      * @return an {@link LDAPPagerInfo} holding the search query used, the result, and the original base context.
149      * @throws RepositoryException
150      */
151     public LDAPPagerInfo search(Filter filter, String[] attributesToReturn) throws RepositoryException
152     {
153         if (UtilTimerStack.isActive())
154             UtilTimerStack.push(this.getClass().getName() + "_search(" + filter + ")");
155 
156         try
157         {
158             if (attributesToReturn == null)
159                 attributesToReturn = new String[]{searchProperties.getGroupnameAttribute()};
160 
161             Filter groupSearchFilter = filterFactory.getGroupSearchFilter();
162             if (filter != null)
163                 groupSearchFilter = LDAPUtils.makeAndFilter(groupSearchFilter, filter);
164 
165             SearchControls ctls = LDAPUtils.createSearchControls(
166                 attributesToReturn, searchProperties.isGroupSearchScopeAllDepths(),
167                 searchProperties.getTimeLimitMillis());
168             NamingEnumeration<SearchResult> groupSearchEnume = null;
169 
170             if (UtilTimerStack.isActive())
171                 UtilTimerStack.push(this.getClass().getName() + "_search_JNDI_RAW_(" + groupSearchFilter + ")");
172             DirContext ctx = null;
173             try
174             {
175                 ctx = repository.getLDAPContext();
176 
177                 log.debug("Searching for groups using base name space:" + searchProperties.getBaseGroupNamespace() +
178                     " and encoded filter " + groupSearchFilter.encode());
179 
180                 groupSearchEnume = ctx.search(searchProperties.getBaseGroupNamespace(), groupSearchFilter.encode(), ctls);
181 
182                 if (groupSearchEnume.hasMore())
183                     log.debug("found at least one group");
184                 else
185                     log.debug("no groups found");
186             }
187             catch (NamingException e)
188             {
189                 String host = "<unknown>";
190                 String ipAddress = "<unknown>";
191                 try
192                 {
193                     String urlString = (String) repository.getJNDIEnv().get(Context.PROVIDER_URL);
194                     URI uri = new URI(urlString);
195                     host = uri.getHost();
196                     ipAddress = Inet4Address.getByName(host).getHostAddress();
197                 }
198                 catch (URISyntaxException use)
199                 {
200                     log.debug("Error while retrieving LDAP server info", use);
201                 }
202                 catch (UnknownHostException uhe)
203                 {
204                     log.debug("Error while retrieving LDAP server info", uhe);
205                 }
206                 log.error("Error searching for groups from LDAP server " + host + "[" + ipAddress + "]");
207                 throw new RepositoryException(e);
208             }
209             finally
210             {
211                 if (UtilTimerStack.isActive())
212                     UtilTimerStack.pop(this.getClass().getName() + "_search_JNDI_RAW_(" + groupSearchFilter + ")");
213 
214                 try
215                 {
216                     if (ctx != null) ctx.close();
217                 }
218                 catch (NamingException e)
219                 {
220                     log.warn("Exception trying to close LDAP context, possible resource leak", e);
221                 }
222             }
223 
224             return new LDAPPagerInfo(groupSearchEnume, groupSearchFilter,
225                 searchProperties.getBaseGroupNamespace(), searchProperties.isGroupSearchScopeAllDepths(),
226                 attributesToReturn, searchProperties.getTimeLimitMillis());
227         }
228         finally
229         {
230             if (UtilTimerStack.isActive())
231                 UtilTimerStack.pop(this.getClass().getName() + "_search(" + filter + ")");
232         }
233     }
234 
235     protected Filter constructGroupSearchFilter(String name)
236     {
237         return constructGroupSearchFilter(name, null);
238     }
239 
240     protected Filter constructGroupSearchFilter(String name, Filter patternToAnd)
241     {
242         Filter searchFilter = null;
243 
244         if (TextUtils.stringSet(name) && !"*".equals(name)) // if we are only searching for *, then we can exclude the group name attribute from the search
245             searchFilter = new EqualsFilter(searchProperties.getGroupnameAttribute(), name);
246 
247         searchFilter = addGroupSearchFilter(searchFilter);
248 
249         if (patternToAnd != null)
250             return LDAPUtils.makeAndFilter(searchFilter, patternToAnd);
251 
252         return searchFilter;
253     }
254 
255     private Filter addGroupSearchFilter(Filter searchFilter)
256     {
257         return LDAPUtils.makeAndFilter(searchFilter, filterFactory.getGroupSearchFilter());
258     }
259 
260     /**
261      * Extracts the username from the first occurrence of the username attribute
262      * <br/>
263      * For example,
264      * <p/>
265      * CN=ldapusera,CN=Users,DC=atlassian,DC=private
266      * </p>
267      * becomes ldapusera
268      *
269      * @return String representing the username in the DN
270      */
271     protected String getFirstPhraseFromDN(String dn)
272     {
273         String[] rdns = dn.split(",");
274         String[] firstPhrase = rdns[0].split("=");
275 
276         return firstPhrase[1];
277     }
278 
279 
280     public String getGroupDN(String groupName) throws EntityException
281     {
282         LDAPPagerInfo ldapPagerInfo = getGroupEntries(groupName, new String[]{"dn"}, null);
283 
284         if (!ldapPagerInfo.getNamingEnumeration().hasMoreElements())
285             throw new EntityMissingException("Could not get DN for group [" + groupName + "]");
286 
287         SearchResult result = ldapPagerInfo.getNamingEnumeration().nextElement();
288 
289         String groupDN = result.getName();
290         //we need a DN. The result only returns the full DN if the result was in the base getGroupEntries context
291         if (groupDN.indexOf(searchProperties.getBaseGroupNamespace()) == -1)
292             groupDN = groupDN + "," + searchProperties.getBaseGroupNamespace();
293 
294         return groupDN;
295     }
296 
297     public String getGroupDN(Group group) throws EntityException
298     {
299         if (group instanceof LDAPEntity)
300         {
301             LDAPEntity entity = (LDAPEntity) group;
302             return entity.getDistinguishedName();
303         }
304         else
305             throw new IllegalArgumentException("Group is not an LDAPEntity");
306     }
307 
308     public LDAPGroupFactory getGroupFactory()
309     {
310         return groupFactory;
311     }
312 }