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 2016 ForgeRock AS. 015 * 016 */ 017package org.forgerock.opendj.rest2ldap; 018 019import static org.forgerock.opendj.ldap.ResultCode.ADMIN_LIMIT_EXCEEDED; 020import static org.forgerock.opendj.ldap.ResultCode.ENTRY_ALREADY_EXISTS; 021import static org.forgerock.opendj.ldap.ResultCode.SIZE_LIMIT_EXCEEDED; 022import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; 023 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.LinkedHashMap; 027import java.util.Map; 028 029import org.forgerock.json.resource.BadRequestException; 030import org.forgerock.json.resource.ForbiddenException; 031import org.forgerock.json.resource.InternalServerErrorException; 032import org.forgerock.json.resource.NotFoundException; 033import org.forgerock.json.resource.PermanentException; 034import org.forgerock.json.resource.PreconditionFailedException; 035import org.forgerock.json.resource.RequestHandler; 036import org.forgerock.json.resource.ResourceException; 037import org.forgerock.json.resource.RetryableException; 038import org.forgerock.json.resource.Router; 039import org.forgerock.json.resource.ServiceUnavailableException; 040import org.forgerock.opendj.ldap.AssertionFailureException; 041import org.forgerock.opendj.ldap.AttributeDescription; 042import org.forgerock.opendj.ldap.AuthenticationException; 043import org.forgerock.opendj.ldap.AuthorizationException; 044import org.forgerock.opendj.ldap.ConnectionException; 045import org.forgerock.opendj.ldap.ConstraintViolationException; 046import org.forgerock.opendj.ldap.DecodeOptions; 047import org.forgerock.opendj.ldap.EntryNotFoundException; 048import org.forgerock.opendj.ldap.LdapException; 049import org.forgerock.opendj.ldap.MultipleEntriesFoundException; 050import org.forgerock.opendj.ldap.ResultCode; 051import org.forgerock.opendj.ldap.TimeoutResultException; 052import org.forgerock.opendj.ldap.schema.Schema; 053import org.forgerock.services.context.Context; 054import org.forgerock.util.Option; 055import org.forgerock.util.Options; 056import org.forgerock.util.Reject; 057 058/** 059 * Provides methods for constructing Rest2Ldap protocol gateways. Applications construct a new Rest2Ldap 060 * instance by calling {@link #rest2Ldap} passing in a list of {@link Resource resources} which together define 061 * the data model being exposed by the gateway. Call {@link #newRequestHandlerFor(String)} in order to obtain 062 * a request handler for a specific resource. The methods in this class can be categorized as follows: 063 * <p/> 064 * Creating Rest2Ldap gateways: 065 * <ul> 066 * <li>{@link #rest2Ldap} - creates a gateway for a given set of resources</li> 067 * <li>{@link #newRequestHandlerFor} - obtains a request handler for the specified endpoint resource.</li> 068 * </ul> 069 * <p/> 070 * Defining resource types, e.g. users, groups, devices, etc: 071 * <ul> 072 * <li>{@link #resource} - creates a resource having a fluent API for defining additional characteristics 073 * such as the resource's inheritance, sub-resources, and properties</li> 074 * </ul> 075 * <p/> 076 * Defining a resource's sub-resources. A sub-resource is a resource which is subordinate to another resource. Or, to 077 * put it another way, sub-resources define parent child relationships where the life-cycle of a child resource is 078 * constrained by the life-cycle of the parent: deleting the parent implies that all children are deleted as well. An 079 * example of a sub-resource is a subscriber having one or more devices: 080 * <ul> 081 * <li>{@link #collectionOf} - creates a one-to-many relationship. Collections support creation, deletion, 082 * and querying of child resources</li> 083 * <li>{@link #singletonOf} - creates a one-to-one relationship. Singletons cannot be created or destroyed, 084 * although they may be modified if they have properties which are modifiable. Singletons are usually only used as 085 * top-level entry points into REST APIs. 086 * </li> 087 * </ul> 088 * <p/> 089 * Defining a resource's properties: 090 * <ul> 091 * <li>{@link #resourceType} - defines a property whose JSON value will be the name of the resource, e.g. "user"</li> 092 * <li>{@link #simple} - defines a property which maps a JSON value to a single LDAP attribute</li> 093 * <li>{@link #object} - defines a property which is a JSON object having zero or more nested properties</li> 094 * <li>{@link #reference} - defines a property whose JSON value is a reference to another resource. Use these for 095 * mapping LDAP attributes which contain the DN of another LDAP entry exposed by Rest2Ldap. For example, a user's 096 * "manager" attribute or the members of a group.</li> 097 * </ul> 098 */ 099public final class Rest2Ldap { 100 /** 101 * Specifies the LDAP decoding options which should be used when decoding LDAP DNs, attribute types, and controls. 102 * By default Rest2Ldap will use a set of options of will always use the default schema. 103 */ 104 public static final Option<DecodeOptions> DECODE_OPTIONS = Option.withDefault(new DecodeOptions()); 105 /** 106 * Specifies whether Rest2Ldap should support multi-version concurrency control (MVCC) through the use of an MVCC 107 * LDAP {@link #MVCC_ATTRIBUTE attribute} such as "etag". By default Rest2Ldap will use MVCC. 108 */ 109 public static final Option<Boolean> USE_MVCC = Option.withDefault(true); 110 /** 111 * Specifies the name of the LDAP attribute which should be used for multi-version concurrency control (MVCC) if 112 * {@link #USE_MVCC enabled}. By default Rest2Ldap will use the "etag" operational attribute. 113 */ 114 public static final Option<String> MVCC_ATTRIBUTE = Option.withDefault("etag"); 115 /** 116 * Specifies the policy which should be used in order to read an entry before it is deleted, or after it is added or 117 * modified. By default Rest2Ldap will use the {@link ReadOnUpdatePolicy#CONTROLS controls} read on update policy. 118 */ 119 public static final Option<ReadOnUpdatePolicy> READ_ON_UPDATE_POLICY = Option.withDefault(CONTROLS); 120 /** 121 * Specifies whether Rest2Ldap should perform LDAP modify operations using the LDAP permissive modify 122 * control. By default Rest2Ldap will use the permissive modify control and use of the control is strongly 123 * recommended. 124 */ 125 public static final Option<Boolean> USE_PERMISSIVE_MODIFY = Option.withDefault(true); 126 /** 127 * Specifies whether Rest2Ldap should perform LDAP delete operations using the LDAP subtree delete control. By 128 * default Rest2Ldap will use the subtree delete control and use of the control is strongly recommended. 129 */ 130 public static final Option<Boolean> USE_SUBTREE_DELETE = Option.withDefault(true); 131 132 /** 133 * Creates a new {@link Rest2Ldap} instance using the provided options and {@link Resource resources}. 134 * Applications should call {@link #newRequestHandlerFor(String)} to obtain a request handler for a specific 135 * resource. 136 * <p> 137 * The supported options are defined in this class. 138 * 139 * @param options The configuration options for interactions with the backend LDAP server. The set of available 140 * options are provided in this class. 141 * @param resources The list of resources. 142 * @return A new Rest2Ldap instance from which REST request handlers can be obtained. 143 */ 144 public static Rest2Ldap rest2Ldap(final Options options, final Collection<Resource> resources) { 145 return new Rest2Ldap(options, resources); 146 } 147 148 /** 149 * Creates a new {@link Rest2Ldap} instance using the provided options and {@link Resource resources}. 150 * Applications should call {@link #newRequestHandlerFor(String)} to obtain a request handler for a specific 151 * resource. 152 * <p> 153 * The supported options are defined in this class. 154 * 155 * @param options The configuration options for interactions with the backend LDAP server. The set of available 156 * options are provided in this class. 157 * @param resources The list of resources. 158 * @return A new Rest2Ldap instance from which REST request handlers can be obtained. 159 */ 160 public static Rest2Ldap rest2Ldap(final Options options, final Resource... resources) { 161 return rest2Ldap(options, Arrays.asList(resources)); 162 } 163 164 /** 165 * Creates a new {@link Resource resource} definition with the provided resource ID. 166 * 167 * @param resourceId 168 * The resource ID. 169 * @return A new resource definition with the provided resource ID. 170 */ 171 public static Resource resource(final String resourceId) { 172 return new Resource(resourceId); 173 } 174 175 /** 176 * Creates a new {@link SubResourceCollection collection} sub-resource definition whose members will be resources 177 * having the provided resource ID or its sub-types. 178 * 179 * @param resourceId 180 * The type of resource contained in the sub-resource collection. 181 * @return A new sub-resource definition with the provided resource ID. 182 */ 183 public static SubResourceCollection collectionOf(final String resourceId) { 184 return new SubResourceCollection(resourceId); 185 } 186 187 /** 188 * Creates a new {@link SubResourceSingleton singleton} sub-resource definition which will reference a single 189 * resource having the specified resource ID. 190 * 191 * @param resourceId 192 * The type of resource referenced by the sub-resource singleton. 193 * @return A new sub-resource definition with the provided resource ID. 194 */ 195 public static SubResourceSingleton singletonOf(final String resourceId) { 196 return new SubResourceSingleton(resourceId); 197 } 198 199 /** 200 * Returns a property mapper which maps a JSON property containing the resource type to its associated LDAP 201 * object classes. 202 * 203 * @return The property mapper. 204 */ 205 public static PropertyMapper resourceType() { 206 return ResourceTypePropertyMapper.INSTANCE; 207 } 208 209 /** 210 * Returns a property mapper which maps a single JSON attribute to a JSON constant. 211 * 212 * @param value 213 * The constant JSON value (a Boolean, Number, String, Map, or List). 214 * @return The property mapper. 215 */ 216 public static PropertyMapper constant(final Object value) { 217 return new JsonConstantPropertyMapper(value); 218 } 219 220 /** 221 * Returns a property mapper which maps JSON objects to LDAP attributes. 222 * 223 * @return The property mapper. 224 */ 225 public static ObjectPropertyMapper object() { 226 return new ObjectPropertyMapper(); 227 } 228 229 /** 230 * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute. 231 * 232 * @param attribute 233 * The DN valued LDAP attribute to be mapped. 234 * @param baseDnTemplate 235 * The DN template which will be used as the search base when performing reverse lookups. The DN template 236 * may include template parameters and also parent RDNs using ".." notation. For example, the DN template 237 * "ou=groups,..,.." specifies that the search base DN should be computed by appending the RDN 238 * "ou=groups" to the grand-parent of the current resource's LDAP entry. 239 * @param primaryKey 240 * The search primary key LDAP attribute to use for performing reverse lookups. 241 * @param mapper 242 * An property mapper which will be used to map LDAP attributes in the referenced entry. 243 * @return The property mapper. 244 */ 245 public static ReferencePropertyMapper reference(final AttributeDescription attribute, final String baseDnTemplate, 246 final AttributeDescription primaryKey, 247 final PropertyMapper mapper) { 248 return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDnTemplate, primaryKey, mapper); 249 } 250 251 /** 252 * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute. 253 * 254 * @param attribute 255 * The DN valued LDAP attribute to be mapped. 256 * @param baseDnTemplate 257 * The DN template which will be used as the search base when performing reverse lookups. The DN template 258 * may include template parameters and also parent RDNs using ".." notation. For example, the DN template 259 * "ou=groups,..,.." specifies that the search base DN should be computed by appending the RDN 260 * "ou=groups" to the grand-parent of the current resource's LDAP entry. 261 * @param primaryKey 262 * The search primary key LDAP attribute to use for performing reverse lookups. 263 * @param mapper 264 * An property mapper which will be used to map LDAP attributes in the referenced entry. 265 * @return The property mapper. 266 */ 267 public static ReferencePropertyMapper reference(final String attribute, final String baseDnTemplate, 268 final String primaryKey, final PropertyMapper mapper) { 269 return reference(AttributeDescription.valueOf(attribute), 270 baseDnTemplate, 271 AttributeDescription.valueOf(primaryKey), 272 mapper); 273 } 274 275 /** 276 * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. 277 * 278 * @param attribute 279 * The LDAP attribute to be mapped. 280 * @return The property mapper. 281 */ 282 public static SimplePropertyMapper simple(final AttributeDescription attribute) { 283 return new SimplePropertyMapper(attribute); 284 } 285 286 /** 287 * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. 288 * 289 * @param attribute 290 * The LDAP attribute to be mapped. 291 * @return The property mapper. 292 */ 293 public static SimplePropertyMapper simple(final String attribute) { 294 return simple(AttributeDescription.valueOf(attribute)); 295 } 296 297 /** 298 * Returns a property mapper which provides a mapping from a JSON value to a LDAP attribute having the JSON syntax. 299 * 300 * @param attribute 301 * The LDAP attribute to be mapped. 302 * @return The property mapper. 303 */ 304 public static JsonPropertyMapper json(final AttributeDescription attribute) { 305 return new JsonPropertyMapper(attribute); 306 } 307 308 /** 309 * Returns a property mapper which provides a mapping from a JSON value to a LDAP attribute having the JSON syntax. 310 * 311 * @param attribute 312 * The LDAP attribute to be mapped. 313 * @return The property mapper. 314 */ 315 public static JsonPropertyMapper json(final String attribute) { 316 return json(AttributeDescription.valueOf(attribute)); 317 } 318 319 /** 320 * Adapts a {@code Throwable} to a {@code ResourceException}. If the {@code Throwable} is an LDAP 321 * {@link LdapException} then an appropriate {@code ResourceException} is returned, otherwise an {@code 322 * InternalServerErrorException} is returned. 323 * @param t 324 * The {@code Throwable} to be converted. 325 * @return The equivalent resource exception. 326 */ 327 public static ResourceException asResourceException(final Throwable t) { 328 try { 329 throw t; 330 } catch (final ResourceException e) { 331 return e; 332 } catch (final AssertionFailureException e) { 333 return new PreconditionFailedException(e); 334 } catch (final ConstraintViolationException e) { 335 final ResultCode rc = e.getResult().getResultCode(); 336 if (rc.equals(ENTRY_ALREADY_EXISTS)) { 337 return new PreconditionFailedException(e); // Consistent with MVCC. 338 } else { 339 return new BadRequestException(e); // Schema violation, etc. 340 } 341 } catch (final AuthenticationException e) { 342 return new PermanentException(401, null, e); // Unauthorized 343 } catch (final AuthorizationException e) { 344 return new ForbiddenException(e); 345 } catch (final ConnectionException e) { 346 return new ServiceUnavailableException(e); 347 } catch (final EntryNotFoundException e) { 348 return new NotFoundException(e); 349 } catch (final MultipleEntriesFoundException e) { 350 return new InternalServerErrorException(e); 351 } catch (final TimeoutResultException e) { 352 return new RetryableException(408, null, e); // Request Timeout 353 } catch (final LdapException e) { 354 final ResultCode rc = e.getResult().getResultCode(); 355 if (rc.equals(ADMIN_LIMIT_EXCEEDED) || rc.equals(SIZE_LIMIT_EXCEEDED)) { 356 return new PermanentException(413, null, e); // Payload Too Large (Request Entity Too Large) 357 } else { 358 return new InternalServerErrorException(e); 359 } 360 } catch (final Throwable tmp) { 361 return new InternalServerErrorException(t); 362 } 363 } 364 365 private final Map<String, Resource> resources = new LinkedHashMap<>(); 366 private final Options options; 367 368 private Rest2Ldap(final Options options, final Collection<Resource> resources) { 369 this.options = options; 370 for (final Resource resource : resources) { 371 this.resources.put(resource.getResourceId(), resource); 372 } 373 // Now build the model. 374 for (final Resource resource : resources) { 375 resource.build(this); 376 } 377 } 378 379 /** 380 * Returns a {@link RequestHandler} which will handle requests to the named resource and any of its sub-resources. 381 * 382 * @param resourceId 383 * The resource ID. 384 * @return A {@link RequestHandler} which will handle requests to the named resource. 385 */ 386 public RequestHandler newRequestHandlerFor(final String resourceId) { 387 Reject.ifTrue(!resources.containsKey(resourceId), "unrecognized resource '" + resourceId + "'"); 388 final SubResourceSingleton root = singletonOf(resourceId); 389 root.build(this, null); 390 return rest2LdapContext(root.addRoutes(new Router())); 391 } 392 393 private RequestHandler rest2LdapContext(final RequestHandler delegate) { 394 return new DescribableRequestHandler(delegate) { 395 @Override 396 protected Context wrap(final Context context) { 397 return new Rest2LdapContext(context, Rest2Ldap.this); 398 } 399 }; 400 } 401 402 Options getOptions() { 403 return options; 404 } 405 406 Resource getResource(final String resourceId) { 407 return resources.get(resourceId); 408 } 409}