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.DN; 047import org.forgerock.opendj.ldap.DecodeOptions; 048import org.forgerock.opendj.ldap.EntryNotFoundException; 049import org.forgerock.opendj.ldap.LdapException; 050import org.forgerock.opendj.ldap.MultipleEntriesFoundException; 051import org.forgerock.opendj.ldap.ResultCode; 052import org.forgerock.opendj.ldap.TimeoutResultException; 053import org.forgerock.opendj.ldap.schema.Schema; 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 baseDN 235 * The search base DN for performing reverse lookups. 236 * @param primaryKey 237 * The search primary key LDAP attribute to use for performing reverse lookups. 238 * @param mapper 239 * An property mapper which will be used to map LDAP attributes in the referenced entry. 240 * @return The property mapper. 241 */ 242 public static ReferencePropertyMapper reference(final AttributeDescription attribute, final DN baseDN, 243 final AttributeDescription primaryKey, 244 final PropertyMapper mapper) { 245 return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDN, primaryKey, mapper); 246 } 247 248 /** 249 * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute. 250 * 251 * @param attribute 252 * The DN valued LDAP attribute to be mapped. 253 * @param baseDN 254 * The search base DN for performing reverse lookups. 255 * @param primaryKey 256 * The search primary key LDAP attribute to use for performing reverse lookups. 257 * @param mapper 258 * An property mapper which will be used to map LDAP attributes in the referenced entry. 259 * @return The property mapper. 260 */ 261 public static ReferencePropertyMapper reference(final String attribute, final String baseDN, 262 final String primaryKey, final PropertyMapper mapper) { 263 return reference(AttributeDescription.valueOf(attribute), 264 DN.valueOf(baseDN), 265 AttributeDescription.valueOf(primaryKey), 266 mapper); 267 } 268 269 /** 270 * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. 271 * 272 * @param attribute 273 * The LDAP attribute to be mapped. 274 * @return The property mapper. 275 */ 276 public static SimplePropertyMapper simple(final AttributeDescription attribute) { 277 return new SimplePropertyMapper(attribute); 278 } 279 280 /** 281 * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. 282 * 283 * @param attribute 284 * The LDAP attribute to be mapped. 285 * @return The property mapper. 286 */ 287 public static SimplePropertyMapper simple(final String attribute) { 288 return simple(AttributeDescription.valueOf(attribute)); 289 } 290 291 /** 292 * Adapts a {@code Throwable} to a {@code ResourceException}. If the {@code Throwable} is an LDAP 293 * {@link LdapException} then an appropriate {@code ResourceException} is returned, otherwise an {@code 294 * InternalServerErrorException} is returned. 295 * @param t 296 * The {@code Throwable} to be converted. 297 * @return The equivalent resource exception. 298 */ 299 public static ResourceException asResourceException(final Throwable t) { 300 try { 301 throw t; 302 } catch (final ResourceException e) { 303 return e; 304 } catch (final AssertionFailureException e) { 305 return new PreconditionFailedException(e); 306 } catch (final ConstraintViolationException e) { 307 final ResultCode rc = e.getResult().getResultCode(); 308 if (rc.equals(ENTRY_ALREADY_EXISTS)) { 309 return new PreconditionFailedException(e); // Consistent with MVCC. 310 } else { 311 return new BadRequestException(e); // Schema violation, etc. 312 } 313 } catch (final AuthenticationException e) { 314 return new PermanentException(401, null, e); // Unauthorized 315 } catch (final AuthorizationException e) { 316 return new ForbiddenException(e); 317 } catch (final ConnectionException e) { 318 return new ServiceUnavailableException(e); 319 } catch (final EntryNotFoundException e) { 320 return new NotFoundException(e); 321 } catch (final MultipleEntriesFoundException e) { 322 return new InternalServerErrorException(e); 323 } catch (final TimeoutResultException e) { 324 return new RetryableException(408, null, e); // Request Timeout 325 } catch (final LdapException e) { 326 final ResultCode rc = e.getResult().getResultCode(); 327 if (rc.equals(ADMIN_LIMIT_EXCEEDED) || rc.equals(SIZE_LIMIT_EXCEEDED)) { 328 return new PermanentException(413, null, e); // Payload Too Large (Request Entity Too Large) 329 } else { 330 return new InternalServerErrorException(e); 331 } 332 } catch (final Throwable tmp) { 333 return new InternalServerErrorException(t); 334 } 335 } 336 337 private final Map<String, Resource> resources = new LinkedHashMap<>(); 338 private final Options options; 339 340 private Rest2Ldap(final Options options, final Collection<Resource> resources) { 341 this.options = options; 342 for (final Resource resource : resources) { 343 this.resources.put(resource.getResourceId(), resource); 344 } 345 // Now build the model. 346 for (final Resource resource : resources) { 347 resource.build(this); 348 } 349 } 350 351 /** 352 * Returns a {@link RequestHandler} which will handle requests to the named resource and any of its sub-resources. 353 * 354 * @param resourceId 355 * The resource ID. 356 * @return A {@link RequestHandler} which will handle requests to the named resource. 357 */ 358 public RequestHandler newRequestHandlerFor(final String resourceId) { 359 Reject.ifTrue(!resources.containsKey(resourceId), "unrecognized resource '" + resourceId + "'"); 360 final SubResourceSingleton root = singletonOf(resourceId); 361 root.build(this, null); 362 return root.addRoutes(new Router()); 363 } 364 365 Options getOptions() { 366 return options; 367 } 368 369 Resource getResource(final String resourceId) { 370 return resources.get(resourceId); 371 } 372}