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