1   package com.atlassian.user.impl.ldap.search.query;
2   
3   /**
4    * Note - any LDAP search query returned from this parserEntity must also be related, finally, to an appropriate
5    * base context in the LDAP DIT. This could be a context under which all group and user entries or stored
6    * or separate contexts for each entity type. The global context will be specified in the JNDI context which
7    * binds to the LDAP server.
8    */
9   
10  import com.atlassian.user.EntityException;
11  import com.atlassian.user.User;
12  import com.atlassian.user.Group;
13  import com.atlassian.user.repository.RepositoryIdentifier;
14  import com.atlassian.user.impl.RepositoryException;
15  import com.atlassian.user.impl.ldap.DefaultLDAPGroupFactory;
16  import com.atlassian.user.impl.ldap.LDAPGroupFactory;
17  import com.atlassian.user.impl.ldap.LDAPUserFactory;
18  import com.atlassian.user.impl.ldap.LiteralFilter;
19  import com.atlassian.user.impl.ldap.adaptor.LDAPGroupAdaptor;
20  import com.atlassian.user.impl.ldap.properties.LdapSearchProperties;
21  import com.atlassian.user.impl.ldap.properties.LdapMembershipProperties;
22  import com.atlassian.user.impl.ldap.repository.LdapContextFactory;
23  import com.atlassian.user.impl.ldap.search.DefaultLDAPUserAdaptor;
24  import com.atlassian.user.impl.ldap.search.LDAPPagerInfo;
25  import com.atlassian.user.impl.ldap.search.LDAPUserAdaptor;
26  import com.atlassian.user.impl.ldap.search.LdapFilterFactory;
27  import com.atlassian.user.impl.ldap.search.page.LDAPEntityPager;
28  import com.atlassian.user.search.DefaultSearchResult;
29  import com.atlassian.user.search.SearchResult;
30  import com.atlassian.user.search.page.Pager;
31  import com.atlassian.user.search.query.*;
32  import com.atlassian.util.profiling.UtilTimerStack;
33  import net.sf.ldaptemplate.support.LdapEncoder;
34  
35  import javax.naming.NamingEnumeration;
36  import javax.naming.NamingException;
37  import javax.naming.directory.Attribute;
38  import javax.naming.directory.Attributes;
39  import java.util.Iterator;
40  
41  public class LDAPEntityQueryParser implements EntityQueryParser
42  {
43      public static final String OPEN_PARAN = "(";
44      public static final String CLOSE_PARAN = ")";
45      public static final String EQ = "=";
46      public static final String AND = "&";
47      public static final String OR = "|";
48      public static final String WILDCARD = TermQuery.WILDCARD;
49  
50      private final LdapContextFactory repository;
51      private final LDAPUserAdaptor userAdaptor;
52      private final LDAPGroupAdaptor groupAdaptor;
53      private final LDAPGroupFactory groupFactory;
54      private final LDAPUserFactory userFactory;
55  
56      private final RepositoryIdentifier repositoryIdentifier;
57      private final LdapSearchProperties searchProperties;
58      private final LdapMembershipProperties membershipProperties;
59  
60      public LDAPEntityQueryParser(LdapContextFactory repository, LDAPGroupAdaptor groupAdaptor,
61          RepositoryIdentifier repositoryIdentifier, LDAPUserFactory userFactory,
62          LdapSearchProperties searchProperties, LdapMembershipProperties membershipProperties,
63          LdapFilterFactory filterFactory)
64      {
65          this.repositoryIdentifier = repositoryIdentifier;
66          this.repository = repository;
67          this.groupAdaptor = groupAdaptor;
68          this.userFactory = userFactory;
69          this.userAdaptor = new DefaultLDAPUserAdaptor(this.repository, searchProperties, filterFactory);
70          this.groupFactory = new DefaultLDAPGroupFactory(searchProperties, membershipProperties);
71          this.searchProperties = searchProperties;
72          this.membershipProperties = membershipProperties;
73      }
74  
75      public com.atlassian.user.search.SearchResult findUsers(Query query) throws EntityException
76      {
77          if (UtilTimerStack.isActive())
78              UtilTimerStack.push(this.getClass().getName() + "_findUsers");
79  
80          String parsedQuery = null;
81          Pager iter;
82          parsedQuery = directQuery(query, parsedQuery);
83  
84          LDAPPagerInfo info = userAdaptor.search(new LiteralFilter(parsedQuery));
85          iter = new LDAPEntityPager<User>(searchProperties, repository, userFactory, info);
86          DefaultSearchResult searchResult = new DefaultSearchResult(iter, repositoryIdentifier.getKey());
87  
88          if (UtilTimerStack.isActive())
89              UtilTimerStack.pop(this.getClass().getName() + "_findUsers");
90  
91          return searchResult;
92      }
93  
94      public com.atlassian.user.search.SearchResult findGroups(Query query) throws EntityException
95      {
96          String parsedQuery = directQuery(query, null);
97          LDAPPagerInfo info = groupAdaptor.search(new LiteralFilter(parsedQuery));
98          Pager pager = new LDAPEntityPager<Group>(searchProperties, repository, groupFactory, info);
99          return new DefaultSearchResult(pager, repositoryIdentifier.getKey());
100     }
101 
102     public SearchResult findUsers(Query query, QueryContext context) throws EntityException
103     {
104         if (!context.contains(repositoryIdentifier))
105             return null;
106 
107         return findUsers(query);
108     }
109 
110     public SearchResult findGroups(Query query, QueryContext context) throws EntityException
111     {
112         if (!context.contains(repositoryIdentifier))
113             return null;
114 
115         return findGroups(query);
116     }
117 
118     private String directQuery(Query query, String defaultQuery) throws EntityException
119     {
120         if (query instanceof TermQuery)
121         {
122             StringBuffer parsedQueryStringBuffer = parseQuery((TermQuery) query);
123             return parsedQueryStringBuffer.toString();
124         }
125         else if (query instanceof BooleanQuery)
126         {
127             return parseQuery((BooleanQuery) query).toString();
128         }
129 
130         return defaultQuery;
131     }
132 
133     public StringBuffer parseQuery(BooleanQuery query) throws EntityException
134     {
135         StringBuffer parsedClause = new StringBuffer();
136         parsedClause.append(OPEN_PARAN);
137 
138         if (query.isAND())
139             parsedClause.append(AND);
140         else
141             parsedClause.append(OR);
142 
143         Iterator queryIter = query.getQueries().iterator();
144 
145         while (queryIter.hasNext())
146         {
147             Query foundQuery = (Query) queryIter.next();
148             if (foundQuery instanceof BooleanQuery)
149                 parsedClause.append(parseQuery((BooleanQuery) foundQuery));
150             else
151                 parsedClause.append(parseQuery((TermQuery) foundQuery));
152         }
153 
154         parsedClause.append(CLOSE_PARAN);
155 
156         return parsedClause;
157     }
158 
159     public StringBuffer parseQuery(TermQuery q) throws EntityException
160     {
161         StringBuffer parsedQuery = null;
162 
163         if (q instanceof UserNameTermQuery)
164             parsedQuery = parseTermQuery(q, searchProperties.getUsernameAttribute());
165         else if (q instanceof GroupNameTermQuery)
166             parsedQuery = parseTermQuery(q, searchProperties.getGroupnameAttribute());
167         else if (q instanceof EmailTermQuery)
168             parsedQuery = parseTermQuery(q, searchProperties.getEmailAttribute());
169         else if (q instanceof FullNameTermQuery)
170             parsedQuery = parseFullNameTermQuery(q);
171         else if (q instanceof UsersInGroupTwoTermQuery)
172             parsedQuery = parseMemberNamesInGroupTermQuery();
173         else if (q instanceof GroupsOfUserTwoTermQuery)
174             parsedQuery = parseGroupsOfUserTwoTermQuery((GroupsOfUserTwoTermQuery) q);
175 
176         return parsedQuery;
177     }
178 
179     private StringBuffer parseFullNameTermQuery(TermQuery q)
180     {
181         StringBuffer query = new StringBuffer();
182 
183         query.insert(0, OR);
184         query.insert(0, "(");
185         query.append(parseTermQuery(q, searchProperties.getFirstnameAttribute()));
186         query.append(parseTermQuery(q, searchProperties.getSurnameAttribute()));
187         query.append(")");
188 
189         return query;
190     }
191 
192     /**
193      * Looks up membership lists associated with the group indicated by {@link UsersInGroupTwoTermQuery} and returns an
194      * ldap getGroupEntries string capable of returning their dns.
195      */
196     private StringBuffer parseMemberNamesInGroupTermQuery() throws EntityException
197     {
198         if (!membershipProperties.isMembershipAttributeOnGroup())
199             throw new UnsupportedOperationException();
200 
201         LDAPPagerInfo pagerInfo = groupAdaptor.getGroupEntries(null);
202         NamingEnumeration enume = pagerInfo.getNamingEnumeration();
203         return parseMembershipAttributesForGroupNames(enume);
204     }
205 
206     /**
207      * @return StringBuffer holding an LDAP getGroupEntries query which will return a list of the user's groups.
208      */
209     private StringBuffer parseGroupsOfUserTwoTermQuery(GroupsOfUserTwoTermQuery q) throws EntityException
210     {
211         String userPattern = null;
212 
213         UserNameTermQuery uQuery = q.getUserNameTermQuery();
214         if (uQuery.isMatchingSubstring())
215         {
216             if (uQuery.getMatchingRule().equals(TermQuery.SUBSTRING_STARTS_WITH))
217                 userPattern = uQuery.getTerm() + WILDCARD;
218             else if (uQuery.getMatchingRule().equals(TermQuery.SUBSTRING_ENDS_WITH))
219                 userPattern = WILDCARD + uQuery.getTerm();
220             else if (uQuery.getMatchingRule().equals(TermQuery.SUBSTRING_CONTAINS))
221                 userPattern = WILDCARD + uQuery.getTerm() + WILDCARD;
222         }
223         else
224             userPattern = uQuery.getTerm();
225 
226         //First we find the membership information for the user via the groupAdaptor
227         LDAPPagerInfo pagerInfo = groupAdaptor.getGroupEntriesViaMembership(userPattern);
228 
229         StringBuffer groupQuery;
230 
231         if (membershipProperties.isMembershipAttributeOnGroup())
232             groupQuery = buildQueryForUsersInStaticGroups(pagerInfo.getNamingEnumeration());
233         else
234             groupQuery = buildQueryForUsersInDynamicGroups(pagerInfo.getNamingEnumeration());
235 
236         return groupQuery;
237     }
238 
239     private StringBuffer buildQueryForUsersInDynamicGroups(NamingEnumeration enume) throws EntityException
240     {
241         StringBuffer parsedQuery = null;
242 
243         while (enume.hasMoreElements())
244         {
245             javax.naming.directory.SearchResult result = (javax.naming.directory.SearchResult) enume.nextElement();
246             Attributes attrs = result.getAttributes();
247 
248             Attribute attr = attrs.get(membershipProperties.getMembershipAttribute());
249 
250             if (attr == null)
251                 return null;
252 
253             try
254             {
255                 NamingEnumeration membershipEnume = attr.getAll();
256                 while (membershipEnume.hasMoreElements())
257                 {
258                     boolean wrapping = false;
259 
260                     if (parsedQuery == null)
261                     {
262                         parsedQuery = new StringBuffer();
263                     }
264                     else
265                     {
266                         parsedQuery.insert(0, OR);
267                         parsedQuery.insert(0, OPEN_PARAN);
268                         wrapping = true;
269                     }
270 
271                     parsedQuery.append(OPEN_PARAN);
272                     parsedQuery.append(((String) membershipEnume.nextElement()).split(",")[0]);
273                     parsedQuery.append(CLOSE_PARAN);
274 
275                     if (wrapping)
276                         parsedQuery.append(CLOSE_PARAN);
277                 }
278             }
279             catch (Exception e)
280             {
281                 throw new RepositoryException(e);
282             }
283         }
284 
285         return parsedQuery;
286     }
287 
288     private StringBuffer buildQueryForUsersInStaticGroups(NamingEnumeration enume) throws EntityException
289     {
290         StringBuffer parsedQuery = null;
291 
292         while (enume.hasMoreElements())
293         {
294             javax.naming.directory.SearchResult result = (javax.naming.directory.SearchResult) enume.nextElement();
295             Attributes attrs = result.getAttributes();
296 
297             Attribute attr = attrs.get(searchProperties.getGroupnameAttribute());
298 
299             if (attr == null)
300                 return null;
301 
302             try
303             {
304                 NamingEnumeration membershipEnume = attr.getAll();
305                 while (membershipEnume.hasMoreElements())
306                 {
307                     boolean wrapping = false;
308 
309                     if (parsedQuery == null)
310                     {
311                         parsedQuery = new StringBuffer();
312                     }
313                     else
314                     {
315                         parsedQuery.insert(0, OR);
316                         parsedQuery.insert(0, OPEN_PARAN);
317                         wrapping = true;
318                     }
319 
320                     parsedQuery.append(OPEN_PARAN);
321                     parsedQuery.append(searchProperties.getGroupnameAttribute());
322                     parsedQuery.append(EQ);
323                     parsedQuery.append(((String) membershipEnume.nextElement()).split(",")[0]);
324                     parsedQuery.append(CLOSE_PARAN);
325 
326                     if (wrapping)
327                         parsedQuery.append(CLOSE_PARAN);
328                 }
329             }
330             catch (Exception e)
331             {
332                 throw new RepositoryException(e);
333             }
334         }
335 
336         return parsedQuery;
337     }
338 
339     private StringBuffer parseMembershipAttributesForGroupNames(NamingEnumeration enume)
340     {
341         StringBuffer parsedQuery = null;
342 
343         while (enume.hasMoreElements())
344         {
345             javax.naming.directory.SearchResult result = (javax.naming.directory.SearchResult) enume.nextElement();
346             Attributes attrs = result.getAttributes();
347 
348             Attribute attr;
349 
350             if (membershipProperties.isMembershipAttributeOnGroup())
351                 attr = attrs.get(membershipProperties.getMembershipAttribute());
352             else
353                 attr = attrs.get(searchProperties.getGroupnameAttribute());
354 
355             try
356             {
357                 if (parsedQuery == null)
358                     parsedQuery = parseMembershipAttributeForGroupNames(attr);
359                 else
360                 {
361                     parsedQuery.insert(0, "(|");
362                     parsedQuery = parsedQuery.append(parseMembershipAttributeForGroupNames(attr));
363                     parsedQuery.append(")");
364                 }
365             }
366             catch (Exception e)
367             {
368                 e.printStackTrace();
369             }
370         }
371         return parsedQuery;
372     }
373 
374     private StringBuffer parseMembershipAttributeForGroupNames(Attribute attr) throws NamingException
375     {
376         if (attr == null)
377             return null;
378 
379         StringBuffer parsedQuery = null;
380         NamingEnumeration membershipEnume = attr.getAll();
381         while (membershipEnume.hasMoreElements())
382         {
383             boolean wrapping = false;
384 
385             if (parsedQuery == null)
386             {
387                 parsedQuery = new StringBuffer();
388             }
389             else
390             {
391                 parsedQuery.insert(0, OR);
392                 parsedQuery.insert(0, OPEN_PARAN);
393                 wrapping = true;
394             }
395 
396             if (membershipProperties.isMembershipAttributeOnGroup())
397             {
398                 parsedQuery.append(OPEN_PARAN);
399                 parsedQuery.append(((String) membershipEnume.nextElement()).split(",")[0]);
400                 parsedQuery.append(CLOSE_PARAN);
401             }
402             else
403             {
404                 parsedQuery.append(OPEN_PARAN);
405                 parsedQuery.append(searchProperties.getGroupnameAttribute());
406                 parsedQuery.append(EQ);
407                 parsedQuery.append(((String) membershipEnume.nextElement()).split(",")[0]);
408                 parsedQuery.append(CLOSE_PARAN);
409 
410             }
411 
412             if (wrapping)
413                 parsedQuery.append(CLOSE_PARAN);
414         }
415 
416         return parsedQuery;
417     }
418 
419     /**
420      * Generic method which can parse a term query and relate it to a targeted attributeType in LDAP speak.
421      * <p/>
422      * If attributeType is 'cn' and q.getTerm() is 'fred' then it will return a {@link StringBuffer} holding (cn=fred).
423      * It will also deal with substring matching rules. If the {@link TermQuery} was matching substrings using {@link
424      * TermQuery#SUBSTRING_STARTS_WITH}, using the example values above, it would return (cn=fred*)
425      */
426     private StringBuffer parseTermQuery(TermQuery q, String attributeType)
427     {
428         StringBuffer parsedQuery;
429         parsedQuery = new StringBuffer();
430         parsedQuery.append(OPEN_PARAN);
431         parsedQuery.append(attributeType);
432         parsedQuery.append(EQ);
433 
434         if (q.isMatchingSubstring())
435         {
436             if ((q.getMatchingRule().equals(TermQuery.SUBSTRING_ENDS_WITH)) ||
437                 (q.getMatchingRule().equals(TermQuery.SUBSTRING_CONTAINS)))
438             {
439                 parsedQuery.append(WILDCARD);
440             }
441         }
442 
443         parsedQuery.append(LdapEncoder.filterEncode(q.getTerm()));
444 
445         if (q.isMatchingSubstring())
446         {
447             if ((q.getMatchingRule().equals(TermQuery.SUBSTRING_STARTS_WITH)) ||
448                 (q.getMatchingRule().equals(TermQuery.SUBSTRING_CONTAINS)))
449             {
450                 parsedQuery.append(WILDCARD);
451             }
452         }
453 
454         parsedQuery.append(CLOSE_PARAN);
455         return parsedQuery;
456     }
457 }