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.opends.server.protocols.http.rest2ldap;
018
019import static org.forgerock.http.routing.RouteMatchers.newResourceApiVersionBehaviourManager;
020import static org.forgerock.http.routing.Version.version;
021import static org.forgerock.json.resource.RouteMatchers.resourceApiVersionContextFilter;
022import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler;
023import static org.forgerock.opendj.ldap.schema.CoreSchema.getBooleanSyntax;
024import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
025import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
026import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.CREATE_ONLY;
027import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_ONLY;
028import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
029import static org.forgerock.util.Options.defaultOptions;
030import static org.opends.messages.ConfigMessages.ERR_BAD_ADMIN_API_RESOURCE_VERSION;
031
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037
038import org.forgerock.http.Handler;
039import org.forgerock.http.HttpApplication;
040import org.forgerock.http.HttpApplicationException;
041import org.forgerock.http.io.Buffer;
042import org.forgerock.http.routing.ResourceApiVersionBehaviourManager;
043import org.forgerock.http.routing.Version;
044import org.forgerock.json.JsonPointer;
045import org.forgerock.json.resource.BadRequestException;
046import org.forgerock.json.resource.FilterChain;
047import org.forgerock.json.resource.Request;
048import org.forgerock.json.resource.RequestHandler;
049import org.forgerock.json.resource.ResourceException;
050import org.forgerock.json.resource.Router;
051import org.forgerock.opendj.config.AbstractManagedObjectDefinition;
052import org.forgerock.opendj.config.AggregationPropertyDefinition;
053import org.forgerock.opendj.config.DefaultBehaviorProvider;
054import org.forgerock.opendj.config.DefinedDefaultBehaviorProvider;
055import org.forgerock.opendj.config.InstantiableRelationDefinition;
056import org.forgerock.opendj.config.LDAPProfile;
057import org.forgerock.opendj.config.ManagedObjectDefinition;
058import org.forgerock.opendj.config.ManagedObjectOption;
059import org.forgerock.opendj.config.PropertyDefinition;
060import org.forgerock.opendj.config.PropertyOption;
061import org.forgerock.opendj.config.RelationDefinition;
062import org.forgerock.opendj.config.RelationOption;
063import org.forgerock.opendj.config.SingletonRelationDefinition;
064import org.forgerock.opendj.config.TopCfgDefn;
065import org.forgerock.opendj.ldap.AttributeDescription;
066import org.forgerock.opendj.ldap.DN;
067import org.forgerock.opendj.ldap.Functions;
068import org.forgerock.opendj.ldap.schema.Syntax;
069import org.forgerock.opendj.rest2ldap.AbstractRequestHandler;
070import org.forgerock.opendj.rest2ldap.ReferencePropertyMapper;
071import org.forgerock.opendj.rest2ldap.Resource;
072import org.forgerock.opendj.rest2ldap.Rest2Ldap;
073import org.forgerock.opendj.rest2ldap.SimplePropertyMapper;
074import org.forgerock.opendj.rest2ldap.SubResourceCollection;
075import org.forgerock.opendj.rest2ldap.SubResourceSingleton;
076import org.forgerock.opendj.server.config.meta.GlobalCfgDefn;
077import org.forgerock.opendj.server.config.meta.RootCfgDefn;
078import org.forgerock.opendj.server.config.server.AdminEndpointCfg;
079import org.forgerock.services.context.Context;
080import org.forgerock.util.Factory;
081import org.forgerock.util.Function;
082import org.forgerock.util.promise.NeverThrowsException;
083import org.forgerock.util.promise.Promise;
084import org.opends.server.api.HttpEndpoint;
085import org.opends.server.core.ServerContext;
086import org.opends.server.types.InitializationException;
087
088/**
089 * An HTTP endpoint providing access to the server's monitoring backend (cn=monitor) and its configuration (cn=config).
090 */
091public final class AdminEndpoint extends HttpEndpoint<AdminEndpointCfg>
092{
093  private static final Version ADMIN_API_VERSION = version("1.0");
094  private static final String TYPE_PROPERTY = "_schema";
095  private static final String ADMIN_API = "admin-api";
096  private static final String MONITOR = "monitor";
097  private static final String CONFIG = "config";
098
099  /**
100   * Create a new AdminEndpoint with the supplied configuration.
101   *
102   * @param configuration
103   *          Configuration to use for the {@link HttpApplication}
104   * @param serverContext
105   *          Server of this LDAP server
106   */
107  public AdminEndpoint(AdminEndpointCfg configuration, ServerContext serverContext)
108  {
109    super(configuration, serverContext);
110  }
111
112  @Override
113  public HttpApplication newHttpApplication() throws InitializationException
114  {
115    return new AdminHttpApplication();
116  }
117
118  /**
119   * Specialized {@link HttpApplication} using internal connections to this local LDAP server.
120   */
121  private final class AdminHttpApplication implements HttpApplication
122  {
123    private LDAPProfile ldapProfile = LDAPProfile.getInstance();
124
125    @Override
126    public Handler start() throws HttpApplicationException
127    {
128      final Map<String, Resource> resources = new HashMap<>();
129
130      // Define the entry point to the admin API.
131      resources.put(ADMIN_API, resource(ADMIN_API).subResources(singletonOf(MONITOR).urlTemplate(MONITOR)
132                                                                                    .dnTemplate("cn=monitor")
133                                                                                    .isReadOnly(true),
134                                                                singletonOf(CONFIG).urlTemplate(CONFIG)
135                                                                                   .dnTemplate("cn=config")));
136
137      // Define the monitoring endpoint.
138      resources.put(MONITOR, resource(MONITOR).includeAllUserAttributesByDefault(true)
139                                              .excludedDefaultUserAttributes("objectClass", "cn")
140                                              .objectClass ("ds-monitor-entry")
141                                              .property("_id", simple("cn"))
142                                              .subResource(collectionOf(MONITOR).useClientDnNaming("cn")));
143
144      // Build the configuration endpoint using the configuration framework.
145      final TopCfgDefn topCfgDefn = TopCfgDefn.getInstance();
146      final RootCfgDefn rootCfgDefn = RootCfgDefn.getInstance();
147      final GlobalCfgDefn globalCfgDefn = GlobalCfgDefn.getInstance();
148
149      // The configuration framework exposes the root and global configuration as separate resources, but it would be
150      // nice if we exposed them as a single resource.
151      final Resource config = resource(CONFIG);
152      configureResourceProperties(globalCfgDefn, config);
153      configureResourceSubResources(rootCfgDefn, config, true);
154      resources.put(CONFIG, config);
155
156      resources.put(topCfgDefn.getName(), buildResource(topCfgDefn));
157      for (final AbstractManagedObjectDefinition<?, ?> mod : topCfgDefn.getAllChildren())
158      {
159        if (!mod.hasOption(ManagedObjectOption.HIDDEN) && mod != globalCfgDefn && mod != rootCfgDefn)
160        {
161          resources.put(mod.getName(), buildResource(mod));
162        }
163      }
164
165      // Now that all resources are defined, perform a second pass processing all relation definitions in order to
166      // identity which attributes should be used for the "_id" property.
167      for (final AbstractManagedObjectDefinition<?, ?> mod : topCfgDefn.getAllChildren())
168      {
169        for (final RelationDefinition<?, ?> rd : mod.getRelationDefinitions())
170        {
171          if (rd instanceof InstantiableRelationDefinition)
172          {
173            final InstantiableRelationDefinition<?, ?> ird = (InstantiableRelationDefinition) rd;
174            final AbstractManagedObjectDefinition<?, ?> d = rd.getChildDefinition();
175            final String rdnType = ldapProfile.getRelationChildRDNType(ird);
176            resources.get(d.getName()).property("_id", simple(rdnType).isRequired(true).writability(CREATE_ONLY));
177          }
178        }
179      }
180
181      final Rest2Ldap rest2Ldap = rest2Ldap(defaultOptions(), resources.values());
182      final RequestHandler handler = rest2Ldap.newRequestHandlerFor(ADMIN_API);
183      final Router versionRouter = new Router();
184      versionRouter.addRoute(ADMIN_API_VERSION, handler);
185      versionRouter.setDefaultRoute(new AbstractRequestHandler()
186      {
187        @Override
188        protected <V> Promise<V, ResourceException> handleRequest(final Context context, final Request request)
189        {
190          final String message = ERR_BAD_ADMIN_API_RESOURCE_VERSION.get(request.getResourceVersion(), ADMIN_API_VERSION)
191                                                                   .toString();
192          return new BadRequestException(message).asPromise();
193        }
194      });
195
196      // FIXME: Disable the warning header for now due to CREST-389 / CREST-390.
197      final ResourceApiVersionBehaviourManager behaviourManager = newResourceApiVersionBehaviourManager();
198      behaviourManager.setWarningEnabled(false);
199      return newHttpHandler(new FilterChain(versionRouter, resourceApiVersionContextFilter(behaviourManager)));
200    }
201
202    private Resource buildResource(final AbstractManagedObjectDefinition<?, ?> mod)
203    {
204      final Resource resource = resource(mod.getName());
205      configureResourceProperties(mod, resource);
206      configureResourceSubResources(mod, resource, false);
207      return resource;
208    }
209
210    private void configureResourceSubResources(final AbstractManagedObjectDefinition<?, ?> mod, final Resource resource,
211                                               final boolean removeCnEqualsConfig)
212    {
213      for (final RelationDefinition<?, ?> rd : mod.getRelationDefinitions())
214      {
215        if (rd.hasOption(RelationOption.HIDDEN))
216        {
217          continue;
218        }
219
220        if (rd instanceof InstantiableRelationDefinition)
221        {
222          final InstantiableRelationDefinition<?, ?> ird = (InstantiableRelationDefinition) rd;
223          final AbstractManagedObjectDefinition<?, ?> d = rd.getChildDefinition();
224          final SubResourceCollection collection = collectionOf(d.getName())
225                  .useClientDnNaming(ldapProfile.getRelationChildRDNType(ird))
226                  .urlTemplate(ird.getPluralName())
227                  .dnTemplate(getRelationRdnSequence(rd, removeCnEqualsConfig))
228                  .glueObjectClasses(ldapProfile.getRelationObjectClasses(rd).toArray(new String[0]));
229          resource.subResource(collection);
230        }
231        else if (rd instanceof SingletonRelationDefinition)
232        {
233          if (mod == RootCfgDefn.getInstance() && rd.getChildDefinition() == GlobalCfgDefn.getInstance())
234          {
235            // Special case: ignore the root -> global configuration relation because these two resources are merged
236            // into a single resource within the REST API.
237            continue;
238          }
239          final SubResourceSingleton singleton = singletonOf(rd.getChildDefinition().getName())
240                  .urlTemplate(rd.getName())
241                  .dnTemplate(getRelationRdnSequence(rd, removeCnEqualsConfig));
242          resource.subResource(singleton);
243        }
244        // Optional/set NYI
245      }
246    }
247
248    private String getRelationRdnSequence(final RelationDefinition<?, ?> rd, final boolean removeCnEqualsConfig)
249    {
250      final String rdnSequence = ldapProfile.getRelationRDNSequence(rd);
251      if (removeCnEqualsConfig)
252      {
253        final DN dn = DN.valueOf(rdnSequence);
254        return dn.localName(dn.size() - 1).toString();
255      }
256      return rdnSequence;
257    }
258
259    private void configureResourceProperties(final AbstractManagedObjectDefinition<?, ?> mod, final Resource resource)
260    {
261      resource.isAbstract(!(mod instanceof ManagedObjectDefinition));
262      if (mod.getParent() != null)
263      {
264        resource.superType(mod.getParent().getName());
265      }
266
267      final String objectClass = ldapProfile.getObjectClass(mod);
268      if (objectClass != null)
269      {
270        resource.objectClass(objectClass);
271      }
272
273      resource.resourceTypeProperty(new JsonPointer(TYPE_PROPERTY));
274      resource.property(TYPE_PROPERTY, resourceType());
275      resource.property("_rev", simple("etag").writability(READ_ONLY));
276
277      for (final PropertyDefinition<?> pd : mod.getPropertyDefinitions())
278      {
279        if (pd.hasOption(PropertyOption.HIDDEN))
280        {
281          continue;
282        }
283
284        final String attributeName = ldapProfile.getAttributeName(mod, pd);
285        if (pd instanceof AggregationPropertyDefinition)
286        {
287          final AggregationPropertyDefinition apd = (AggregationPropertyDefinition) pd;
288          final String relationChildRdnType = ldapProfile.getRelationChildRDNType(apd.getRelationDefinition());
289          final SimplePropertyMapper referencePropertyMapper = simple(relationChildRdnType).isRequired(true);
290          final DN baseDn = apd.getParentPath().toDN()
291                               .child(ldapProfile.getRelationRDNSequence(apd.getRelationDefinition()));
292          final ReferencePropertyMapper mapper = reference(attributeName,
293                                                           baseDn.toString(),
294                                                           relationChildRdnType,
295                                                           referencePropertyMapper);
296          resource.property(pd.getName(), mapper);
297        }
298        else
299        {
300          final SimplePropertyMapper mapper = simple(attributeName)
301                  .isRequired(pd.hasOption(PropertyOption.MANDATORY))
302                  .writability(pd.hasOption(PropertyOption.READ_ONLY) ? CREATE_ONLY : READ_WRITE)
303                  .isMultiValued(pd.hasOption(PropertyOption.MULTI_VALUED));
304
305          // Define the default value as well if possible.
306          final DefaultBehaviorProvider<?> dbp = pd.getDefaultBehaviorProvider();
307          if (dbp instanceof DefinedDefaultBehaviorProvider)
308          {
309            final DefinedDefaultBehaviorProvider<?> ddbp = (DefinedDefaultBehaviorProvider) dbp;
310            final Collection<String> defaultValues = ddbp.getDefaultValues();
311            final List<Object> decodedDefaultValues = new ArrayList<>(defaultValues.size());
312            final Function<String, ?, NeverThrowsException> converter = getConverter(attributeName);
313            for (final String defaultValue : defaultValues)
314            {
315              decodedDefaultValues.add(converter.apply(defaultValue));
316            }
317            mapper.defaultJsonValues(decodedDefaultValues);
318          }
319          resource.property(pd.getName(), mapper);
320        }
321      }
322    }
323
324    private Function<String, ?, NeverThrowsException> getConverter(final String attributeName)
325    {
326      final AttributeDescription attributeDescription = AttributeDescription.valueOf(attributeName);
327      final Syntax syntax = attributeDescription.getAttributeType().getSyntax();
328      if (syntax.equals(getBooleanSyntax()))
329      {
330        return Functions.stringToBoolean();
331      }
332      else if (syntax.equals(getIntegerSyntax()))
333      {
334        return Functions.stringToLong();
335      }
336      else
337      {
338        return Functions.identityFunction();
339      }
340    }
341
342    @Override
343    public void stop()
344    {
345      // Nothing to do
346    }
347
348    @Override
349    public Factory<Buffer> getBufferFactory()
350    {
351      return null;
352    }
353  }
354}