View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.acegisecurity.ldap;
17  
18  import org.springframework.dao.DataAccessException;
19  import org.springframework.dao.IncorrectResultSizeDataAccessException;
20  
21  import org.springframework.util.Assert;
22  import org.springframework.util.StringUtils;
23  
24  import java.util.HashSet;
25  import java.util.Set;
26  
27  import javax.naming.NameNotFoundException;
28  import javax.naming.NamingEnumeration;
29  import javax.naming.NamingException;
30  import javax.naming.Context;
31  import javax.naming.directory.Attribute;
32  import javax.naming.directory.Attributes;
33  import javax.naming.directory.DirContext;
34  import javax.naming.directory.SearchControls;
35  import javax.naming.directory.SearchResult;
36  
37  
38  /**
39   * LDAP equivalent of the Spring JdbcTemplate class.<p>This is mainly intended to simplify Ldap access within Acegi
40   * Security's LDAP-related services.</p>
41   *
42   * @author Ben Alex
43   * @author Luke Taylor
44   */
45  public class LdapTemplate {
46      //~ Static fields/initializers =====================================================================================
47  
48      public static final String[] NO_ATTRS = new String[0];
49  
50      //~ Instance fields ================================================================================================
51  
52      private InitialDirContextFactory dirContextFactory;
53      private NamingExceptionTranslator exceptionTranslator = new LdapExceptionTranslator();
54  
55      /** Default search controls */
56      private SearchControls searchControls = new SearchControls();
57      private String password = null;
58      private String principalDn = null;
59  
60      //~ Constructors ===================================================================================================
61  
62      public LdapTemplate(InitialDirContextFactory dirContextFactory) {
63          Assert.notNull(dirContextFactory, "An InitialDirContextFactory is required");
64          this.dirContextFactory = dirContextFactory;
65  
66          searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
67      }
68  
69  /**
70       *
71       * @param dirContextFactory the source of DirContexts
72       * @param userDn the user name to authenticate as when obtaining new contexts
73       * @param password the user's password
74       */
75      public LdapTemplate(InitialDirContextFactory dirContextFactory, String userDn, String password) {
76          this(dirContextFactory);
77  
78          Assert.hasLength(userDn, "userDn must not be null or empty");
79          Assert.notNull(password, "password cannot be null");
80  
81          this.principalDn = userDn;
82          this.password = password;
83      }
84  
85      //~ Methods ========================================================================================================
86  
87      /**
88       * Performs an LDAP compare operation of the value of an attribute for a particular directory entry.
89       *
90       * @param dn the entry who's attribute is to be used
91       * @param attributeName the attribute who's value we want to compare
92       * @param value the value to be checked against the directory value
93       *
94       * @return true if the supplied value matches that in the directory
95       */
96      public boolean compare(final String dn, final String attributeName, final Object value) {
97          final String comparisonFilter = "(" + attributeName + "={0})";
98  
99          class LdapCompareCallback implements LdapCallback {
100             public Object doInDirContext(DirContext ctx)
101                 throws NamingException {
102                 SearchControls ctls = new SearchControls();
103                 ctls.setReturningAttributes(NO_ATTRS);
104                 ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
105 
106                 String relativeName = LdapUtils.getRelativeName(dn, ctx);
107 
108                 NamingEnumeration results = ctx.search(relativeName, comparisonFilter, new Object[] {value}, ctls);
109 
110                 return Boolean.valueOf(results.hasMore());
111             }
112         }
113 
114         Boolean matches = (Boolean) execute(new LdapCompareCallback());
115 
116         return matches.booleanValue();
117     }
118 
119     public Object execute(LdapCallback callback) throws DataAccessException {
120         DirContext ctx = null;
121 
122         try {
123             ctx = (principalDn == null) ? dirContextFactory.newInitialDirContext()
124                                         : dirContextFactory.newInitialDirContext(principalDn, password);
125 
126             return callback.doInDirContext(ctx);
127         } catch (NamingException exception) {
128             throw exceptionTranslator.translate("LdapCallback", exception);
129         } finally {
130             LdapUtils.closeContext(ctx);
131         }
132     }
133 
134     public boolean nameExists(final String dn) {
135         Boolean exists = (Boolean) execute(new LdapCallback() {
136                 public Object doInDirContext(DirContext ctx)
137                     throws NamingException {
138                     try {
139                         Object obj = ctx.lookup(LdapUtils.getRelativeName(dn, ctx));
140                         if (obj instanceof Context) {
141                             LdapUtils.closeContext((Context) obj);
142                         }
143 
144                     } catch (NameNotFoundException nnfe) {
145                         return Boolean.FALSE;
146                     }
147 
148                     return Boolean.TRUE;
149                 }
150             });
151 
152         return exists.booleanValue();
153     }
154 
155     /**
156      * Composes an object from the attributes of the given DN.
157      *
158      * @param dn the directory entry which will be read
159      * @param mapper maps the attributes to the required object
160      * @param attributesToRetrieve the named attributes which will be retrieved from the directory entry.
161      *
162      * @return the object created by the mapper
163      */
164     public Object retrieveEntry(final String dn, final LdapEntryMapper mapper, final String[] attributesToRetrieve) {
165         return execute(new LdapCallback() {
166                 public Object doInDirContext(DirContext ctx)
167                     throws NamingException {
168                     return mapper.mapAttributes(dn,
169                         ctx.getAttributes(LdapUtils.getRelativeName(dn, ctx), attributesToRetrieve));
170                 }
171             });
172     }
173 
174     /**
175      * Performs a search using the supplied filter and returns the union of the values of the named attribute
176      * found in all entries matched by the search. Note that one directory entry may have several values for the
177      * attribute. Intended for role searches and similar scenarios.
178      *
179      * @param base the DN to search in
180      * @param filter search filter to use
181      * @param params the parameters to substitute in the search filter
182      * @param attributeName the attribute who's values are to be retrieved.
183      *
184      * @return the set of String values for the attribute as a union of the values found in all the matching entries.
185      */
186     public Set searchForSingleAttributeValues(final String base, final String filter, final Object[] params,
187         final String attributeName) {
188         class SingleAttributeSearchCallback implements LdapCallback {
189             public Object doInDirContext(DirContext ctx)
190                 throws NamingException {
191                 Set unionOfValues = new HashSet();
192 
193                 // We're only interested in a single attribute for this method, so we make a copy of
194                 // the search controls and override the returningAttributes property
195                 SearchControls ctls = new SearchControls();
196 
197                 ctls.setSearchScope(searchControls.getSearchScope());
198                 ctls.setTimeLimit(searchControls.getTimeLimit());
199                 ctls.setDerefLinkFlag(searchControls.getDerefLinkFlag());
200                 ctls.setReturningAttributes(new String[] {attributeName});
201 
202                 NamingEnumeration matchingEntries = ctx.search(base, filter, params, ctls);
203 
204                 while (matchingEntries.hasMore()) {
205                     SearchResult result = (SearchResult) matchingEntries.next();
206                     Attributes attrs = result.getAttributes();
207 
208                     // There should only be one attribute in each matching entry.
209                     NamingEnumeration returnedAttributes = attrs.getAll();
210 
211                     while (returnedAttributes.hasMore()) {
212                         Attribute returnedAttribute = (Attribute) returnedAttributes.next();
213                         NamingEnumeration attributeValues = returnedAttribute.getAll();
214 
215                         while (attributeValues.hasMore()) {
216                             Object value = attributeValues.next();
217 
218                             unionOfValues.add(value.toString());
219                         }
220                     }
221                 }
222 
223                 return unionOfValues;
224             }
225         }
226 
227         return (Set) execute(new SingleAttributeSearchCallback());
228     }
229 
230     /**
231      * Performs a search, with the requirement that the search shall return a single directory entry, and uses
232      * the supplied mapper to create the object from that entry.
233      *
234      * @param base
235      * @param filter
236      * @param params
237      * @param mapper
238      *
239      * @return the object created by the mapper from the matching entry
240      *
241      * @throws IncorrectResultSizeDataAccessException if no results are found or the search returns more than one
242      *         result.
243      */
244     public Object searchForSingleEntry(final String base, final String filter, final Object[] params,
245         final LdapEntryMapper mapper) {
246         return execute(new LdapCallback() {
247                 public Object doInDirContext(DirContext ctx)
248                     throws NamingException {
249                     NamingEnumeration results = ctx.search(base, filter, params, searchControls);
250 
251                     if (!results.hasMore()) {
252                         throw new IncorrectResultSizeDataAccessException(1, 0);
253                     }
254 
255                     SearchResult searchResult = (SearchResult) results.next();
256 
257                     if (results.hasMore()) {
258                         // We don't know how many results but set to 2 which is good enough
259                         throw new IncorrectResultSizeDataAccessException(1, 2);
260                     }
261 
262                     // Work out the DN of the matched entry
263                     StringBuffer dn = new StringBuffer(searchResult.getName());
264 
265                     if (base.length() > 0) {
266                         dn.append(",");
267                         dn.append(base);
268                     }
269 
270                     String nameInNamespace = ctx.getNameInNamespace();
271 
272                     if (StringUtils.hasLength(nameInNamespace)) {
273                         dn.append(",");
274                         dn.append(nameInNamespace);
275                     }
276 
277                     return mapper.mapAttributes(dn.toString(), searchResult.getAttributes());
278                 }
279             });
280     }
281 
282     /**
283      * Sets the search controls which will be used for search operations by the template.
284      *
285      * @param searchControls the SearchControls instance which will be cached in the template.
286      */
287     public void setSearchControls(SearchControls searchControls) {
288         this.searchControls = searchControls;
289     }
290 
291     //~ Inner Classes ==================================================================================================
292 
293     private static class LdapExceptionTranslator implements NamingExceptionTranslator {
294         public DataAccessException translate(String task, NamingException e) {
295             return new LdapDataAccessException(task + ";" + e.getMessage(), e);
296         }
297     }
298 }