From c9009e34111d77ef4e30872dfb01cc0dc4335a56 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 23 Jun 2016 00:21:24 +0000
Subject: [PATCH] OPENDJ-3130 Provide REST access to OpenDJ's configuration and monitoring

---
 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/AdminEndpointConfiguration.xml |   41 +++++
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/AdminEndpoint.java                    |  323 ++++++++++++++++++++++++++++++++++++++++++++++
 opendj-server-legacy/resource/schema/02-config.ldif                                                                 |    5 
 opendj-server-legacy/resource/config/config.ldif                                                                    |    9 +
 4 files changed, 378 insertions(+), 0 deletions(-)

diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/AdminEndpointConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/AdminEndpointConfiguration.xml
new file mode 100644
index 0000000..835ccf4
--- /dev/null
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/AdminEndpointConfiguration.xml
@@ -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>
diff --git a/opendj-server-legacy/resource/config/config.ldif b/opendj-server-legacy/resource/config/config.ldif
index 0d8d11d..97f7a60 100644
--- a/opendj-server-legacy/resource/config/config.ldif
+++ b/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
diff --git a/opendj-server-legacy/resource/schema/02-config.ldif b/opendj-server-legacy/resource/schema/02-config.ldif
index 79cacd5..571d451 100644
--- a/opendj-server-legacy/resource/schema/02-config.ldif
+++ b/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' )
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/AdminEndpoint.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/AdminEndpoint.java
new file mode 100644
index 0000000..b0a7fb5
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/AdminEndpoint.java
@@ -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;
+    }
+  }
+}

--
Gitblit v1.10.0