| opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java | ●●●●● patch | view | raw | blame | history | |
| opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java | ●●●●● patch | view | raw | blame | history | |
| opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapContext.java | ●●●●● patch | view | raw | blame | history | |
| opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java | ●●●●● patch | view | raw | blame | history | |
| opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java | ●●●●● patch | view | raw | blame | history | |
| opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java | ●●●●● patch | view | raw | blame | history | |
| opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java | ●●●●● patch | view | raw | blame | history |
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java
New file @@ -0,0 +1,160 @@ /* * 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.forgerock.opendj.rest2ldap; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.DECODE_OPTIONS; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.forgerock.http.routing.UriRouterContext; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.services.context.Context; import org.forgerock.util.Options; /** * Represents a DN template whose RDN values may be substituted with URL template parameters parsed during routing. * Two types of DN template are supported: {@link #compile(String) absolute/relative} or {@link #compileRelative(String) * relative}. The table below shows how DN templates will be resolved to DNs when the template parameter "subdomain" * has the value "www" and the current routing state references the DN "dc=example,dc=com": * <p> * <table> * <tr><th>DN Template</th><th>{@link #compile(String)}</th><th>{@link #compileRelative(String)}</th></tr> * <tr><td>dc=www</td><td>dc=www</td><td>dc=www,dc=example,dc=com</td></tr> * <tr><td>..</td><td>dc=com</td><td>dc=com</td></tr> * <tr><td>dc={subdomain}</td><td>dc=www</td><td>dc=www,dc=example,dc=com</td></tr> * <tr><td>dc={subdomain},..</td><td>dc=www,dc=com</td><td>dc=www,dc=com</td></tr> * </table> */ final class DnTemplate { private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}"); private final String template; private final String formatString; private final List<String> variables; private final int relativeOffset; /** * Compiles a DN template which will resolve LDAP entries relative to the current routing state. The DN template may * contain trailing ".." RDNs in order to resolve entries which are relative to a parent of the current routing * state. * * @param template * The string representation of the DN template. * @return The compiled DN template. */ static DnTemplate compileRelative(String template) { return compile(template, true); } /** * Compiles a DN template which will resolve LDAP entries relative to the root DSE by default, but MAY include * relative RDNs indicating that that the DN template will be resolved against current routing state * instead. * * @param template * The string representation of the DN template. * @return The compiled DN template. */ static DnTemplate compile(String template) { return compile(template, false); } private static DnTemplate compile(String template, boolean isRelative) { // Parse any trailing relative RDNs. String trimmedTemplate; int relativeOffset; if (template.equals("..")) { trimmedTemplate = ""; relativeOffset = 1; } else if (template.endsWith(",..")) { relativeOffset = 0; for (trimmedTemplate = template; trimmedTemplate.endsWith(",.."); trimmedTemplate = trimmedTemplate.substring(0, trimmedTemplate.length() - 3)) { relativeOffset++; } } else if (isRelative) { trimmedTemplate = template; relativeOffset = 0; } else { trimmedTemplate = template; relativeOffset = -1; } final List<String> templateVariables = new ArrayList<>(); final Matcher matcher = TEMPLATE_KEY_RE.matcher(trimmedTemplate); final StringBuffer buffer = new StringBuffer(trimmedTemplate.length()); while (matcher.find()) { matcher.appendReplacement(buffer, "%s"); templateVariables.add(matcher.group(1)); } matcher.appendTail(buffer); return new DnTemplate(trimmedTemplate, buffer.toString(), templateVariables, relativeOffset); } private DnTemplate(String template, String formatString, List<String> variables, int relativeOffset) { this.template = template; this.formatString = formatString; this.variables = variables; this.relativeOffset = relativeOffset; } DN format(final Context context) { // First determine the base DN based on the context DN and the relative offset. DN baseDn = null; if (relativeOffset >= 0 && context.containsContext(RoutingContext.class)) { baseDn = context.asContext(RoutingContext.class).getDn().parent(relativeOffset); } if (baseDn == null) { baseDn = DN.rootDN(); } // Construct a DN using any routing template parameters. final Options options = context.asContext(Rest2LdapContext.class).getRest2ldap().getOptions(); final Schema schema = options.get(DECODE_OPTIONS).getSchemaResolver().resolveSchema(template); if (variables.isEmpty()) { final DN relativeDn = DN.valueOf(template, schema); return baseDn.child(relativeDn); } else { final String[] values = new String[variables.size()]; for (int i = 0; i < values.length; i++) { values[i] = getTemplateParameter(context, variables.get(i)); } final DN relativeDn = DN.format(formatString, schema, (Object[]) values); return baseDn.child(relativeDn); } } private String getTemplateParameter(final Context context, final String parameter) { UriRouterContext uriRouterContext = context.asContext(UriRouterContext.class); for (;;) { final Map<String, String> uriTemplateVariables = uriRouterContext.getUriTemplateVariables(); final String value = uriTemplateVariables.get(parameter); if (value != null) { return value; } if (!uriRouterContext.getParent().containsContext(UriRouterContext.class)) { throw new IllegalStateException("DN template parameter " + parameter + " cannot be resolved"); } uriRouterContext = uriRouterContext.getParent().asContext(UriRouterContext.class); } } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -26,17 +26,28 @@ import java.util.LinkedHashMap; import java.util.Map; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.BadRequestException; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.ForbiddenException; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.json.resource.NotFoundException; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.PermanentException; import org.forgerock.json.resource.PreconditionFailedException; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResourceHandler; import org.forgerock.json.resource.QueryResponse; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.RetryableException; import org.forgerock.json.resource.Router; import org.forgerock.json.resource.ServiceUnavailableException; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.opendj.ldap.AssertionFailureException; import org.forgerock.opendj.ldap.AttributeDescription; import org.forgerock.opendj.ldap.AuthenticationException; @@ -51,9 +62,11 @@ import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.TimeoutResultException; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.services.context.Context; import org.forgerock.util.Option; import org.forgerock.util.Options; import org.forgerock.util.Reject; import org.forgerock.util.promise.Promise; /** * Provides methods for constructing Rest2Ldap protocol gateways. Applications construct a new Rest2Ldap @@ -359,7 +372,51 @@ Reject.ifTrue(!resources.containsKey(resourceId), "unrecognized resource '" + resourceId + "'"); final SubResourceSingleton root = singletonOf(resourceId); root.build(this, null); return root.addRoutes(new Router()); return rest2LdapContext(root.addRoutes(new Router())); } private RequestHandler rest2LdapContext(final RequestHandler delegate) { return new RequestHandler() { public Promise<ActionResponse, ResourceException> handleAction(final Context context, final ActionRequest request) { return delegate.handleAction(wrap(context), request); } public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, final CreateRequest request) { return delegate.handleCreate(wrap(context), request); } public Promise<ResourceResponse, ResourceException> handleDelete(final Context context, final DeleteRequest request) { return delegate.handleDelete(wrap(context), request); } public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, final PatchRequest request) { return delegate.handlePatch(wrap(context), request); } public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, final QueryResourceHandler handler) { return delegate.handleQuery(wrap(context), request, handler); } public Promise<ResourceResponse, ResourceException> handleRead(final Context context, final ReadRequest request) { return delegate.handleRead(wrap(context), request); } public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context, final UpdateRequest request) { return delegate.handleUpdate(wrap(context), request); } private Context wrap(final Context context) { return new Rest2LdapContext(context, Rest2Ldap.this); } }; } Options getOptions() { opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapContext.java
New file @@ -0,0 +1,35 @@ /* * 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.forgerock.opendj.rest2ldap; import org.forgerock.services.context.AbstractContext; import org.forgerock.services.context.Context; /** * A {@link Context} which communicates the {@link Rest2Ldap} instance to downstream handlers and property mappers. */ final class Rest2LdapContext extends AbstractContext { private final Rest2Ldap rest2ldap; Rest2LdapContext(final Context parent, final Rest2Ldap rest2ldap) { super(parent, "rest2ldap context"); this.rest2ldap = rest2ldap; } Rest2Ldap getRest2ldap() { return rest2ldap; } } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
@@ -16,16 +16,8 @@ */ package org.forgerock.opendj.rest2ldap; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.DECODE_OPTIONS; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.forgerock.http.routing.UriRouterContext; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.json.resource.BadRequestException; @@ -34,7 +26,6 @@ import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.Router; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.services.context.Context; import org.forgerock.util.Function; @@ -49,14 +40,11 @@ * </ul> */ public abstract class SubResource { private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}"); private final String resourceId; private final List<String> dnTemplateVariables = new ArrayList<>(); private String dnTemplateFormatString; private DnTemplate dnTemplate; String urlTemplate = ""; String dnTemplate = ""; String dnTemplateString = ""; boolean isReadOnly = false; Rest2Ldap rest2Ldap; Resource resource; @@ -90,19 +78,7 @@ if (resource == null) { throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE.get(parent, resourceId)); } this.dnTemplateFormatString = formatTemplate(dnTemplate, dnTemplateVariables); } // Parse the template keys and replace them with %s for formatting. private String formatTemplate(final String template, final List<String> templateVariables) { final Matcher matcher = TEMPLATE_KEY_RE.matcher(template); final StringBuffer buffer = new StringBuffer(template.length()); while (matcher.find()) { matcher.appendReplacement(buffer, "%s"); templateVariables.add(matcher.group(1)); } matcher.appendTail(buffer); return buffer.toString(); this.dnTemplate = DnTemplate.compileRelative(dnTemplateString); } abstract Router addRoutes(Router router); @@ -125,24 +101,7 @@ } final DN dnFrom(final Context context) { final DN baseDn = context.containsContext(RoutingContext.class) ? context.asContext(RoutingContext.class).getDn() : DN.rootDN(); final Schema schema = rest2Ldap.getOptions().get(DECODE_OPTIONS).getSchemaResolver().resolveSchema(dnTemplate); if (dnTemplateVariables.isEmpty()) { final DN relativeDn = DN.valueOf(dnTemplate, schema); return baseDn.child(relativeDn); } else { final UriRouterContext uriRouterContext = context.asContext(UriRouterContext.class); final Map<String, String> uriTemplateVariables = uriRouterContext.getUriTemplateVariables(); final String[] values = new String[dnTemplateVariables.size()]; for (int i = 0; i < values.length; i++) { final String key = dnTemplateVariables.get(i); values[i] = uriTemplateVariables.get(key); } final DN relativeDn = DN.format(dnTemplateFormatString, schema, (Object[]) values); return baseDn.child(relativeDn); } return dnTemplate.format(context); } final RequestHandler subResourceRouterFrom(final RoutingContext context) { opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -177,7 +177,7 @@ * @return A reference to this object. */ public SubResourceCollection dnTemplate(final String dnTemplate) { this.dnTemplate = dnTemplate; this.dnTemplateString = dnTemplate; return this; } @@ -257,7 +257,7 @@ private SubResourceImpl collection(final Context context) { return new SubResourceImpl(rest2Ldap, dnFrom(context), dnTemplate.isEmpty() ? null : glueObjectClasses, dnTemplateString.isEmpty() ? null : glueObjectClasses, namingStrategy, resource); } opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -111,7 +111,7 @@ * @return A reference to this object. */ public SubResourceSingleton dnTemplate(final String dnTemplate) { this.dnTemplate = dnTemplate; this.dnTemplateString = dnTemplate; return this; } opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java
New file @@ -0,0 +1,72 @@ /* * 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.forgerock.opendj.rest2ldap; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.rest2Ldap; import static org.forgerock.util.Options.defaultOptions; import org.forgerock.http.routing.UriRouterContext; import org.forgerock.opendj.ldap.DN; import org.forgerock.services.context.Context; import org.forgerock.services.context.RootContext; import org.forgerock.testng.ForgeRockTestCase; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @Test @SuppressWarnings("javadoc") public final class DnTemplateTest extends ForgeRockTestCase { private final Context context; { Context ctx = new RootContext(); ctx = new Rest2LdapContext(ctx, rest2Ldap(defaultOptions())); ctx = new UriRouterContext(ctx, "", "", singletonMap("subdomain", "www")); ctx = new RoutingContext(ctx, DN.valueOf("dc=example,dc=com"), null); ctx = new UriRouterContext(ctx, "", "", singletonMap("tenant", "acme")); context = ctx; } @DataProvider Object[][] templateData() { // @formatter:off return new Object[][] { { "dc=www", "dc=www", "dc=www,dc=example,dc=com" }, { "..", "dc=com", "dc=com" }, { "dc={subdomain}", "dc=www", "dc=www,dc=example,dc=com" }, { "dc={subdomain},..", "dc=www,dc=com", "dc=www,dc=com" }, { "dc={subdomain},dc={tenant},..", "dc=www,dc=acme,dc=com", "dc=www,dc=acme,dc=com" }, }; // @formatter:on } @SuppressWarnings("unused") @Test(dataProvider = "templateData") public void testCompile(String template, String expectedDn, String expectedRelativeDn) { DnTemplate dnTemplate = DnTemplate.compile(template); DN dn = dnTemplate.format(context); assertThat((Object) dn).isEqualTo(DN.valueOf(expectedDn)); } @SuppressWarnings("unused") @Test(dataProvider = "templateData") public void testCompileRelative(String template, String expectedDn, String expectedRelativeDn) { DnTemplate dnTemplate = DnTemplate.compileRelative(template); DN dn = dnTemplate.format(context); assertThat((Object) dn).isEqualTo(DN.valueOf(expectedRelativeDn)); } }