001: /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
002: *
003: * Licensed under the Apache License, Version 2.0 (the "License");
004: * you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015:
016: package org.acegisecurity.ldap;
017:
018: import org.springframework.dao.DataAccessException;
019: import org.springframework.dao.IncorrectResultSizeDataAccessException;
020:
021: import org.springframework.util.Assert;
022: import org.springframework.util.StringUtils;
023:
024: import java.util.HashSet;
025: import java.util.Set;
026:
027: import javax.naming.NameNotFoundException;
028: import javax.naming.NamingEnumeration;
029: import javax.naming.NamingException;
030: import javax.naming.Context;
031: import javax.naming.directory.Attribute;
032: import javax.naming.directory.Attributes;
033: import javax.naming.directory.DirContext;
034: import javax.naming.directory.SearchControls;
035: import javax.naming.directory.SearchResult;
036:
037: /**
038: * LDAP equivalent of the Spring JdbcTemplate class.<p>This is mainly intended to simplify Ldap access within Acegi
039: * Security's LDAP-related services.</p>
040: *
041: * @author Ben Alex
042: * @author Luke Taylor
043: */
044: public class LdapTemplate {
045: //~ Static fields/initializers =====================================================================================
046:
047: public static final String[] NO_ATTRS = new String[0];
048:
049: //~ Instance fields ================================================================================================
050:
051: private InitialDirContextFactory dirContextFactory;
052: private NamingExceptionTranslator exceptionTranslator = new LdapExceptionTranslator();
053:
054: /** Default search controls */
055: private SearchControls searchControls = new SearchControls();
056: private String password = null;
057: private String principalDn = null;
058:
059: //~ Constructors ===================================================================================================
060:
061: public LdapTemplate(InitialDirContextFactory dirContextFactory) {
062: Assert.notNull(dirContextFactory,
063: "An InitialDirContextFactory is required");
064: this .dirContextFactory = dirContextFactory;
065:
066: searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
067: }
068:
069: /**
070: *
071: * @param dirContextFactory the source of DirContexts
072: * @param userDn the user name to authenticate as when obtaining new contexts
073: * @param password the user's password
074: */
075: public LdapTemplate(InitialDirContextFactory dirContextFactory,
076: String userDn, String password) {
077: this (dirContextFactory);
078:
079: Assert.hasLength(userDn, "userDn must not be null or empty");
080: Assert.notNull(password, "password cannot be null");
081:
082: this .principalDn = userDn;
083: this .password = password;
084: }
085:
086: //~ Methods ========================================================================================================
087:
088: /**
089: * Performs an LDAP compare operation of the value of an attribute for a particular directory entry.
090: *
091: * @param dn the entry who's attribute is to be used
092: * @param attributeName the attribute who's value we want to compare
093: * @param value the value to be checked against the directory value
094: *
095: * @return true if the supplied value matches that in the directory
096: */
097: public boolean compare(final String dn, final String attributeName,
098: final Object value) {
099: final String comparisonFilter = "(" + attributeName + "={0})";
100:
101: class LdapCompareCallback implements LdapCallback {
102: public Object doInDirContext(DirContext ctx)
103: throws NamingException {
104: SearchControls ctls = new SearchControls();
105: ctls.setReturningAttributes(NO_ATTRS);
106: ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
107:
108: String relativeName = LdapUtils
109: .getRelativeName(dn, ctx);
110:
111: NamingEnumeration results = ctx.search(relativeName,
112: comparisonFilter, new Object[] { value }, ctls);
113:
114: return Boolean.valueOf(results.hasMore());
115: }
116: }
117:
118: Boolean matches = (Boolean) execute(new LdapCompareCallback());
119:
120: return matches.booleanValue();
121: }
122:
123: public Object execute(LdapCallback callback)
124: throws DataAccessException {
125: DirContext ctx = null;
126:
127: try {
128: ctx = (principalDn == null) ? dirContextFactory
129: .newInitialDirContext() : dirContextFactory
130: .newInitialDirContext(principalDn, password);
131:
132: return callback.doInDirContext(ctx);
133: } catch (NamingException exception) {
134: throw exceptionTranslator.translate("LdapCallback",
135: exception);
136: } finally {
137: LdapUtils.closeContext(ctx);
138: }
139: }
140:
141: public boolean nameExists(final String dn) {
142: Boolean exists = (Boolean) execute(new LdapCallback() {
143: public Object doInDirContext(DirContext ctx)
144: throws NamingException {
145: try {
146: Object obj = ctx.lookup(LdapUtils.getRelativeName(
147: dn, ctx));
148: if (obj instanceof Context) {
149: LdapUtils.closeContext((Context) obj);
150: }
151:
152: } catch (NameNotFoundException nnfe) {
153: return Boolean.FALSE;
154: }
155:
156: return Boolean.TRUE;
157: }
158: });
159:
160: return exists.booleanValue();
161: }
162:
163: /**
164: * Composes an object from the attributes of the given DN.
165: *
166: * @param dn the directory entry which will be read
167: * @param mapper maps the attributes to the required object
168: * @param attributesToRetrieve the named attributes which will be retrieved from the directory entry.
169: *
170: * @return the object created by the mapper
171: */
172: public Object retrieveEntry(final String dn,
173: final LdapEntryMapper mapper,
174: final String[] attributesToRetrieve) {
175: return execute(new LdapCallback() {
176: public Object doInDirContext(DirContext ctx)
177: throws NamingException {
178: return mapper.mapAttributes(dn, ctx.getAttributes(
179: LdapUtils.getRelativeName(dn, ctx),
180: attributesToRetrieve));
181: }
182: });
183: }
184:
185: /**
186: * Performs a search using the supplied filter and returns the union of the values of the named attribute
187: * found in all entries matched by the search. Note that one directory entry may have several values for the
188: * attribute. Intended for role searches and similar scenarios.
189: *
190: * @param base the DN to search in
191: * @param filter search filter to use
192: * @param params the parameters to substitute in the search filter
193: * @param attributeName the attribute who's values are to be retrieved.
194: *
195: * @return the set of String values for the attribute as a union of the values found in all the matching entries.
196: */
197: public Set searchForSingleAttributeValues(final String base,
198: final String filter, final Object[] params,
199: final String attributeName) {
200: class SingleAttributeSearchCallback implements LdapCallback {
201: public Object doInDirContext(DirContext ctx)
202: throws NamingException {
203: Set unionOfValues = new HashSet();
204:
205: // We're only interested in a single attribute for this method, so we make a copy of
206: // the search controls and override the returningAttributes property
207: SearchControls ctls = new SearchControls();
208:
209: ctls.setSearchScope(searchControls.getSearchScope());
210: ctls.setTimeLimit(searchControls.getTimeLimit());
211: ctls
212: .setDerefLinkFlag(searchControls
213: .getDerefLinkFlag());
214: ctls
215: .setReturningAttributes(new String[] { attributeName });
216:
217: NamingEnumeration matchingEntries = ctx.search(base,
218: filter, params, ctls);
219:
220: while (matchingEntries.hasMore()) {
221: SearchResult result = (SearchResult) matchingEntries
222: .next();
223: Attributes attrs = result.getAttributes();
224:
225: // There should only be one attribute in each matching entry.
226: NamingEnumeration returnedAttributes = attrs
227: .getAll();
228:
229: while (returnedAttributes.hasMore()) {
230: Attribute returnedAttribute = (Attribute) returnedAttributes
231: .next();
232: NamingEnumeration attributeValues = returnedAttribute
233: .getAll();
234:
235: while (attributeValues.hasMore()) {
236: Object value = attributeValues.next();
237:
238: unionOfValues.add(value.toString());
239: }
240: }
241: }
242:
243: return unionOfValues;
244: }
245: }
246:
247: return (Set) execute(new SingleAttributeSearchCallback());
248: }
249:
250: /**
251: * Performs a search, with the requirement that the search shall return a single directory entry, and uses
252: * the supplied mapper to create the object from that entry.
253: *
254: * @param base
255: * @param filter
256: * @param params
257: * @param mapper
258: *
259: * @return the object created by the mapper from the matching entry
260: *
261: * @throws IncorrectResultSizeDataAccessException if no results are found or the search returns more than one
262: * result.
263: */
264: public Object searchForSingleEntry(final String base,
265: final String filter, final Object[] params,
266: final LdapEntryMapper mapper) {
267: return execute(new LdapCallback() {
268: public Object doInDirContext(DirContext ctx)
269: throws NamingException {
270: NamingEnumeration results = ctx.search(base, filter,
271: params, searchControls);
272:
273: if (!results.hasMore()) {
274: throw new IncorrectResultSizeDataAccessException(1,
275: 0);
276: }
277:
278: SearchResult searchResult = (SearchResult) results
279: .next();
280:
281: if (results.hasMore()) {
282: // We don't know how many results but set to 2 which is good enough
283: throw new IncorrectResultSizeDataAccessException(1,
284: 2);
285: }
286:
287: // Work out the DN of the matched entry
288: StringBuffer dn = new StringBuffer(searchResult
289: .getName());
290:
291: if (base.length() > 0) {
292: dn.append(",");
293: dn.append(base);
294: }
295:
296: String nameInNamespace = ctx.getNameInNamespace();
297:
298: if (StringUtils.hasLength(nameInNamespace)) {
299: dn.append(",");
300: dn.append(nameInNamespace);
301: }
302:
303: return mapper.mapAttributes(dn.toString(), searchResult
304: .getAttributes());
305: }
306: });
307: }
308:
309: /**
310: * Sets the search controls which will be used for search operations by the template.
311: *
312: * @param searchControls the SearchControls instance which will be cached in the template.
313: */
314: public void setSearchControls(SearchControls searchControls) {
315: this .searchControls = searchControls;
316: }
317:
318: //~ Inner Classes ==================================================================================================
319:
320: private static class LdapExceptionTranslator implements
321: NamingExceptionTranslator {
322: public DataAccessException translate(String task,
323: NamingException e) {
324: return new LdapDataAccessException(task + ";"
325: + e.getMessage(), e);
326: }
327: }
328: }
|