mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
15.55.2016 c9009e34111d77ef4e30872dfb01cc0dc4335a56
OPENDJ-3130 Provide REST access to OpenDJ's configuration and monitoring

Implemented a new Rest2Ldap based endpoint "admin" which exposes two
paths:

* admin/monitor: a read-only endpoint providing a simple recursive
mapping to all entries exposed in cn=monitor. Unfortunately, cn=monitor
is schema-less so the attribute mappings are sub-optimal

* admin/config: a fully functional endpoint providing access to
cn=config. The mapping is driven from the configuration framework.

NOTE: the admin endpoint is only available in the embedded listener,
although we could make it available in the gateway if requested.
2 files added
2 files modified
378 ■■■■■ changed files
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/AdminEndpointConfiguration.xml 41 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/config/config.ldif 9 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/schema/02-config.ldif 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/AdminEndpoint.java 323 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/AdminEndpointConfiguration.xml
New file
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
    The contents of this file are subject to the terms of the Common Development and
    Distribution License (the License). You may not use this file except in compliance with the
    License.
    You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
    specific language governing permission and limitations under the License.
    When distributing Covered Software, include this CDDL Header Notice in each file and include
    the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
    Header, with the fields enclosed by brackets [] replaced by your own identifying
    information: "Portions copyright [year] [name of copyright owner]".
    Copyright 2016 ForgeRock AS.
  -->
<adm:managed-object name="admin-endpoint"
  plural-name="admin-endpoints" extends="http-endpoint"
  package="org.forgerock.opendj.server.config" xmlns:adm="http://opendj.forgerock.org/admin"
  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
  <adm:synopsis>
    The <adm:user-friendly-name /> provides RESTful access to <adm:product-name />'s
    monitoring and configuration backends.
  </adm:synopsis>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-admin-endpoint</ldap:name>
      <ldap:superior>ds-cfg-http-endpoint</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property-override name="java-class"
    advanced="true">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>
          org.opends.server.protocols.http.rest2ldap.AdminEndpoint
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
</adm:managed-object>
opendj-server-legacy/resource/config/config.ldif
@@ -389,6 +389,15 @@
ds-cfg-config-directory: config/rest2ldap/endpoints/api
ds-cfg-http-authorization-mechanism: cn=HTTP Basic,cn=HTTP Authorization Mechanisms,cn=config
dn: ds-cfg-base-path=/admin,cn=HTTP Endpoints,cn=config
objectClass: top
objectClass: ds-cfg-http-endpoint
objectClass: ds-cfg-admin-endpoint
ds-cfg-enabled: true
ds-cfg-base-path: /admin
ds-cfg-java-class: org.opends.server.protocols.http.rest2ldap.AdminEndpoint
ds-cfg-http-authorization-mechanism: cn=HTTP Basic,cn=HTTP Authorization Mechanisms,cn=config
dn: cn=HTTP Authorization Mechanisms,cn=config
objectClass: top
objectClass: ds-cfg-branch
opendj-server-legacy/resource/schema/02-config.ldif
@@ -6062,3 +6062,8 @@
  STRUCTURAL
  MUST ( ds-cfg-oauth2-access-token-directory )
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.44
  NAME 'ds-cfg-admin-endpoint'
  SUP ds-cfg-http-endpoint
  STRUCTURAL
  X-ORIGIN 'OpenDJ Directory Server' )
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/AdminEndpoint.java
New file
@@ -0,0 +1,323 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 *
 */
package org.opends.server.protocols.http.rest2ldap;
import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler;
import static org.forgerock.opendj.ldap.schema.CoreSchema.getBooleanSyntax;
import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.CREATE_ONLY;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_ONLY;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import static org.forgerock.util.Options.defaultOptions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.forgerock.http.Handler;
import org.forgerock.http.HttpApplication;
import org.forgerock.http.HttpApplicationException;
import org.forgerock.http.io.Buffer;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.opendj.config.AbstractManagedObjectDefinition;
import org.forgerock.opendj.config.AggregationPropertyDefinition;
import org.forgerock.opendj.config.DefaultBehaviorProvider;
import org.forgerock.opendj.config.DefinedDefaultBehaviorProvider;
import org.forgerock.opendj.config.InstantiableRelationDefinition;
import org.forgerock.opendj.config.LDAPProfile;
import org.forgerock.opendj.config.ManagedObjectDefinition;
import org.forgerock.opendj.config.ManagedObjectOption;
import org.forgerock.opendj.config.PropertyDefinition;
import org.forgerock.opendj.config.PropertyOption;
import org.forgerock.opendj.config.RelationDefinition;
import org.forgerock.opendj.config.RelationOption;
import org.forgerock.opendj.config.SingletonRelationDefinition;
import org.forgerock.opendj.config.TopCfgDefn;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Functions;
import org.forgerock.opendj.ldap.schema.Syntax;
import org.forgerock.opendj.rest2ldap.ReferencePropertyMapper;
import org.forgerock.opendj.rest2ldap.Resource;
import org.forgerock.opendj.rest2ldap.Rest2Ldap;
import org.forgerock.opendj.rest2ldap.SimplePropertyMapper;
import org.forgerock.opendj.rest2ldap.SubResourceCollection;
import org.forgerock.opendj.rest2ldap.SubResourceSingleton;
import org.forgerock.opendj.server.config.meta.GlobalCfgDefn;
import org.forgerock.opendj.server.config.meta.RootCfgDefn;
import org.forgerock.opendj.server.config.server.AdminEndpointCfg;
import org.forgerock.util.Factory;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.opends.server.api.HttpEndpoint;
import org.opends.server.core.ServerContext;
import org.opends.server.types.InitializationException;
/**
 * An HTTP endpoint providing access to the server's monitoring backend (cn=monitor) and its configuration (cn=config).
 */
public final class AdminEndpoint extends HttpEndpoint<AdminEndpointCfg>
{
  private static final String TYPE_PROPERTY = "_schema";
  private static final String ADMIN_API = "admin-api";
  private static final String MONITOR = "monitor";
  private static final String CONFIG = "config";
  /**
   * Create a new AdminEndpoint with the supplied configuration.
   *
   * @param configuration
   *          Configuration to use for the {@link HttpApplication}
   * @param serverContext
   *          Server of this LDAP server
   */
  public AdminEndpoint(AdminEndpointCfg configuration, ServerContext serverContext)
  {
    super(configuration, serverContext);
  }
  @Override
  public HttpApplication newHttpApplication() throws InitializationException
  {
    return new AdminHttpApplication();
  }
  /**
   * Specialized {@link HttpApplication} using internal connections to this local LDAP server.
   */
  private final class AdminHttpApplication implements HttpApplication
  {
    private LDAPProfile ldapProfile = LDAPProfile.getInstance();
    @Override
    public Handler start() throws HttpApplicationException
    {
      final Map<String, Resource> resources = new HashMap<>();
      // Define the entry point to the admin API.
      resources.put(ADMIN_API, resource(ADMIN_API).subResources(singletonOf(MONITOR).urlTemplate(MONITOR)
                                                                                    .dnTemplate("cn=monitor")
                                                                                    .isReadOnly(true),
                                                                singletonOf(CONFIG).urlTemplate(CONFIG)
                                                                                   .dnTemplate("cn=config")));
      // Define the monitoring endpoint.
      resources.put(MONITOR, resource(MONITOR).includeAllUserAttributesByDefault(true)
                                              .excludedDefaultUserAttributes("objectClass", "cn")
                                              .objectClass ("ds-monitor-entry")
                                              .property("_id", simple("cn"))
                                              .subResource(collectionOf(MONITOR).useClientDnNaming("cn")));
      // Build the configuration endpoint using the configuration framework.
      final TopCfgDefn topCfgDefn = TopCfgDefn.getInstance();
      final RootCfgDefn rootCfgDefn = RootCfgDefn.getInstance();
      final GlobalCfgDefn globalCfgDefn = GlobalCfgDefn.getInstance();
      // The configuration framework exposes the root and global configuration as separate resources, but it would be
      // nice if we exposed them as a single resource.
      final Resource config = resource(CONFIG);
      configureResourceProperties(globalCfgDefn, config);
      configureResourceSubResources(rootCfgDefn, config, true);
      resources.put(CONFIG, config);
      resources.put(topCfgDefn.getName(), buildResource(topCfgDefn));
      for (final AbstractManagedObjectDefinition<?, ?> mod : topCfgDefn.getAllChildren())
      {
        if (!mod.hasOption(ManagedObjectOption.HIDDEN) && mod != globalCfgDefn && mod != rootCfgDefn)
        {
          resources.put(mod.getName(), buildResource(mod));
        }
      }
      // Now that all resources are defined, perform a second pass processing all relation definitions in order to
      // identity which attributes should be used for the "_id" property.
      for (final AbstractManagedObjectDefinition<?, ?> mod : topCfgDefn.getAllChildren())
      {
        for (final RelationDefinition<?, ?> rd : mod.getRelationDefinitions())
        {
          if (rd instanceof InstantiableRelationDefinition)
          {
            final InstantiableRelationDefinition<?, ?> ird = (InstantiableRelationDefinition) rd;
            final AbstractManagedObjectDefinition<?, ?> d = rd.getChildDefinition();
            final String rdnType = ldapProfile.getRelationChildRDNType(ird);
            resources.get(d.getName()).property("_id", simple(rdnType).isRequired(true).writability(CREATE_ONLY));
          }
        }
      }
      final Rest2Ldap rest2Ldap = rest2Ldap(defaultOptions(), resources.values());
      final RequestHandler handler = rest2Ldap.newRequestHandlerFor(ADMIN_API);
      return newHttpHandler(handler);
    }
    private Resource buildResource(final AbstractManagedObjectDefinition<?, ?> mod)
    {
      final Resource resource = resource(mod.getName());
      configureResourceProperties(mod, resource);
      configureResourceSubResources(mod, resource, false);
      return resource;
    }
    private void configureResourceSubResources(final AbstractManagedObjectDefinition<?, ?> mod, final Resource resource,
                                               final boolean removeCnEqualsConfig)
    {
      for (final RelationDefinition<?, ?> rd : mod.getRelationDefinitions())
      {
        if (rd.hasOption(RelationOption.HIDDEN))
        {
          continue;
        }
        if (rd instanceof InstantiableRelationDefinition)
        {
          final InstantiableRelationDefinition<?, ?> ird = (InstantiableRelationDefinition) rd;
          final AbstractManagedObjectDefinition<?, ?> d = rd.getChildDefinition();
          final SubResourceCollection collection = collectionOf(d.getName())
                  .useClientDnNaming(ldapProfile.getRelationChildRDNType(ird))
                  .urlTemplate(ird.getPluralName())
                  .dnTemplate(getRelationRdnSequence(rd, removeCnEqualsConfig))
                  .glueObjectClasses(ldapProfile.getRelationObjectClasses(rd).toArray(new String[0]));
          resource.subResource(collection);
        }
        else if (rd instanceof SingletonRelationDefinition)
        {
          if (mod == RootCfgDefn.getInstance() && rd.getChildDefinition() == GlobalCfgDefn.getInstance())
          {
            // Special case: ignore the root -> global configuration relation because these two resources are merged
            // into a single resource within the REST API.
            continue;
          }
          final SubResourceSingleton singleton = singletonOf(rd.getChildDefinition().getName())
                  .urlTemplate(rd.getName())
                  .dnTemplate(getRelationRdnSequence(rd, removeCnEqualsConfig));
          resource.subResource(singleton);
        }
        // Optional/set NYI
      }
    }
    private String getRelationRdnSequence(final RelationDefinition<?, ?> rd, final boolean removeCnEqualsConfig)
    {
      final String rdnSequence = ldapProfile.getRelationRDNSequence(rd);
      if (removeCnEqualsConfig)
      {
        final DN dn = DN.valueOf(rdnSequence);
        return dn.localName(dn.size() - 1).toString();
      }
      return rdnSequence;
    }
    private void configureResourceProperties(final AbstractManagedObjectDefinition<?, ?> mod, final Resource resource)
    {
      resource.isAbstract(!(mod instanceof ManagedObjectDefinition));
      if (mod.getParent() != null)
      {
        resource.superType(mod.getParent().getName());
      }
      final String objectClass = ldapProfile.getObjectClass(mod);
      if (objectClass != null)
      {
        resource.objectClass(objectClass);
      }
      resource.resourceTypeProperty(new JsonPointer(TYPE_PROPERTY));
      resource.property(TYPE_PROPERTY, resourceType());
      resource.property("_rev", simple("etag").writability(READ_ONLY));
      for (final PropertyDefinition<?> pd : mod.getPropertyDefinitions())
      {
        if (pd.hasOption(PropertyOption.HIDDEN))
        {
          continue;
        }
        final String attributeName = ldapProfile.getAttributeName(mod, pd);
        if (pd instanceof AggregationPropertyDefinition)
        {
          final AggregationPropertyDefinition apd = (AggregationPropertyDefinition) pd;
          final String relationChildRdnType = ldapProfile.getRelationChildRDNType(apd.getRelationDefinition());
          final SimplePropertyMapper referencePropertyMapper = simple(relationChildRdnType).isRequired(true);
          final DN baseDn = apd.getParentPath().toDN()
                               .child(ldapProfile.getRelationRDNSequence(apd.getRelationDefinition()));
          final ReferencePropertyMapper mapper = reference(attributeName,
                                                           baseDn.toString(),
                                                           relationChildRdnType,
                                                           referencePropertyMapper);
          resource.property(pd.getName(), mapper);
        }
        else
        {
          final SimplePropertyMapper mapper = simple(attributeName)
                  .isRequired(pd.hasOption(PropertyOption.MANDATORY))
                  .writability(pd.hasOption(PropertyOption.READ_ONLY) ? CREATE_ONLY : READ_WRITE)
                  .isMultiValued(pd.hasOption(PropertyOption.MULTI_VALUED));
          // Define the default value as well if possible.
          final DefaultBehaviorProvider<?> dbp = pd.getDefaultBehaviorProvider();
          if (dbp instanceof DefinedDefaultBehaviorProvider)
          {
            final DefinedDefaultBehaviorProvider<?> ddbp = (DefinedDefaultBehaviorProvider) dbp;
            final Collection<String> defaultValues = ddbp.getDefaultValues();
            final List<Object> decodedDefaultValues = new ArrayList<>(defaultValues.size());
            final Function<String, ?, NeverThrowsException> converter = getConverter(attributeName);
            for (final String defaultValue : defaultValues)
            {
              decodedDefaultValues.add(converter.apply(defaultValue));
            }
            mapper.defaultJsonValues(decodedDefaultValues);
          }
          resource.property(pd.getName(), mapper);
        }
      }
    }
    private Function<String, ?, NeverThrowsException> getConverter(final String attributeName)
    {
      final AttributeDescription attributeDescription = AttributeDescription.valueOf(attributeName);
      final Syntax syntax = attributeDescription.getAttributeType().getSyntax();
      if (syntax.equals(getBooleanSyntax()))
      {
        return Functions.stringToBoolean();
      }
      else if (syntax.equals(getIntegerSyntax()))
      {
        return Functions.stringToLong();
      }
      else
      {
        return Functions.identityFunction();
      }
    }
    @Override
    public void stop()
    {
      // Nothing to do
    }
    @Override
    public Factory<Buffer> getBufferFactory()
    {
      return null;
    }
  }
}