/* * 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.http.routing.RouteMatchers.newResourceApiVersionBehaviourManager; import static org.forgerock.http.routing.Version.version; import static org.forgerock.json.resource.RouteMatchers.resourceApiVersionContextFilter; 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 static org.opends.messages.ConfigMessages.ERR_BAD_ADMIN_API_RESOURCE_VERSION; 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.http.routing.ResourceApiVersionBehaviourManager; import org.forgerock.http.routing.Version; import org.forgerock.json.JsonPointer; import org.forgerock.json.resource.BadRequestException; import org.forgerock.json.resource.FilterChain; import org.forgerock.json.resource.Request; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.Router; 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.AbstractRequestHandler; 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.services.context.Context; import org.forgerock.util.Factory; import org.forgerock.util.Function; import org.forgerock.util.promise.NeverThrowsException; import org.forgerock.util.promise.Promise; 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 { private static final Version ADMIN_API_VERSION = version("1.0"); 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 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); final Router versionRouter = new Router(); versionRouter.addRoute(ADMIN_API_VERSION, handler); versionRouter.setDefaultRoute(new AbstractRequestHandler() { @Override protected Promise handleRequest(final Context context, final Request request) { final String message = ERR_BAD_ADMIN_API_RESOURCE_VERSION.get(request.getResourceVersion(), ADMIN_API_VERSION) .toString(); return new BadRequestException(message).asPromise(); } }); // FIXME: Disable the warning header for now due to CREST-389 / CREST-390. final ResourceApiVersionBehaviourManager behaviourManager = newResourceApiVersionBehaviourManager(); behaviourManager.setWarningEnabled(false); return newHttpHandler(new FilterChain(versionRouter, resourceApiVersionContextFilter(behaviourManager))); } 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 defaultValues = ddbp.getDefaultValues(); final List decodedDefaultValues = new ArrayList<>(defaultValues.size()); final Function converter = getConverter(attributeName); for (final String defaultValue : defaultValues) { decodedDefaultValues.add(converter.apply(defaultValue)); } mapper.defaultJsonValues(decodedDefaultValues); } resource.property(pd.getName(), mapper); } } } private Function 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 getBufferFactory() { return null; } } }