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.springframework.security.ldap;
17  
18  import java.text.MessageFormat;
19  import java.util.Arrays;
20  import java.util.HashSet;
21  import java.util.Set;
22  
23  import javax.naming.NamingEnumeration;
24  import javax.naming.NamingException;
25  import javax.naming.PartialResultException;
26  import javax.naming.directory.Attributes;
27  import javax.naming.directory.DirContext;
28  import javax.naming.directory.SearchControls;
29  import javax.naming.directory.SearchResult;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.springframework.dao.IncorrectResultSizeDataAccessException;
34  import org.springframework.ldap.core.ContextExecutor;
35  import org.springframework.ldap.core.ContextMapper;
36  import org.springframework.ldap.core.ContextSource;
37  import org.springframework.ldap.core.DirContextAdapter;
38  import org.springframework.ldap.core.DirContextOperations;
39  import org.springframework.ldap.core.DistinguishedName;
40  import org.springframework.ldap.core.LdapEncoder;
41  import org.springframework.ldap.core.LdapTemplate;
42  import org.springframework.util.Assert;
43  
44  
45  /**
46   * Extension of Spring LDAP's LdapTemplate class which adds extra functionality required by Spring Security.
47   *
48   * @author Ben Alex
49   * @author Luke Taylor
50   * @since 2.0
51   */
52  public class SpringSecurityLdapTemplate extends LdapTemplate {
53      //~ Static fields/initializers =====================================================================================
54      private static final Log logger = LogFactory.getLog(SpringSecurityLdapTemplate.class);
55  
56      public static final String[] NO_ATTRS = new String[0];
57  
58      //~ Instance fields ================================================================================================
59  
60      /** Default search controls */
61      private SearchControls searchControls = new SearchControls();
62  
63      //~ Constructors ===================================================================================================
64  
65      public SpringSecurityLdapTemplate(ContextSource contextSource) {
66          Assert.notNull(contextSource, "ContextSource cannot be null");
67          setContextSource(contextSource);
68  
69          searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
70      }
71  
72      //~ Methods ========================================================================================================
73  
74      /**
75       * Performs an LDAP compare operation of the value of an attribute for a particular directory entry.
76       *
77       * @param dn the entry who's attribute is to be used
78       * @param attributeName the attribute who's value we want to compare
79       * @param value the value to be checked against the directory value
80       *
81       * @return true if the supplied value matches that in the directory
82       */
83      public boolean compare(final String dn, final String attributeName, final Object value) {
84          final String comparisonFilter = "(" + attributeName + "={0})";
85  
86          class LdapCompareCallback implements ContextExecutor {
87  
88              public Object executeWithContext(DirContext ctx) throws NamingException {
89                  SearchControls ctls = new SearchControls();
90                  ctls.setReturningAttributes(NO_ATTRS);
91                  ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
92  
93                  NamingEnumeration results = ctx.search(dn, comparisonFilter, new Object[] {value}, ctls);
94  
95                  return Boolean.valueOf(results.hasMore());
96              }
97          }
98  
99          Boolean matches = (Boolean) executeReadOnly(new LdapCompareCallback());
100 
101         return matches.booleanValue();
102     }
103 
104     /**
105      * Composes an object from the attributes of the given DN.
106      *
107      * @param dn the directory entry which will be read
108      * @param attributesToRetrieve the named attributes which will be retrieved from the directory entry.
109      *
110      * @return the object created by the mapper
111      */
112     public DirContextOperations retrieveEntry(final String dn, final String[] attributesToRetrieve) {
113 
114         return (DirContextOperations) executeReadOnly(new ContextExecutor() {
115                 public Object executeWithContext(DirContext ctx) throws NamingException {
116                     Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve);
117 
118                     // Object object = ctx.lookup(LdapUtils.getRelativeName(dn, ctx));
119 
120                     return new DirContextAdapter(attrs, new DistinguishedName(dn),
121                             new DistinguishedName(ctx.getNameInNamespace()));
122                 }
123             });
124     }
125 
126     /**
127      * Performs a search using the supplied filter and returns the union of the values of the named attribute
128      * found in all entries matched by the search. Note that one directory entry may have several values for the
129      * attribute. Intended for role searches and similar scenarios.
130      *
131      * @param base the DN to search in
132      * @param filter search filter to use
133      * @param params the parameters to substitute in the search filter
134      * @param attributeName the attribute who's values are to be retrieved.
135      *
136      * @return the set of String values for the attribute as a union of the values found in all the matching entries.
137      */
138     public Set searchForSingleAttributeValues(final String base, final String filter, final Object[] params,
139             final String attributeName) {
140         // Escape the params acording to RFC2254
141         Object[] encodedParams = new String[params.length];
142 
143         for (int i=0; i < params.length; i++) {
144             encodedParams[i] = LdapEncoder.filterEncode(params[i].toString());
145         }
146 
147         String formattedFilter = MessageFormat.format(filter, encodedParams);
148         logger.debug("Using filter: " + formattedFilter);
149 
150         final HashSet set = new HashSet();
151 
152         ContextMapper roleMapper = new ContextMapper() {
153             public Object mapFromContext(Object ctx) {
154                 DirContextAdapter adapter = (DirContextAdapter) ctx;
155                 String[] values = adapter.getStringAttributes(attributeName);
156                 if (values == null || values.length == 0) {
157                     logger.debug("No attribute value found for '" + attributeName + "'");
158                 } else {
159                     set.addAll(Arrays.asList(values));
160                 }
161                 return null;
162             }
163         };
164 
165         SearchControls ctls = new SearchControls();
166         ctls.setSearchScope(searchControls.getSearchScope());
167         ctls.setReturningAttributes(new String[] {attributeName});
168         ctls.setReturningObjFlag(false);
169 
170         search(base, formattedFilter, ctls, roleMapper);
171 
172         return set;
173     }
174 
175     /**
176      * Performs a search, with the requirement that the search shall return a single directory entry, and uses
177      * the supplied mapper to create the object from that entry.
178      * <p>
179      * Ignores <tt>PartialResultException</tt> if thrown, for compatibility with Active Directory
180      * (see {@link LdapTemplate#setIgnorePartialResultException(boolean)}).
181      *
182      * @param base the search base, relative to the base context supplied by the context source.
183      * @param filter the LDAP search filter
184      * @param params parameters to be substituted in the search.
185      *
186      * @return a DirContextOperations instance created from the matching entry.
187      *
188      * @throws IncorrectResultSizeDataAccessException if no results are found or the search returns more than one
189      *         result.
190      */
191     public DirContextOperations searchForSingleEntry(final String base, final String filter, final Object[] params) {
192 
193         return (DirContextOperations) executeReadOnly(new ContextExecutor() {
194                 public Object executeWithContext(DirContext ctx) throws NamingException {
195                     DistinguishedName ctxBaseDn = new DistinguishedName(ctx.getNameInNamespace());
196                     NamingEnumeration resultsEnum = ctx.search(base, filter, params, searchControls);
197                     Set results = new HashSet();
198                     try {
199                         while (resultsEnum.hasMore()) {
200 
201                             SearchResult searchResult = (SearchResult) resultsEnum.next();
202                             // Work out the DN of the matched entry
203                             StringBuffer dn = new StringBuffer(searchResult.getName());
204 
205                             if (base.length() > 0) {
206                                 dn.append(",");
207                                 dn.append(base);
208                             }
209 
210                             results.add(new DirContextAdapter(searchResult.getAttributes(),
211                                     new DistinguishedName(dn.toString()), ctxBaseDn));
212                         }
213                     } catch (PartialResultException e) {
214                         logger.info("Ignoring PartialResultException");
215                     }
216 
217                     if (results.size() == 0) {
218                         throw new IncorrectResultSizeDataAccessException(1, 0);
219                     }
220 
221                     if (results.size() > 1) {
222                         throw new IncorrectResultSizeDataAccessException(1, results.size());
223                     }
224 
225                     return results.toArray()[0];
226                 }
227             });
228     }
229 
230     /**
231      * Sets the search controls which will be used for search operations by the template.
232      *
233      * @param searchControls the SearchControls instance which will be cached in the template.
234      */
235     public void setSearchControls(SearchControls searchControls) {
236         this.searchControls = searchControls;
237     }
238 }