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