001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2012-2015 ForgeRock AS. 015 */ 016package org.forgerock.opendj.rest2ldap; 017 018import java.util.ArrayList; 019import java.util.LinkedHashSet; 020import java.util.LinkedList; 021import java.util.List; 022import java.util.Set; 023import java.util.concurrent.atomic.AtomicInteger; 024import java.util.concurrent.atomic.AtomicReference; 025 026import org.forgerock.json.fluent.JsonPointer; 027import org.forgerock.json.fluent.JsonValue; 028import org.forgerock.json.resource.BadRequestException; 029import org.forgerock.json.resource.ResourceException; 030import org.forgerock.json.resource.ResultHandler; 031import org.forgerock.opendj.ldap.Attribute; 032import org.forgerock.opendj.ldap.AttributeDescription; 033import org.forgerock.opendj.ldap.ByteString; 034import org.forgerock.opendj.ldap.DN; 035import org.forgerock.opendj.ldap.Entry; 036import org.forgerock.opendj.ldap.EntryNotFoundException; 037import org.forgerock.opendj.ldap.LdapException; 038import org.forgerock.opendj.ldap.Filter; 039import org.forgerock.opendj.ldap.LinkedAttribute; 040import org.forgerock.opendj.ldap.MultipleEntriesFoundException; 041import org.forgerock.opendj.ldap.ResultCode; 042import org.forgerock.opendj.ldap.SearchResultHandler; 043import org.forgerock.opendj.ldap.SearchScope; 044import org.forgerock.opendj.ldap.requests.SearchRequest; 045import org.forgerock.opendj.ldap.responses.Result; 046import org.forgerock.opendj.ldap.responses.SearchResultEntry; 047import org.forgerock.opendj.ldap.responses.SearchResultReference; 048import org.forgerock.util.Function; 049import org.forgerock.util.promise.NeverThrowsException; 050import org.forgerock.util.promise.ExceptionHandler; 051 052import static org.forgerock.opendj.ldap.LdapException.*; 053import static org.forgerock.opendj.ldap.requests.Requests.*; 054import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*; 055import static org.forgerock.opendj.rest2ldap.Utils.*; 056 057/** 058 * An attribute mapper which provides a mapping from a JSON value to a single DN 059 * valued LDAP attribute. 060 */ 061public final class ReferenceAttributeMapper extends AbstractLDAPAttributeMapper<ReferenceAttributeMapper> { 062 /** 063 * The maximum number of candidate references to allow in search filters. 064 */ 065 private static final int SEARCH_MAX_CANDIDATES = 1000; 066 067 private final DN baseDN; 068 private Filter filter; 069 private final AttributeMapper mapper; 070 private final AttributeDescription primaryKey; 071 private SearchScope scope = SearchScope.WHOLE_SUBTREE; 072 073 ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN, 074 final AttributeDescription primaryKey, final AttributeMapper mapper) { 075 super(ldapAttributeName); 076 this.baseDN = baseDN; 077 this.primaryKey = primaryKey; 078 this.mapper = mapper; 079 } 080 081 /** 082 * Sets the filter which should be used when searching for referenced LDAP 083 * entries. The default is {@code (objectClass=*)}. 084 * 085 * @param filter 086 * The filter which should be used when searching for referenced 087 * LDAP entries. 088 * @return This attribute mapper. 089 */ 090 public ReferenceAttributeMapper searchFilter(final Filter filter) { 091 this.filter = ensureNotNull(filter); 092 return this; 093 } 094 095 /** 096 * Sets the filter which should be used when searching for referenced LDAP 097 * entries. The default is {@code (objectClass=*)}. 098 * 099 * @param filter 100 * The filter which should be used when searching for referenced 101 * LDAP entries. 102 * @return This attribute mapper. 103 */ 104 public ReferenceAttributeMapper searchFilter(final String filter) { 105 return searchFilter(Filter.valueOf(filter)); 106 } 107 108 /** 109 * Sets the search scope which should be used when searching for referenced 110 * LDAP entries. The default is {@link SearchScope#WHOLE_SUBTREE}. 111 * 112 * @param scope 113 * The search scope which should be used when searching for 114 * referenced LDAP entries. 115 * @return This attribute mapper. 116 */ 117 public ReferenceAttributeMapper searchScope(final SearchScope scope) { 118 this.scope = ensureNotNull(scope); 119 return this; 120 } 121 122 @Override 123 public String toString() { 124 return "reference(" + ldapAttributeName + ")"; 125 } 126 127 @Override 128 void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, final FilterType type, 129 final String operator, final Object valueAssertion, final ResultHandler<Filter> h) { 130 // Construct a filter which can be used to find referenced resources. 131 mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion, new ResultHandler<Filter>() { 132 @Override 133 public void handleError(final ResourceException error) { 134 h.handleError(error); // Propagate. 135 } 136 137 @Override 138 public void handleResult(final Filter result) { 139 // Search for all referenced entries and construct a filter. 140 final SearchRequest request = createSearchRequest(result); 141 final List<Filter> subFilters = new LinkedList<>(); 142 143 final ExceptionHandler<LdapException> exceptionHandler = new ExceptionHandler<LdapException>() { 144 @Override 145 public void handleException(LdapException exception) { 146 h.handleError(asResourceException(exception)); // Propagate. 147 } 148 }; 149 150 c.getConnection().searchAsync(request, new SearchResultHandler() { 151 @Override 152 public boolean handleEntry(final SearchResultEntry entry) { 153 if (subFilters.size() < SEARCH_MAX_CANDIDATES) { 154 subFilters.add(Filter.equality(ldapAttributeName.toString(), entry.getName())); 155 return true; 156 } else { 157 // No point in continuing - maximum candidates reached. 158 return false; 159 } 160 } 161 162 @Override 163 public boolean handleReference(final SearchResultReference reference) { 164 // Ignore references. 165 return true; 166 } 167 }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() { 168 @Override 169 public void handleResult(Result result) { 170 if (subFilters.size() >= SEARCH_MAX_CANDIDATES) { 171 exceptionHandler.handleException(newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED)); 172 } else if (subFilters.size() == 1) { 173 h.handleResult(subFilters.get(0)); 174 } else { 175 h.handleResult(Filter.or(subFilters)); 176 } 177 } 178 }).thenOnException(exceptionHandler); 179 } 180 }); 181 } 182 183 @Override 184 void getNewLDAPAttributes(final Context c, final JsonPointer path, final List<Object> newValues, 185 final ResultHandler<Attribute> h) { 186 /* 187 * For each value use the subordinate mapper to obtain the LDAP primary 188 * key, the perform a search for each one to find the corresponding entries. 189 */ 190 final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName); 191 final AtomicInteger pendingSearches = new AtomicInteger(newValues.size()); 192 final AtomicReference<ResourceException> exception = new AtomicReference<>(); 193 194 for (final Object value : newValues) { 195 mapper.create(c, path, new JsonValue(value), new ResultHandler<List<Attribute>>() { 196 197 @Override 198 public void handleError(final ResourceException error) { 199 h.handleError(error); 200 } 201 202 @Override 203 public void handleResult(final List<Attribute> result) { 204 Attribute primaryKeyAttribute = null; 205 for (final Attribute attribute : result) { 206 if (attribute.getAttributeDescription().equals(primaryKey)) { 207 primaryKeyAttribute = attribute; 208 break; 209 } 210 } 211 212 if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) { 213 h.handleError(new BadRequestException(i18n( 214 "The request cannot be processed because the reference " 215 + "field '%s' contains a value which does not contain " + "a primary key", path))); 216 return; 217 } 218 219 if (primaryKeyAttribute.size() > 1) { 220 h.handleError(new BadRequestException(i18n( 221 "The request cannot be processed because the reference " 222 + "field '%s' contains a value which contains multiple " + "primary keys", path))); 223 return; 224 } 225 226 // Now search for the referenced entry in to get its DN. 227 final ByteString primaryKeyValue = primaryKeyAttribute.firstValue(); 228 final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue); 229 final SearchRequest search = createSearchRequest(filter); 230 c.getConnection().searchSingleEntryAsync(search).thenOnResult( 231 new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() { 232 @Override 233 public void handleResult(final SearchResultEntry result) { 234 synchronized (newLDAPAttribute) { 235 newLDAPAttribute.add(result.getName()); 236 } 237 completeIfNecessary(); 238 } 239 }).thenOnException(new ExceptionHandler<LdapException>() { 240 @Override 241 public void handleException(final LdapException error) { 242 ResourceException re; 243 try { 244 throw error; 245 } catch (final EntryNotFoundException e) { 246 re = new BadRequestException(i18n( 247 "The request cannot be processed " + "because the resource '%s' " 248 + "referenced in field '%s' does " + "not exist", 249 primaryKeyValue.toString(), path)); 250 } catch (final MultipleEntriesFoundException e) { 251 re = new BadRequestException(i18n( 252 "The request cannot be processed " + "because the resource '%s' " 253 + "referenced in field '%s' is " + "ambiguous", 254 primaryKeyValue.toString(), path)); 255 } catch (final LdapException e) { 256 re = asResourceException(e); 257 } 258 exception.compareAndSet(null, re); 259 completeIfNecessary(); 260 } 261 }); 262 } 263 264 private void completeIfNecessary() { 265 if (pendingSearches.decrementAndGet() == 0) { 266 if (exception.get() != null) { 267 h.handleError(exception.get()); 268 } else { 269 h.handleResult(newLDAPAttribute); 270 } 271 } 272 } 273 }); 274 } 275 } 276 277 @Override 278 ReferenceAttributeMapper getThis() { 279 return this; 280 } 281 282 @Override 283 void read(final Context c, final JsonPointer path, final Entry e, final ResultHandler<JsonValue> h) { 284 final Attribute attribute = e.getAttribute(ldapAttributeName); 285 if (attribute == null || attribute.isEmpty()) { 286 h.handleResult(null); 287 } else if (attributeIsSingleValued()) { 288 try { 289 final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN(); 290 readEntry(c, path, dn, h); 291 } catch (final Exception ex) { 292 // The LDAP attribute could not be decoded. 293 h.handleError(asResourceException(ex)); 294 } 295 } else { 296 try { 297 final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN(); 298 final ResultHandler<JsonValue> handler = 299 accumulate(dns.size(), transform(new Function<List<JsonValue>, JsonValue, NeverThrowsException>() { 300 @Override 301 public JsonValue apply(final List<JsonValue> value) { 302 if (value.isEmpty()) { 303 /* 304 * No values, so omit the entire JSON object 305 * from the resource. 306 */ 307 return null; 308 } else { 309 // Combine values into a single JSON array. 310 final List<Object> result = new ArrayList<>(value.size()); 311 for (final JsonValue e : value) { 312 result.add(e.getObject()); 313 } 314 return new JsonValue(result); 315 } 316 } 317 }, h)); 318 for (final DN dn : dns) { 319 readEntry(c, path, dn, handler); 320 } 321 } catch (final Exception ex) { 322 // The LDAP attribute could not be decoded. 323 h.handleError(asResourceException(ex)); 324 } 325 } 326 } 327 328 private SearchRequest createSearchRequest(final Filter result) { 329 final Filter searchFilter = filter != null ? Filter.and(filter, result) : result; 330 return newSearchRequest(baseDN, scope, searchFilter, "1.1"); 331 } 332 333 private void readEntry(final Context c, final JsonPointer path, final DN dn, 334 final ResultHandler<JsonValue> handler) { 335 final Set<String> requestedLDAPAttributes = new LinkedHashSet<>(); 336 mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes); 337 c.getConnection().readEntryAsync(dn, requestedLDAPAttributes) 338 .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() { 339 @Override 340 public void handleResult(final SearchResultEntry result) { 341 mapper.read(c, path, result, handler); 342 } 343 }).thenOnException(new ExceptionHandler<LdapException>() { 344 @Override 345 public void handleException(final LdapException error) { 346 if (!(error instanceof EntryNotFoundException)) { 347 handler.handleError(asResourceException(error)); 348 } else { 349 /* 350 * The referenced entry does not exist so ignore it 351 * since it cannot be mapped. 352 */ 353 handler.handleResult(null); 354 } 355 } 356 }); 357 } 358 359}