OPENDJ-1666 PR-19 CREST-3.0.0 Migration
Design changes overview:
* Make the code compiling against CREST-3.0.0
* Implement rest2ldap servlet and Http connection handler as a CHF filter/handler chain
* Move all of the functionality out of the opendj-rest2ldap-servlet module and into the opendj-rest2ldap module, since it doesn't require Servlet - Servlet integration is provided by CHF.
* Create an Ldap Http Handler which is shared by both rest2ldap servlet and httpConnectionHandler
* Remove Rest2LdapContextFactory because the authn filter and handler can communicate directly via Context objects rather than using Servlet "attributes".
* Rename Context to RequestState
Detailed changes performed:
Files which has been mostly modified:
* org.opends.server.protocols.http.CollectClientsConnectionFilter
* org.forgerock.opendj.rest2ldap.LDAPCollectionResourceProvider
* org.forgerock.opendj.rest2ldap.ReferenceAttributeMapper
Other changes are mostly minor, they just ensure that code compile against CREST-3.0.0.
4 files deleted
4 files added
2 files renamed
25 files modified
| | |
| | | ! Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | ! information: "Portions Copyright [year] [name of copyright owner]". |
| | | ! |
| | | ! Copyright 2013 ForgeRock AS. |
| | | ! Copyright 2013-2015 ForgeRock AS. |
| | | ! |
| | | --> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" |
| | | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | | <modelVersion>4.0.0</modelVersion> |
| | | <parent> |
| | | <artifactId>opendj-project</artifactId> |
| | | <groupId>org.forgerock.opendj</groupId> |
| | | <version>3.0.0-SNAPSHOT</version> |
| | | </parent> |
| | | |
| | | <artifactId>opendj-rest2ldap-servlet</artifactId> |
| | | <name>OpenDJ Commons REST LDAP Gateway</name> |
| | | <description> |
| | | Provides integration between the OpenDJ Commons REST Adapter and Servlet APIs. |
| | | </description> |
| | | <packaging>bundle</packaging> |
| | | <packaging>war</packaging> |
| | | |
| | | <properties> |
| | | <jacksonVersion>1.9.2</jacksonVersion> |
| | | <!-- When released, with the 'binary.license.url' property set, |
| | | this artifact will contain an additional binary license --> |
| | | <include.binary.license>${project.build.directory}/${project.build.finalName}/WEB-INF/legal-notices</include.binary.license> |
| | | <checkstyleHeaderLocation>org/forgerock/checkstyle/default-java-header</checkstyleHeaderLocation> |
| | | </properties> |
| | | |
| | | <dependencies> |
| | | <dependency> |
| | | <groupId>org.forgerock.http</groupId> |
| | | <artifactId>chf-http-servlet</artifactId> |
| | | <version>0.0.1-SNAPSHOT</version> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.forgerock.opendj</groupId> |
| | | <artifactId>opendj-rest2ldap</artifactId> |
| | | <version>${project.version}</version> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.forgerock.opendj</groupId> |
| | | <artifactId>opendj-grizzly</artifactId> |
| | | <version>${project.version}</version> |
| | | <optional>true</optional> |
| | | </dependency> |
| | | <dependency> |
| | | <!-- Required for compilation --> |
| | | <groupId>org.forgerock.commons</groupId> |
| | | <artifactId>json-resource-servlet</artifactId> |
| | | <version>${forgerockRestVersion}</version> |
| | | </dependency> |
| | | <dependency> |
| | | <!-- Required for runtime via WAR overlay --> |
| | | <groupId>org.forgerock.commons</groupId> |
| | | <artifactId>json-resource-servlet</artifactId> |
| | | <version>${forgerockRestVersion}</version> |
| | | <type>war</type> |
| | | <classifier>servlet</classifier> |
| | | <scope>runtime</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.slf4j</groupId> |
| | | <artifactId>slf4j-api</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.slf4j</groupId> |
| | | <artifactId>slf4j-jdk14</artifactId> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>javax.servlet</groupId> |
| | | <artifactId>javax.servlet-api</artifactId> |
| | | <version>3.0.1</version> |
| | | <scope>provided</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.codehaus.jackson</groupId> |
| | | <artifactId>jackson-core-asl</artifactId> |
| | | <version>${jacksonVersion}</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.codehaus.jackson</groupId> |
| | | <artifactId>jackson-mapper-asl</artifactId> |
| | | <version>${jacksonVersion}</version> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | |
| | | <build> |
| | | <resources> |
| | | <resource> |
| | | <directory>src/main/resources</directory> |
| | | <filtering>true</filtering> |
| | | </resource> |
| | | </resources> |
| | | <plugins> |
| | | <plugin> |
| | | <groupId>org.apache.felix</groupId> |
| | | <artifactId>maven-bundle-plugin</artifactId> |
| | | <extensions>true</extensions> |
| | | </plugin> |
| | | <!-- include opendj-grizzly and its dependencies in war --> |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-dependency-plugin</artifactId> |
| | | <executions> |
| | | <execution> |
| | | <id>copy-grizzly</id> |
| | | <phase>package</phase> |
| | | <goals> |
| | | <goal>copy-dependencies</goal> |
| | | </goals> |
| | | <configuration> |
| | | <includeArtifactIds>opendj-grizzly</includeArtifactIds> |
| | | <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/lib</outputDirectory> |
| | | <overWriteReleases>false</overWriteReleases> |
| | | <overWriteSnapshots>false</overWriteSnapshots> |
| | | <overWriteIfNewer>true</overWriteIfNewer> |
| | | </configuration> |
| | | </execution> |
| | | </executions> |
| | | </plugin> |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-war-plugin</artifactId> |
| | | <executions> |
| | | <execution> |
| | | <id>war</id> |
| | | <phase>package</phase> |
| | | <goals> |
| | | <goal>war</goal> |
| | | </goals> |
| | | <configuration> |
| | | <classifier>servlet</classifier> |
| | | <overlays> |
| | | <overlay> |
| | | <groupId>org.forgerock.commons</groupId> |
| | | <artifactId>json-resource-servlet</artifactId> |
| | | <classifier>servlet</classifier> |
| | | <includes> |
| | | <include>WEB-INF/*.xml</include> |
| | | </includes> |
| | | </overlay> |
| | | </overlays> |
| | | </configuration> |
| | | </execution> |
| | | </executions> |
| | | </plugin> |
| | | <plugin> |
| | | <groupId>org.eclipse.jetty</groupId> |
| | | <artifactId>jetty-maven-plugin</artifactId> |
| | | <version>9.0.4.v20130625</version> |
| | | </plugin> |
| | | </plugins> |
| | | <pluginManagement> |
| | | <plugins> |
| | | <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.--> |
| | | <plugin> |
| | | <groupId>org.eclipse.m2e</groupId> |
| | | <artifactId>lifecycle-mapping</artifactId> |
| | | <version>1.0.0</version> |
| | | <version>9.2.11.v20150529</version> |
| | | <configuration> |
| | | <lifecycleMappingMetadata> |
| | | <pluginExecutions> |
| | | <pluginExecution> |
| | | <pluginExecutionFilter> |
| | | <scanIntervalSeconds>10</scanIntervalSeconds> |
| | | <webAppConfig> |
| | | <contextPath>/</contextPath> |
| | | </webAppConfig> |
| | | </configuration> |
| | | </plugin> |
| | | |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-dependency-plugin</artifactId> |
| | | <versionRange>[2.6,)</versionRange> |
| | | <goals> |
| | | <goal>copy-dependencies</goal> |
| | | </goals> |
| | | </pluginExecutionFilter> |
| | | <action> |
| | | <ignore /> |
| | | </action> |
| | | </pluginExecution> |
| | | </pluginExecutions> |
| | | </lifecycleMappingMetadata> |
| | | <artifactId>maven-war-plugin</artifactId> |
| | | <configuration> |
| | | <webResources> |
| | | <resource> |
| | | <targetPath>WEB-INF/legal-notices</targetPath> |
| | | <directory>../legal-notices</directory> |
| | | <excludes> |
| | | <!-- The web-app does not include the documentation --> |
| | | <exclude>CC-BY-NC-ND.txt</exclude> |
| | | </excludes> |
| | | </resource> |
| | | <resource> |
| | | <targetPath>/</targetPath> |
| | | <directory>src/main/webapp/</directory> |
| | | <excludes> |
| | | <exclude>web.xml</exclude> |
| | | </excludes> |
| | | </resource> |
| | | </webResources> |
| | | </configuration> |
| | | </plugin> |
| | | </plugins> |
| | | </pluginManagement> |
| | | </build> |
| | | <reporting> |
| | | <plugins> |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-project-info-reports-plugin</artifactId> |
| | | <reportSets> |
| | | <reportSet> |
| | | <reports> |
| | | <report>dependencies</report> |
| | | </reports> |
| | | </reportSet> |
| | | </reportSets> |
| | | </plugin> |
| | | </plugins> |
| | | </reporting> |
| | | </project> |
| New file |
| | |
| | | # |
| | | # 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 2015 ForgeRock AS. |
| | | # |
| | | org.forgerock.opendj.rest2ldap.Rest2LDAPHttpApplication |
| | |
| | | <!-- |
| | | ! 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 2015 ForgeRock AS. |
| | | ! |
| | | --> |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| | | xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" |
| | | version="2.5"> |
| | | xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" |
| | | version="3.0"> |
| | | |
| | | <display-name>Json Resource Servlet</display-name> |
| | | <display-name>OpenDJ REST LDAP Gateway</display-name> |
| | | |
| | | <servlet> |
| | | <servlet-name>OpenDJ Commons REST LDAP Gateway</servlet-name> |
| | | <servlet-class>org.forgerock.json.resource.servlet.HttpServlet</servlet-class> |
| | | |
| | | <init-param> |
| | | <param-name>connection-factory-class</param-name> |
| | | <param-value>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPConnectionFactoryProvider</param-value> |
| | | </init-param> |
| | | |
| | | <init-param> |
| | | <param-name>config-file</param-name> |
| | | <param-value>/opendj-rest2ldap-servlet.json</param-value> |
| | | </init-param> |
| | | |
| | | <init-param> |
| | | <param-name>context-factory-class</param-name> |
| | | <param-value>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory</param-value> |
| | | </init-param> |
| | | <servlet-name>OpenDJ REST LDAP Gateway</servlet-name> |
| | | <servlet-class>org.forgerock.http.servlet.HttpFrameworkServlet</servlet-class> |
| | | <async-supported>true</async-supported> |
| | | </servlet> |
| | | |
| | | <servlet-mapping> |
| | | <servlet-name>OpenDJ Commons REST LDAP Gateway</servlet-name> |
| | | <servlet-name>OpenDJ REST LDAP Gateway</servlet-name> |
| | | <url-pattern>/*</url-pattern> |
| | | </servlet-mapping> |
| | | |
| | | <filter> |
| | | <filter-name>OpenDJ Commons REST LDAP Authentication Filter</filter-name> |
| | | <filter-class>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPAuthnFilter</filter-class> |
| | | |
| | | <init-param> |
| | | <param-name>config-file</param-name> |
| | | <param-value>/opendj-rest2ldap-servlet.json</param-value> |
| | | </init-param> |
| | | </filter> |
| | | |
| | | <filter-mapping> |
| | | <filter-name>OpenDJ Commons REST LDAP Authentication Filter</filter-name> |
| | | <url-pattern>/*</url-pattern> |
| | | </filter-mapping> |
| | | |
| | | </web-app> |
| | | |
| | |
| | | ! Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | ! information: "Portions Copyright [year] [name of copyright owner]". |
| | | ! |
| | | ! Copyright 2012 ForgeRock AS. |
| | | ! Copyright 2012-2015 ForgeRock AS. |
| | | ! |
| | | --> |
| | | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| | |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.forgerock.commons</groupId> |
| | | <artifactId>json-fluent</artifactId> |
| | | <artifactId>json-resource</artifactId> |
| | | <version>${forgerockRestVersion}</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.forgerock.commons</groupId> |
| | | <artifactId>json-resource</artifactId> |
| | | <artifactId>json-resource-http</artifactId> |
| | | <version>${forgerockRestVersion}</version> |
| | | </dependency> |
| | | <dependency> |
| | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.transform; |
| | | import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE; |
| | | |
| | | import java.util.ArrayList; |
| | |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.NotSupportedException; |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.opendj.ldap.ModificationType; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | |
| | | /** |
| | | * An abstract LDAP attribute mapper which provides a simple mapping from a JSON |
| | | * value to a single LDAP attribute. |
| | | */ |
| | | abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends |
| | | AttributeMapper { |
| | | abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends AttributeMapper { |
| | | List<Object> defaultJSONValues = emptyList(); |
| | | final AttributeDescription ldapAttributeName; |
| | | private boolean isRequired; |
| | |
| | | } |
| | | |
| | | @Override |
| | | void create(final Context c, final JsonPointer path, final JsonValue v, |
| | | final ResultHandler<List<Attribute>> h) { |
| | | getNewLDAPAttributes(c, path, v, createAttributeHandler(path, h)); |
| | | Promise<List<Attribute>, ResourceException> create( |
| | | final RequestState requestState, final JsonPointer path, final JsonValue v) { |
| | | return getNewLDAPAttributes(requestState, path, v).then( |
| | | new Function<Attribute, List<Attribute>, ResourceException>() { |
| | | @Override |
| | | public List<Attribute> apply(Attribute newLDAPAttribute) throws ResourceException { |
| | | if (!writabilityPolicy.canCreate(ldapAttributeName)) { |
| | | if (!newLDAPAttribute.isEmpty() && !writabilityPolicy.discardWrites()) { |
| | | throw new BadRequestException(i18n("The request cannot be processed because it attempts " |
| | | + "to create the read-only field '%s'", path)); |
| | | } |
| | | return Collections.emptyList(); |
| | | } else if (newLDAPAttribute.isEmpty()) { |
| | | if (isRequired) { |
| | | throw new BadRequestException(i18n("The request cannot be processed because it attempts " |
| | | + "to remove the required field '%s'", path)); |
| | | } |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | return singletonList(newLDAPAttribute); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | void getLDAPAttributes(final RequestState requestState, final JsonPointer path, |
| | | final JsonPointer subPath, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(ldapAttributeName.toString()); |
| | | } |
| | | |
| | | abstract void getNewLDAPAttributes(Context c, JsonPointer path, List<Object> newValues, |
| | | ResultHandler<Attribute> h); |
| | | abstract Promise<Attribute, ResourceException> getNewLDAPAttributes( |
| | | RequestState requestState, JsonPointer path, List<Object> newValues); |
| | | |
| | | abstract T getThis(); |
| | | |
| | | @Override |
| | | void patch(final Context c, final JsonPointer path, final PatchOperation operation, |
| | | final ResultHandler<List<Modification>> h) { |
| | | Promise<List<Modification>, ResourceException> patch( |
| | | final RequestState requestState, final JsonPointer path, final PatchOperation operation) { |
| | | try { |
| | | final JsonPointer field = operation.getField(); |
| | | final JsonValue v = operation.getValue(); |
| | |
| | | if (newValues.isEmpty()) { |
| | | // Deleting the attribute. |
| | | if (isRequired) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to remove " |
| | | + "the required field '%s'", path))); |
| | | return Promises.<List<Modification>, ResourceException> newExceptionPromise( |
| | | new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to remove the required field '%s'", |
| | | path))); |
| | | } else { |
| | | h.handleResult(singletonList(new Modification(modType, |
| | | emptyAttribute(ldapAttributeName)))); |
| | | return Promises.newResultPromise( |
| | | singletonList(new Modification(modType, emptyAttribute(ldapAttributeName)))); |
| | | } |
| | | } else { |
| | | getNewLDAPAttributes(c, path, newValues, transform( |
| | | new Function<Attribute, List<Modification>, NeverThrowsException>() { |
| | | return getNewLDAPAttributes(requestState, path, newValues) |
| | | .then(new Function<Attribute, List<Modification>, ResourceException>() { |
| | | @Override |
| | | public List<Modification> apply(final Attribute value) { |
| | | return singletonList(new Modification(modType, value)); |
| | | } |
| | | }, h)); |
| | | }); |
| | | } |
| | | } catch (final RuntimeException e) { |
| | | h.handleError(asResourceException(e)); |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } catch (final ResourceException e) { |
| | | h.handleError(e); |
| | | return Promises.newExceptionPromise(e); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v, |
| | | final ResultHandler<List<Modification>> h) { |
| | | getNewLDAPAttributes(c, path, v, updateAttributeHandler(path, e, h)); |
| | | Promise<List<Modification>, ResourceException> update( |
| | | final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | return getNewLDAPAttributes(requestState, path, v).then( |
| | | new Function<Attribute, List<Modification>, ResourceException>() { |
| | | @Override |
| | | public List<Modification> apply(final Attribute newLDAPAttribute) throws ResourceException { |
| | | // Get the existing LDAP attribute. |
| | | final Attribute tmp = e.getAttribute(ldapAttributeName); |
| | | final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName); |
| | | /* |
| | | * If the attribute is read-only then handle the following cases: |
| | | * 1) new values are provided and they are the same as the existing values |
| | | * 2) no new values are provided. |
| | | */ |
| | | if (!writabilityPolicy.canWrite(ldapAttributeName)) { |
| | | if (newLDAPAttribute.isEmpty() |
| | | || newLDAPAttribute.equals(oldLDAPAttribute) |
| | | || writabilityPolicy.discardWrites()) { |
| | | // No change. |
| | | return Collections.emptyList(); |
| | | } |
| | | throw new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to modify the read-only field '%s'", |
| | | path)); |
| | | } |
| | | |
| | | if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) { |
| | | // No change. |
| | | return Collections.emptyList(); |
| | | } else if (oldLDAPAttribute.isEmpty()) { |
| | | // The attribute is being added. |
| | | return singletonList(new Modification(ModificationType.REPLACE, newLDAPAttribute)); |
| | | } else if (newLDAPAttribute.isEmpty()) { |
| | | // The attribute is being deleted - this is not allowed if the attribute is required. |
| | | if (isRequired) { |
| | | throw new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to remove the required field '%s'", |
| | | path)); |
| | | } |
| | | return singletonList(new Modification(ModificationType.REPLACE, newLDAPAttribute)); |
| | | } else { |
| | | /* |
| | | * We could do a replace, but try to save bandwidth and send diffs instead. |
| | | * Perform deletes first in case we don't have an appropriate normalizer: |
| | | * permissive add(x) followed by delete(x) is destructive, whereas |
| | | * delete(x) followed by add(x) is idempotent when adding/removing the same value. |
| | | */ |
| | | final List<Modification> modifications = new ArrayList<>(2); |
| | | |
| | | final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute); |
| | | deletedValues.removeAll(newLDAPAttribute); |
| | | if (!deletedValues.isEmpty()) { |
| | | modifications.add(new Modification(ModificationType.DELETE, deletedValues)); |
| | | } |
| | | |
| | | final Attribute addedValues = new LinkedAttribute(newLDAPAttribute); |
| | | addedValues.removeAll(oldLDAPAttribute); |
| | | if (!addedValues.isEmpty()) { |
| | | modifications.add(new Modification(ModificationType.ADD, addedValues)); |
| | | } |
| | | return modifications; |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private List<Object> asList(final JsonValue v, final List<Object> defaultValues) { |
| | |
| | | } |
| | | } |
| | | |
| | | private ResultHandler<Attribute> createAttributeHandler(final JsonPointer path, |
| | | final ResultHandler<List<Attribute>> h) { |
| | | return new ResultHandler<Attribute>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final Attribute newLDAPAttribute) { |
| | | if (!writabilityPolicy.canCreate(ldapAttributeName)) { |
| | | if (newLDAPAttribute.isEmpty() || writabilityPolicy.discardWrites()) { |
| | | h.handleResult(Collections.<Attribute> emptyList()); |
| | | } else { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to create " |
| | | + "the read-only field '%s'", path))); |
| | | } |
| | | } else if (newLDAPAttribute.isEmpty()) { |
| | | if (isRequired) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to remove " |
| | | + "the required field '%s'", path))); |
| | | return; |
| | | } else { |
| | | h.handleResult(Collections.<Attribute> emptyList()); |
| | | } |
| | | } else { |
| | | h.handleResult(singletonList(newLDAPAttribute)); |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private void getNewLDAPAttributes(final Context c, final JsonPointer path, final JsonValue v, |
| | | final ResultHandler<Attribute> attributeHandler) { |
| | | private Promise<Attribute, ResourceException> getNewLDAPAttributes( |
| | | final RequestState requestState, final JsonPointer path, final JsonValue v) { |
| | | try { |
| | | // Ensure that the value is of the correct type. |
| | | checkSchema(path, v); |
| | | final List<Object> newValues = asList(v, defaultJSONValues); |
| | | if (newValues.isEmpty()) { |
| | | // Skip sub-class implementation if there are no values. |
| | | attributeHandler.handleResult(emptyAttribute(ldapAttributeName)); |
| | | return Promises.newResultPromise(emptyAttribute(ldapAttributeName)); |
| | | } else { |
| | | getNewLDAPAttributes(c, path, newValues, attributeHandler); |
| | | return getNewLDAPAttributes(requestState, path, newValues); |
| | | } |
| | | } catch (final Exception ex) { |
| | | attributeHandler.handleError(asResourceException(ex)); |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | } |
| | | } |
| | | |
| | | private ResultHandler<Attribute> updateAttributeHandler(final JsonPointer path, final Entry e, |
| | | final ResultHandler<List<Modification>> h) { |
| | | // Get the existing LDAP attribute. |
| | | final Attribute tmp = e.getAttribute(ldapAttributeName); |
| | | final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName); |
| | | return new ResultHandler<Attribute>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final Attribute newLDAPAttribute) { |
| | | /* |
| | | * If the attribute is read-only then handle the following |
| | | * cases: |
| | | * |
| | | * 1) new values are provided and they are the same as the |
| | | * existing values |
| | | * |
| | | * 2) no new values are provided. |
| | | */ |
| | | if (!writabilityPolicy.canWrite(ldapAttributeName)) { |
| | | if (newLDAPAttribute.isEmpty() || newLDAPAttribute.equals(oldLDAPAttribute) |
| | | || writabilityPolicy.discardWrites()) { |
| | | // No change. |
| | | h.handleResult(Collections.<Modification> emptyList()); |
| | | } else { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to modify " |
| | | + "the read-only field '%s'", path))); |
| | | } |
| | | } else { |
| | | // Compute the changes to the attribute. |
| | | final List<Modification> modifications; |
| | | if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) { |
| | | // No change. |
| | | modifications = Collections.<Modification> emptyList(); |
| | | } else if (oldLDAPAttribute.isEmpty()) { |
| | | // The attribute is being added. |
| | | modifications = |
| | | singletonList(new Modification(ModificationType.REPLACE, |
| | | newLDAPAttribute)); |
| | | } else if (newLDAPAttribute.isEmpty()) { |
| | | /* |
| | | * The attribute is being deleted - this is not allowed |
| | | * if the attribute is required. |
| | | */ |
| | | if (isRequired) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to remove " |
| | | + "the required field '%s'", path))); |
| | | return; |
| | | } else { |
| | | modifications = |
| | | singletonList(new Modification(ModificationType.REPLACE, |
| | | newLDAPAttribute)); |
| | | } |
| | | } else { |
| | | /* |
| | | * We could do a replace, but try to save bandwidth and |
| | | * send diffs instead. Perform deletes first in case we |
| | | * don't have an appropriate normalizer: permissive |
| | | * add(x) followed by delete(x) is destructive, whereas |
| | | * delete(x) followed by add(x) is idempotent when |
| | | * adding/removing the same value. |
| | | */ |
| | | modifications = new ArrayList<>(2); |
| | | |
| | | final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute); |
| | | deletedValues.removeAll(newLDAPAttribute); |
| | | if (!deletedValues.isEmpty()) { |
| | | modifications.add(new Modification(ModificationType.DELETE, |
| | | deletedValues)); |
| | | } |
| | | |
| | | final Attribute addedValues = new LinkedAttribute(newLDAPAttribute); |
| | | addedValues.removeAll(oldLDAPAttribute); |
| | | if (!addedValues.isEmpty()) { |
| | | modifications.add(new Modification(ModificationType.ADD, addedValues)); |
| | | } |
| | | } |
| | | h.handleResult(modifications); |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2013 ForgeRock AS. |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * An attribute mapper is responsible for converting JSON values to and from |
| | | * LDAP attributes. |
| | | */ |
| | | /** An attribute mapper is responsible for converting JSON values to and from LDAP attributes. */ |
| | | public abstract class AttributeMapper { |
| | | /* |
| | | * This interface is an abstract class so that methods can be made package |
| | |
| | | } |
| | | |
| | | /** |
| | | * Maps a JSON value to one or more LDAP attributes, invoking a completion |
| | | * handler once the transformation has completed. This method is invoked |
| | | * when a REST resource is created using a create request. |
| | | * Maps a JSON value to one or more LDAP attributes, returning a promise |
| | | * once the transformation has completed. This method is invoked when a REST |
| | | * resource is created using a create request. |
| | | * <p> |
| | | * If the JSON value corresponding to this mapper is not present in the |
| | | * resource then this method will be invoked with a value of {@code null}. |
| | | * It is the responsibility of the mapper implementation to take appropriate |
| | | * action in this case, perhaps by substituting default LDAP values, or by |
| | | * rejecting the update by invoking the result handler's |
| | | * {@link ResultHandler#handleError handleError} method. |
| | | * returning a failed promise with an appropriate {@link ResourceException}. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute mapper. This may be used when constructing error |
| | |
| | | * The JSON value to be converted to LDAP attributes, which may |
| | | * be {@code null} indicating that the JSON value was not present |
| | | * in the resource. |
| | | * @param h |
| | | * The result handler. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract void create(Context c, JsonPointer path, JsonValue v, ResultHandler<List<Attribute>> h); |
| | | abstract Promise<List<Attribute>, ResourceException> create( |
| | | RequestState requestState, JsonPointer path, JsonValue v); |
| | | |
| | | /** |
| | | * Adds the names of the LDAP attributes required by this attribute mapper |
| | |
| | | * Implementations should only add the names of attributes found in the LDAP |
| | | * entry directly associated with the resource. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute mapper. This may be used when constructing error |
| | |
| | | * The set into which the required LDAP attribute names should be |
| | | * put. |
| | | */ |
| | | abstract void getLDAPAttributes(Context c, JsonPointer path, JsonPointer subPath, |
| | | Set<String> ldapAttributes); |
| | | abstract void getLDAPAttributes( |
| | | RequestState requestState, JsonPointer path, JsonPointer subPath, Set<String> ldapAttributes); |
| | | |
| | | /** |
| | | * Transforms the provided REST comparison filter parameters to an LDAP |
| | | * filter representation, invoking a completion handler once the |
| | | * transformation has completed. |
| | | * filter representation, returning a promise once the transformation has |
| | | * completed. |
| | | * <p> |
| | | * If an error occurred while constructing the LDAP filter, then the result |
| | | * handler's {@link ResultHandler#handleError handleError} method must be |
| | | * invoked with an appropriate exception indicating the problem which |
| | | * occurred. |
| | | * If an error occurred while constructing the LDAP filter, then a failed |
| | | * promise must be returned with an appropriate {@link ResourceException} |
| | | * indicating the problem which occurred. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute mapper. This may be used when constructing error |
| | |
| | | * @param valueAssertion |
| | | * The value assertion, or {@code null} if {@code type} is |
| | | * {@link FilterType#PRESENT}. |
| | | * @param h |
| | | * The result handler. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract void getLDAPFilter(Context c, JsonPointer path, JsonPointer subPath, FilterType type, |
| | | String operator, Object valueAssertion, ResultHandler<Filter> h); |
| | | abstract Promise<Filter, ResourceException> getLDAPFilter(RequestState requestState, JsonPointer path, |
| | | JsonPointer subPath, FilterType type, String operator, Object valueAssertion); |
| | | |
| | | /** |
| | | * Maps a JSON patch operation to one or more LDAP modifications, invoking a |
| | | * completion handler once the transformation has completed. This method is |
| | | * invoked when a REST resource is modified using a patch request. |
| | | * Maps a JSON patch operation to one or more LDAP modifications, returning |
| | | * a promise once the transformation has completed. This method is invoked |
| | | * when a REST resource is modified using a patch request. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute mapper. This may be used when constructing error |
| | |
| | | * modifications. The targeted JSON field will be relative to |
| | | * this attribute mapper, or root if all attributes associated |
| | | * with this mapper have been targeted. |
| | | * @param h |
| | | * The result handler. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract void patch(Context c, JsonPointer path, PatchOperation operation, |
| | | ResultHandler<List<Modification>> h); |
| | | abstract Promise<List<Modification>, ResourceException> patch( |
| | | RequestState requestState, JsonPointer path, PatchOperation operation); |
| | | |
| | | /** |
| | | * Maps one or more LDAP attributes to their JSON representation, invoking a |
| | | * completion handler once the transformation has completed. |
| | | * Maps one or more LDAP attributes to their JSON representation, returning |
| | | * a promise once the transformation has completed. |
| | | * <p> |
| | | * This method is invoked whenever an LDAP entry is converted to a REST |
| | | * resource, i.e. when responding to read, query, create, put, or patch |
| | | * requests. |
| | | * <p> |
| | | * If the LDAP attributes are not present in the entry, perhaps because they |
| | | * are optional, then implementations should invoke the result handler's |
| | | * {@link ResultHandler#handleResult handleResult} method with a result of |
| | | * {@code null}. If the LDAP attributes cannot be mapped for any other |
| | | * reason, perhaps because they are required but missing, or they contain |
| | | * unexpected content, then the result handler's |
| | | * {@link ResultHandler#handleError handleError} method must be invoked with |
| | | * an appropriate exception indicating the problem which occurred. |
| | | * are optional, then implementations should return a successful promise |
| | | * with a result of {@code null}. If the LDAP attributes cannot be mapped |
| | | * for any other reason, perhaps because they are required but missing, or |
| | | * they contain unexpected content, then a failed promise must be returned |
| | | * with an appropriate exception indicating the problem which occurred. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute mapper. This may be used when constructing error |
| | | * messages. |
| | | * @param e |
| | | * The LDAP entry to be converted to JSON. |
| | | * @param h |
| | | * The result handler. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract void read(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h); |
| | | abstract Promise<JsonValue, ResourceException> read(RequestState requestState, JsonPointer path, Entry e); |
| | | |
| | | /** |
| | | * Maps a JSON value to one or more LDAP modifications, invoking a |
| | | * completion handler once the transformation has completed. This method is |
| | | * invoked when a REST resource is modified using an update request. |
| | | * Maps a JSON value to one or more LDAP modifications, returning a promise |
| | | * once the transformation has completed. This method is invoked when a REST |
| | | * resource is modified using an update request. |
| | | * <p> |
| | | * If the JSON value corresponding to this mapper is not present in the |
| | | * resource then this method will be invoked with a value of {@code null}. |
| | | * It is the responsibility of the mapper implementation to take appropriate |
| | | * action in this case, perhaps by substituting default LDAP values, or by |
| | | * rejecting the update by invoking the result handler's |
| | | * {@link ResultHandler#handleError handleError} method. |
| | | * returning a failed promise with an appropriate {@link ResourceException}. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param v |
| | | * The JSON value to be converted to LDAP attributes, which may |
| | | * be {@code null} indicating that the JSON value was not present |
| | | * in the resource. |
| | | * @param h |
| | | * The result handler. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract void update(Context c, JsonPointer path, Entry e, JsonValue v, |
| | | ResultHandler<List<Modification>> h); |
| | | abstract Promise<List<Modification>, ResourceException> update( |
| | | RequestState requestState, JsonPointer path, Entry e, JsonValue v); |
| | | |
| | | // TODO: methods for obtaining schema information (e.g. name, description, |
| | | // type information). |
| | | // TODO: methods for obtaining schema information (e.g. name, description, type information). |
| | | // TODO: methods for creating sort controls. |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013 ForgeRock AS. |
| | | * Copyright 2013-2015 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.*; |
| | | |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.Context; |
| | | import org.forgerock.json.resource.InternalServerErrorException; |
| | | import org.forgerock.json.resource.PersistenceConfig; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.http.Context; |
| | | import org.forgerock.http.context.AbstractContext; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | |
| | | /** |
| | |
| | | * servlet filter. It is the responsibility of the component which acquired the |
| | | * connection to release once processing has completed. |
| | | */ |
| | | public final class AuthenticatedConnectionContext extends Context { |
| | | public final class AuthenticatedConnectionContext extends AbstractContext { |
| | | /* |
| | | * TODO: this context does not support persistence because there is no |
| | | * obvious way to restore the connection. We could just persist the context |
| | |
| | | * re-used for subsequent LDAP operations. |
| | | */ |
| | | public AuthenticatedConnectionContext(final Context parent, final Connection connection) { |
| | | super(ensureNotNull(parent)); |
| | | super(ensureNotNull(parent), "authenticated connection"); |
| | | this.connection = connection; |
| | | } |
| | | |
| | |
| | | * The cached pre-authenticated LDAP connection which should be |
| | | * re-used for subsequent LDAP operations. |
| | | */ |
| | | public AuthenticatedConnectionContext(final String id, final Context parent, |
| | | AuthenticatedConnectionContext(final String id, final Context parent, |
| | | final Connection connection) { |
| | | super(id, ensureNotNull(parent)); |
| | | super(id, "authenticated connection", ensureNotNull(parent)); |
| | | this.connection = connection; |
| | | } |
| | | |
| | | /** |
| | | * Restore from JSON representation. |
| | | * |
| | | * @param savedContext |
| | | * The JSON representation from which this context's attributes |
| | | * should be parsed. |
| | | * @param config |
| | | * The persistence configuration. |
| | | * @throws ResourceException |
| | | * If the JSON representation could not be parsed. |
| | | */ |
| | | AuthenticatedConnectionContext(final JsonValue savedContext, final PersistenceConfig config) |
| | | throws ResourceException { |
| | | super(savedContext, config); |
| | | throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be restored")); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | protected void saveToJson(final JsonValue savedContext, final PersistenceConfig config) |
| | | throws ResourceException { |
| | | super.saveToJson(savedContext, config); |
| | | throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be persisted")); |
| | | } |
| | | |
| | | /** |
| | | * Returns the cached pre-authenticated LDAP connection which should be |
| | | * re-used for subsequent LDAP operations. |
| | | * |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013 ForgeRock AS. |
| | | * Copyright 2013-2015 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import org.forgerock.json.resource.QueryFilter; |
| | | import org.forgerock.util.query.QueryFilter; |
| | | |
| | | /** |
| | | * An enumeration of the commons REST query comparison filter types. |
| | | */ |
| | | /** An enumeration of the commons REST query comparison filter types. */ |
| | | enum FilterType { |
| | | |
| | | /** |
| | |
| | | * |
| | | * @see QueryFilter#startsWith |
| | | */ |
| | | STARTS_WITH; |
| | | STARTS_WITH |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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 2013-2015 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.json.resource.SecurityContext.AUTHZID_DN; |
| | | import static org.forgerock.json.resource.SecurityContext.AUTHZID_ID; |
| | | import static org.forgerock.opendj.ldap.Connections.uncloseable; |
| | | import static org.forgerock.opendj.ldap.LdapException.newLdapException; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newPlainSASLBindRequest; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | import static org.forgerock.util.Utils.closeSilently; |
| | | |
| | | import java.io.Closeable; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.StringTokenizer; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.http.Context; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.http.protocol.Status; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.JsonValueException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.SecurityContext; |
| | | import org.forgerock.opendj.ldap.AuthenticationException; |
| | | import org.forgerock.opendj.ldap.AuthorizationException; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.EntryNotFoundException; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.MultipleEntriesFoundException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.requests.BindRequest; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.opendj.ldap.responses.BindResult; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | |
| | | /** An LDAP based HTTP authentication filter. */ |
| | | final class HttpAuthenticationFilter implements org.forgerock.http.Filter, Closeable { |
| | | |
| | | /** Indicates how authentication should be performed. */ |
| | | private enum AuthenticationMethod { |
| | | SASL_PLAIN, |
| | | SEARCH_SIMPLE, |
| | | SIMPLE |
| | | } |
| | | |
| | | private final Schema schema = Schema.getDefaultSchema(); |
| | | private final String altAuthenticationPasswordHeader; |
| | | private final String altAuthenticationUsernameHeader; |
| | | private final AuthenticationMethod authenticationMethod; |
| | | private final ConnectionFactory bindLDAPConnectionFactory; |
| | | private final boolean reuseAuthenticatedConnection; |
| | | private final String saslAuthzIdTemplate; |
| | | private final DN searchBaseDN; |
| | | private final String searchFilterTemplate; |
| | | private final ConnectionFactory searchLDAPConnectionFactory; |
| | | private final SearchScope searchScope; |
| | | private final boolean supportAltAuthentication; |
| | | |
| | | private final boolean supportHTTPBasicAuthentication; |
| | | |
| | | HttpAuthenticationFilter(final JsonValue configuration) { |
| | | // Parse the authentication configuration. |
| | | final JsonValue authnConfig = configuration.get("authenticationFilter"); |
| | | supportHTTPBasicAuthentication = authnConfig.get("supportHTTPBasicAuthentication").required().asBoolean(); |
| | | |
| | | // Alternative HTTP authentication. |
| | | supportAltAuthentication = authnConfig.get("supportAltAuthentication").required().asBoolean(); |
| | | if (supportAltAuthentication) { |
| | | altAuthenticationUsernameHeader = authnConfig.get("altAuthenticationUsernameHeader").required().asString(); |
| | | altAuthenticationPasswordHeader = authnConfig.get("altAuthenticationPasswordHeader").required().asString(); |
| | | } else { |
| | | altAuthenticationUsernameHeader = null; |
| | | altAuthenticationPasswordHeader = null; |
| | | } |
| | | |
| | | // Should the authenticated connection should be cached for use by subsequent LDAP operations? |
| | | reuseAuthenticatedConnection = authnConfig.get("reuseAuthenticatedConnection").required().asBoolean(); |
| | | |
| | | // Parse the authentication method and associated parameters. |
| | | authenticationMethod = parseAuthenticationMethod(authnConfig); |
| | | switch (authenticationMethod) { |
| | | case SASL_PLAIN: |
| | | saslAuthzIdTemplate = authnConfig.get("saslAuthzIdTemplate").required().asString(); |
| | | searchBaseDN = null; |
| | | searchScope = null; |
| | | searchFilterTemplate = null; |
| | | searchLDAPConnectionFactory = null; |
| | | break; |
| | | case SEARCH_SIMPLE: |
| | | searchBaseDN = DN.valueOf(authnConfig.get("searchBaseDN").required().asString(), schema); |
| | | searchScope = parseSearchScope(authnConfig); |
| | | searchFilterTemplate = authnConfig.get("searchFilterTemplate").required().asString(); |
| | | |
| | | // Parse the LDAP connection factory to be used for searches. |
| | | final String ldapFactoryName = authnConfig.get("searchLDAPConnectionFactory").required().asString(); |
| | | searchLDAPConnectionFactory = Rest2LDAP |
| | | .configureConnectionFactory(configuration.get("ldapConnectionFactories").required(), ldapFactoryName); |
| | | |
| | | saslAuthzIdTemplate = null; |
| | | break; |
| | | case SIMPLE: |
| | | default: |
| | | saslAuthzIdTemplate = null; |
| | | searchBaseDN = null; |
| | | searchScope = null; |
| | | searchFilterTemplate = null; |
| | | searchLDAPConnectionFactory = null; |
| | | break; |
| | | } |
| | | |
| | | // Parse the LDAP connection factory to be used for binds. |
| | | final String ldapFactoryName = authnConfig.get("bindLDAPConnectionFactory").required().asString(); |
| | | bindLDAPConnectionFactory = Rest2LDAP.configureConnectionFactory( |
| | | configuration.get("ldapConnectionFactories").required(), ldapFactoryName); |
| | | } |
| | | |
| | | private static AuthenticationMethod parseAuthenticationMethod(final JsonValue configuration) { |
| | | if (configuration.isDefined("method")) { |
| | | final String method = configuration.get("method").asString(); |
| | | if ("simple".equalsIgnoreCase(method)) { |
| | | return AuthenticationMethod.SIMPLE; |
| | | } else if ("sasl-plain".equalsIgnoreCase(method)) { |
| | | return AuthenticationMethod.SASL_PLAIN; |
| | | } else if ("search-simple".equalsIgnoreCase(method)) { |
| | | return AuthenticationMethod.SEARCH_SIMPLE; |
| | | } else { |
| | | throw new JsonValueException(configuration, |
| | | "Illegal authentication method: must be either 'simple', 'sasl-plain', or 'search-simple'"); |
| | | } |
| | | } else { |
| | | return AuthenticationMethod.SEARCH_SIMPLE; |
| | | } |
| | | } |
| | | |
| | | private static SearchScope parseSearchScope(final JsonValue configuration) { |
| | | if (configuration.isDefined("searchScope")) { |
| | | final String scope = configuration.get("searchScope").asString(); |
| | | if ("sub".equalsIgnoreCase(scope)) { |
| | | return SearchScope.WHOLE_SUBTREE; |
| | | } else if ("one".equalsIgnoreCase(scope)) { |
| | | return SearchScope.SINGLE_LEVEL; |
| | | } else { |
| | | throw new JsonValueException(configuration, "Illegal search scope: must be either 'sub' or 'one'"); |
| | | } |
| | | } else { |
| | | return SearchScope.WHOLE_SUBTREE; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> filter(final Context context, final Request request, |
| | | final Handler next) { |
| | | // Store the authenticated connection so that it can be re-used by the handler if needed. |
| | | // However, make sure that it is closed on completion. |
| | | try { |
| | | final String headerUsername = |
| | | supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationUsernameHeader) : null; |
| | | final String headerPassword = |
| | | supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationPasswordHeader) : null; |
| | | final String headerAuthorization = |
| | | supportHTTPBasicAuthentication ? request.getHeaders().getFirst("Authorization") : null; |
| | | |
| | | final String username; |
| | | final char[] password; |
| | | if (headerUsername != null) { |
| | | if (headerPassword == null || headerUsername.isEmpty() || headerPassword.isEmpty()) { |
| | | throw ResourceException.getException(401); |
| | | } |
| | | username = headerUsername; |
| | | password = headerPassword.toCharArray(); |
| | | } else if (headerAuthorization != null) { |
| | | final StringTokenizer st = new StringTokenizer(headerAuthorization); |
| | | final String method = st.nextToken(); |
| | | if (method == null || !"BASIC".equalsIgnoreCase(method)) { |
| | | throw ResourceException.getException(401); |
| | | } |
| | | final String b64Credentials = st.nextToken(); |
| | | if (b64Credentials == null) { |
| | | throw ResourceException.getException(401); |
| | | } |
| | | final String credentials = ByteString.valueOfBase64(b64Credentials).toString(); |
| | | final String[] usernameAndPassword = credentials.split(":"); |
| | | if (usernameAndPassword.length != 2) { |
| | | throw ResourceException.getException(401); |
| | | } |
| | | username = usernameAndPassword[0]; |
| | | password = usernameAndPassword[1].toCharArray(); |
| | | } else { |
| | | throw ResourceException.getException(401); |
| | | } |
| | | |
| | | // If we've got here then we have a username and password. |
| | | switch (authenticationMethod) { |
| | | case SIMPLE: { |
| | | final Map<String, Object> authzid; |
| | | authzid = new LinkedHashMap<>(2); |
| | | authzid.put(AUTHZID_DN, username); |
| | | authzid.put(AUTHZID_ID, username); |
| | | return doBind( |
| | | context, request, next, Requests.newSimpleBindRequest(username, password), username, authzid); |
| | | } |
| | | case SASL_PLAIN: { |
| | | final Map<String, Object> authzid; |
| | | final String bindId; |
| | | if (saslAuthzIdTemplate.startsWith("dn:")) { |
| | | final String bindDN = DN.format(saslAuthzIdTemplate.substring(3), schema, username).toString(); |
| | | bindId = "dn:" + bindDN; |
| | | authzid = new LinkedHashMap<>(2); |
| | | authzid.put(AUTHZID_DN, bindDN); |
| | | authzid.put(AUTHZID_ID, username); |
| | | } else { |
| | | bindId = String.format(saslAuthzIdTemplate, username); |
| | | authzid = Collections.singletonMap(AUTHZID_ID, (Object) username); |
| | | } |
| | | return doBind(context, request, next, newPlainSASLBindRequest(bindId, password), username, authzid); |
| | | } |
| | | default: // SEARCH_SIMPLE |
| | | final AtomicReference<Connection> savedConnection = new AtomicReference<>(); |
| | | return searchLDAPConnectionFactory.getConnectionAsync() |
| | | .thenAsync(doSearchForUser(username, savedConnection)) |
| | | .thenAsync(doBindAfterSearch(context, request, next, username, password, savedConnection), |
| | | returnErrorAfterFailedSearch(savedConnection)); |
| | | } |
| | | } catch (final Throwable t) { |
| | | return asErrorResponse(t); |
| | | } |
| | | } |
| | | |
| | | private AsyncFunction<Connection, SearchResultEntry, LdapException> doSearchForUser( |
| | | final String username, final AtomicReference<Connection> savedConnection) { |
| | | return new AsyncFunction<Connection, SearchResultEntry, LdapException>() { |
| | | @Override |
| | | public Promise<SearchResultEntry, LdapException> apply(final Connection connection) throws LdapException { |
| | | savedConnection.set(connection); |
| | | final Filter filter = Filter.format(searchFilterTemplate, username); |
| | | return connection.searchSingleEntryAsync(newSearchRequest(searchBaseDN, searchScope, filter, "1.1")); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch( |
| | | final Context context, final Request request, final Handler next, final String username, |
| | | final char[] password, final AtomicReference<Connection> savedConnection) { |
| | | return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(final SearchResultEntry entry) { |
| | | closeConnection(savedConnection); |
| | | final String bindDN = entry.getName().toString(); |
| | | final Map<String, Object> authzid = new LinkedHashMap<>(2); |
| | | authzid.put(AUTHZID_DN, bindDN); |
| | | authzid.put(AUTHZID_ID, username); |
| | | return doBind(context, request, next, newSimpleBindRequest(bindDN, password), username, authzid); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Get a bind connection and then perform the bind operation, setting the cached connection and authorization |
| | | * credentials on completion. |
| | | */ |
| | | private Promise<Response, NeverThrowsException> doBind( |
| | | final Context context, final Request request, final Handler next, final BindRequest bindRequest, |
| | | final String authcid, final Map<String, Object> authzid) { |
| | | final AtomicReference<Connection> savedConnection = new AtomicReference<>(); |
| | | return bindLDAPConnectionFactory.getConnectionAsync() |
| | | .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() { |
| | | @Override |
| | | public Promise<BindResult, LdapException> apply(final Connection connection) throws LdapException { |
| | | savedConnection.set(connection); |
| | | return connection.bindAsync(bindRequest); |
| | | } |
| | | }) |
| | | .thenAsync(doChain(context, request, next, authcid, authzid, savedConnection), |
| | | returnErrorAfterFailedBind()) |
| | | .thenFinally(new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | closeConnection(savedConnection); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private AsyncFunction<BindResult, Response, NeverThrowsException> doChain( |
| | | final Context context, final Request request, final Handler next, final String authcid, |
| | | final Map<String, Object> authzid, final AtomicReference<Connection> savedConnection) { |
| | | return new AsyncFunction<BindResult, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(final BindResult result) { |
| | | // Pass through the authentication ID and authorization principals. |
| | | Context forwardedContext = new SecurityContext(context, authcid, authzid); |
| | | |
| | | // Cache the pre-authenticated connection and prevent downstream |
| | | // components from closing it since this filter will close it. |
| | | if (reuseAuthenticatedConnection) { |
| | | forwardedContext = new AuthenticatedConnectionContext( |
| | | forwardedContext, uncloseable(savedConnection.get())); |
| | | } |
| | | |
| | | return next.handle(forwardedContext, request); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch( |
| | | final AtomicReference<Connection> savedConnection) { |
| | | return new AsyncFunction<LdapException, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(final LdapException e) { |
| | | if (closeConnection(savedConnection)) { |
| | | // The search error should not be passed as-is back to the user. |
| | | if (e instanceof EntryNotFoundException || e instanceof MultipleEntriesFoundException) { |
| | | return asErrorResponse(newLdapException(ResultCode.INVALID_CREDENTIALS, e)); |
| | | } else if (e instanceof AuthenticationException || e instanceof AuthorizationException) { |
| | | return asErrorResponse(newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e)); |
| | | } else { |
| | | return asErrorResponse(e); |
| | | } |
| | | } else { |
| | | return asErrorResponse(e); |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind() { |
| | | return new AsyncFunction<LdapException, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(final LdapException e) { |
| | | return asErrorResponse(e); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t) { |
| | | final ResourceException e = asResourceException(t); |
| | | final Response response = |
| | | new Response().setStatus(Status.valueOf(e.getCode())).setEntity(e.toJsonValue().getObject()); |
| | | return Promises.newResultPromise(response); |
| | | } |
| | | |
| | | private boolean closeConnection(final AtomicReference<Connection> savedConnection) { |
| | | final Connection connection = savedConnection.get(); |
| | | if (connection != null) { |
| | | connection.close(); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public void close() { |
| | | closeSilently(searchLDAPConnectionFactory, bindLDAPConnectionFactory); |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2013 ForgeRock AS. |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | |
| | | /** |
| | | * An attribute mapper which maps a single JSON attribute to a fixed value. |
| | |
| | | } |
| | | |
| | | @Override |
| | | void create(final Context c, final JsonPointer path, final JsonValue v, |
| | | final ResultHandler<List<Attribute>> h) { |
| | | Promise<List<Attribute>, ResourceException> create( |
| | | final RequestState requestState, final JsonPointer path, final JsonValue v) { |
| | | if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to create " |
| | | + "the read-only field '%s'", path))); |
| | | return Promises.<List<Attribute>, ResourceException> newExceptionPromise(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to create the read-only field '%s'", path))); |
| | | } else { |
| | | h.handleResult(Collections.<Attribute> emptyList()); |
| | | return Promises.newResultPromise(Collections.<Attribute> emptyList()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | // Nothing to do. |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final FilterType type, final String operator, final Object valueAssertion, |
| | | final ResultHandler<Filter> h) { |
| | | Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) { |
| | | final Filter filter; |
| | | final JsonValue subValue = value.get(subPath); |
| | | if (subValue == null) { |
| | |
| | | // This attribute mapper is a candidate but it does not match. |
| | | filter = alwaysFalse(); |
| | | } |
| | | h.handleResult(filter); |
| | | return Promises.newResultPromise(filter); |
| | | } |
| | | |
| | | @Override |
| | | void patch(final Context c, final JsonPointer path, final PatchOperation operation, |
| | | final ResultHandler<List<Modification>> h) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to patch " |
| | | + "the read-only field '%s'", path))); |
| | | Promise<List<Modification>, ResourceException> patch(final RequestState requestState, final JsonPointer path, |
| | | final PatchOperation operation) { |
| | | return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to patch the read-only field '%s'", path))); |
| | | } |
| | | |
| | | @Override |
| | | void read(final Context c, final JsonPointer path, final Entry e, |
| | | final ResultHandler<JsonValue> h) { |
| | | h.handleResult(value.copy()); |
| | | Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) { |
| | | return Promises.newResultPromise(value.copy()); |
| | | } |
| | | |
| | | @Override |
| | | void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v, |
| | | final ResultHandler<List<Modification>> h) { |
| | | Promise<List<Modification>, ResourceException> update( |
| | | final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to modify " |
| | | + "the read-only field '%s'", path))); |
| | | return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to modify the read-only field '%s'", path))); |
| | | } else { |
| | | h.handleResult(Collections.<Modification> emptyList()); |
| | | return Promises.newResultPromise(Collections.<Modification> emptyList()); |
| | | } |
| | | } |
| | | |
| | |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static java.util.Arrays.asList; |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysFalse; |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysTrue; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; |
| | | import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toFilter; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.http.Context; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.ActionRequest; |
| | | import org.forgerock.json.resource.ActionResponse; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.CreateRequest; |
| | | import org.forgerock.json.resource.DeleteRequest; |
| | |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.PatchRequest; |
| | | import org.forgerock.json.resource.PreconditionFailedException; |
| | | import org.forgerock.json.resource.QueryFilter; |
| | | import org.forgerock.json.resource.QueryFilterVisitor; |
| | | import org.forgerock.json.resource.QueryRequest; |
| | | import org.forgerock.json.resource.QueryResult; |
| | | import org.forgerock.json.resource.QueryResultHandler; |
| | | import org.forgerock.json.resource.QueryResourceHandler; |
| | | import org.forgerock.json.resource.QueryResponse; |
| | | import org.forgerock.json.resource.ReadRequest; |
| | | import org.forgerock.json.resource.Resource; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.json.resource.ServerContext; |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.json.resource.Responses; |
| | | import org.forgerock.json.resource.UncategorizedException; |
| | | import org.forgerock.json.resource.UpdateRequest; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.DecodeOptions; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultReference; |
| | | import org.forgerock.opendj.ldif.ChangeRecord; |
| | | import org.forgerock.util.promise.ExceptionHandler; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.ExceptionHandler; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.PromiseImpl; |
| | | import org.forgerock.util.promise.Promises; |
| | | |
| | | import static java.util.Arrays.*; |
| | | |
| | | import static org.forgerock.opendj.ldap.Filter.*; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.*; |
| | | import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.*; |
| | | import org.forgerock.util.promise.ResultHandler; |
| | | import org.forgerock.util.query.QueryFilter; |
| | | import org.forgerock.util.query.QueryFilterVisitor; |
| | | |
| | | /** |
| | | * A {@code CollectionResourceProvider} implementation which maps a JSON |
| | | * resource collection to LDAP entries beneath a base DN. |
| | | */ |
| | | final class LDAPCollectionResourceProvider implements CollectionResourceProvider { |
| | | private static class ResultHandlerFromPromise<T> implements ResultHandler<T> { |
| | | private final PromiseImpl<T, ResourceException> promise; |
| | | |
| | | ResultHandlerFromPromise() { |
| | | promise = PromiseImpl.create(); |
| | | } |
| | | |
| | | @Override |
| | | public void handleError(ResourceException error) { |
| | | promise.handleException(error); |
| | | |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(T result) { |
| | | promise.handleResult(result); |
| | | } |
| | | } |
| | | |
| | | /** Dummy exception used for signalling search success. */ |
| | | private static final ResourceException SUCCESS = new UncategorizedException(0, null, null); |
| | | |
| | |
| | | } |
| | | |
| | | @Override |
| | | public void actionCollection(final ServerContext context, final ActionRequest request, |
| | | final ResultHandler<JsonValue> handler) { |
| | | handler.handleError(new NotSupportedException("Not yet implemented")); |
| | | public Promise<ActionResponse, ResourceException> actionCollection( |
| | | final Context context, final ActionRequest request) { |
| | | return Promises.<ActionResponse, ResourceException> newExceptionPromise( |
| | | new NotSupportedException("Not yet implemented")); |
| | | } |
| | | |
| | | @Override |
| | | public void actionInstance(final ServerContext context, final String resourceId, |
| | | final ActionRequest request, final ResultHandler<JsonValue> handler) { |
| | | handler.handleError(new NotSupportedException("Not yet implemented")); |
| | | public Promise<ActionResponse, ResourceException> actionInstance( |
| | | final Context context, final String resourceId, final ActionRequest request) { |
| | | return Promises.<ActionResponse, ResourceException> newExceptionPromise( |
| | | new NotSupportedException("Not yet implemented")); |
| | | } |
| | | |
| | | @Override |
| | | public void createInstance(final ServerContext context, final CreateRequest request, |
| | | final ResultHandler<Resource> handler) { |
| | | final Context c = wrap(context); |
| | | final ResultHandler<Resource> h = wrap(c, handler); |
| | | public Promise<ResourceResponse, ResourceException> createInstance( |
| | | final Context context, final CreateRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | |
| | | // Get the connection, then determine entry content, then perform add. |
| | | c.run(h, new Runnable() { |
| | | return requestState.getConnection().thenAsync( |
| | | new AsyncFunction<Connection, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void run() { |
| | | public Promise<ResourceResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | // Calculate entry content. |
| | | attributeMapper.create(c, new JsonPointer(), request.getContent(), |
| | | new ResultHandler<List<Attribute>>() { |
| | | return attributeMapper.create(requestState, new JsonPointer(), request.getContent()) |
| | | .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final List<Attribute> result) { |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | final List<Attribute> attributes) { |
| | | // Perform add operation. |
| | | final AddRequest addRequest = newAddRequest(DN.rootDN()); |
| | | for (final Attribute attribute : additionalLDAPAttributes) { |
| | | addRequest.addAttribute(attribute); |
| | | } |
| | | for (final Attribute attribute : result) { |
| | | for (final Attribute attribute : attributes) { |
| | | addRequest.addAttribute(attribute); |
| | | } |
| | | try { |
| | | nameStrategy.setResourceId(c, getBaseDN(c), request.getNewResourceId(), addRequest); |
| | | nameStrategy.setResourceId(requestState, getBaseDN(requestState), |
| | | request.getNewResourceId(), addRequest); |
| | | } catch (final ResourceException e) { |
| | | h.handleError(e); |
| | | return; |
| | | return Promises.newExceptionPromise(e); |
| | | } |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | final String[] attributes = getLDAPAttributes(c, request.getFields()); |
| | | addRequest.addControl(PostReadRequestControl.newControl(false, attributes)); |
| | | addRequest.addControl(PostReadRequestControl.newControl( |
| | | false, getLDAPAttributes(requestState, request.getFields()))); |
| | | } |
| | | c.getConnection().applyChangeAsync(addRequest) |
| | | .thenOnResult(postUpdateResultHandler(c, h)) |
| | | .thenOnException(postUpdateExceptionHandler(h)); |
| | | return connection.applyChangeAsync(addRequest) |
| | | .thenAsync(postUpdateResultAsyncFunction(requestState), |
| | | ldapExceptionToResourceException()); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | }).thenFinally(close(requestState)); |
| | | } |
| | | |
| | | @Override |
| | | public void deleteInstance(final ServerContext context, final String resourceId, |
| | | final DeleteRequest request, final ResultHandler<Resource> handler) { |
| | | final Context c = wrap(context); |
| | | final ResultHandler<Resource> h = wrap(c, handler); |
| | | |
| | | // Get connection, search if needed, then delete. |
| | | c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() { |
| | | public Promise<ResourceResponse, ResourceException> deleteInstance( |
| | | final Context context, final String resourceId, final DeleteRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | final AtomicReference<Connection> connectionHolder = new AtomicReference<>(); |
| | | return requestState.getConnection() |
| | | .thenOnResult(saveConnection(connectionHolder)) |
| | | .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision())) |
| | | .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final DN dn) { |
| | | public Promise<ResourceResponse, ResourceException> apply(DN dn) throws ResourceException { |
| | | try { |
| | | final ChangeRecord deleteRequest = newDeleteRequest(dn); |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | final String[] attributes = getLDAPAttributes(c, request.getFields()); |
| | | final String[] attributes = getLDAPAttributes(requestState, request.getFields()); |
| | | deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (config.useSubtreeDelete()) { |
| | | deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(deleteRequest, request.getRevision()); |
| | | c.getConnection().applyChangeAsync(deleteRequest).thenOnResult(postUpdateResultHandler(c, h)) |
| | | .thenOnException(postUpdateExceptionHandler(h)); |
| | | return connectionHolder.get().applyChangeAsync(deleteRequest) |
| | | .thenAsync(postUpdateResultAsyncFunction(requestState), |
| | | ldapExceptionToResourceException()); |
| | | |
| | | } catch (final Exception e) { |
| | | h.handleError(asResourceException(e)); |
| | | return Promises.newExceptionPromise((asResourceException(e))); |
| | | } |
| | | } |
| | | })); |
| | | }).thenFinally(close(requestState)); |
| | | } |
| | | |
| | | @Override |
| | | public void patchInstance(final ServerContext context, final String resourceId, final PatchRequest request, |
| | | final ResultHandler<Resource> handler) { |
| | | final Context c = wrap(context); |
| | | final ResultHandler<Resource> h = wrap(c, handler); |
| | | public Promise<ResourceResponse, ResourceException> patchInstance( |
| | | final Context context, final String resourceId, final PatchRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | |
| | | if (request.getPatchOperations().isEmpty()) { |
| | | /* |
| | | * This patch is a no-op so just read the entry and check its version. |
| | | */ |
| | | c.run(h, new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | final String[] attributes = getLDAPAttributes(c, request.getFields()); |
| | | final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId) |
| | | .addAttribute(attributes); |
| | | c.getConnection().searchSingleEntryAsync(searchRequest) |
| | | .thenOnResult(postEmptyPatchResultHandler(c, request, h)) |
| | | .thenOnException(postEmptyPatchExceptionHandler(h)); |
| | | } |
| | | }); |
| | | } else { |
| | | /* |
| | | * Get the connection, search if needed, then determine modifications, then perform modify. |
| | | */ |
| | | c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | return emptyPatchInstance(requestState, resourceId, request); |
| | | } |
| | | |
| | | final AtomicReference<Connection> connectionHolder = new AtomicReference<>(); |
| | | return requestState.getConnection() |
| | | .thenOnResult(saveConnection(connectionHolder)) |
| | | .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision())) |
| | | .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void handleResult(final DN dn) { |
| | | public Promise<ResourceResponse, ResourceException> apply(final DN dn) throws ResourceException { |
| | | // Convert the patch operations to LDAP modifications. |
| | | List<Promise<List<Modification>, ResourceException>> promises = |
| | | new ArrayList<>(request.getPatchOperations().size()); |
| | | for (final PatchOperation operation : request.getPatchOperations()) { |
| | | final ResultHandlerFromPromise<List<Modification>> handler = new ResultHandlerFromPromise<>(); |
| | | attributeMapper.patch(c, new JsonPointer(), operation, handler); |
| | | promises.add(handler.promise); |
| | | promises.add(attributeMapper.patch(requestState, new JsonPointer(), operation)); |
| | | } |
| | | |
| | | Promises.when(promises).thenOnResult( |
| | | new org.forgerock.util.promise.ResultHandler<List<List<Modification>>>() { |
| | | return Promises.when(promises).thenAsync( |
| | | new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void handleResult(final List<List<Modification>> result) { |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | final List<List<Modification>> result) { |
| | | // The patch operations have been converted successfully. |
| | | try { |
| | | final ModifyRequest modifyRequest = newModifyRequest(dn); |
| | |
| | | } |
| | | } |
| | | |
| | | final List<String> attributes = asList(getLDAPAttributes(c, request.getFields())); |
| | | final List<String> attributes = |
| | | asList(getLDAPAttributes(requestState, request.getFields())); |
| | | if (modifyRequest.getModifications().isEmpty()) { |
| | | /* |
| | | * This patch is a no-op so just read the entry and check its version. |
| | | */ |
| | | c.getConnection().readEntryAsync(dn, attributes) |
| | | .thenOnResult(postEmptyPatchResultHandler(c, request, h)) |
| | | .thenOnException(postEmptyPatchExceptionHandler(h)); |
| | | // This patch is a no-op so just read the entry and check its version. |
| | | return connectionHolder.get() |
| | | .readEntryAsync(dn, attributes) |
| | | .thenAsync(postEmptyPatchAsyncFunction(requestState, request), |
| | | ldapExceptionToResourceException()); |
| | | } else { |
| | | // Add controls and perform the modify request. |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | |
| | | PostReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (config.usePermissiveModify()) { |
| | | modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true)); |
| | | modifyRequest.addControl( |
| | | PermissiveModifyRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | c.getConnection().applyChangeAsync(modifyRequest) |
| | | .thenOnResult(postUpdateResultHandler(c, h)) |
| | | .thenOnException(postUpdateExceptionHandler(h)); |
| | | return connectionHolder.get() |
| | | .applyChangeAsync(modifyRequest) |
| | | .thenAsync(postUpdateResultAsyncFunction(requestState), |
| | | ldapExceptionToResourceException()); |
| | | } |
| | | } catch (final Exception e) { |
| | | h.handleError(asResourceException(e)); |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }).thenOnException(new ExceptionHandler<ResourceException>() { |
| | | @Override |
| | | public void handleException(ResourceException exception) { |
| | | h.handleError(asResourceException(exception)); |
| | | } |
| | | }); |
| | | } |
| | | })); |
| | | }).thenFinally(close(requestState)); |
| | | } |
| | | |
| | | /** Just read the entry and check its version. */ |
| | | private Promise<ResourceResponse, ResourceException> emptyPatchInstance( |
| | | final RequestState requestState, final String resourceId, final PatchRequest request) { |
| | | return requestState.getConnection() |
| | | .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | final String[] attributes = getLDAPAttributes(requestState, request.getFields()); |
| | | final SearchRequest searchRequest = |
| | | nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId) |
| | | .addAttribute(attributes); |
| | | return connection.searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(postEmptyPatchAsyncFunction(requestState, request), |
| | | ldapExceptionToResourceException()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction( |
| | | final RequestState requestState, final PatchRequest request) { |
| | | return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) |
| | | throws ResourceException { |
| | | try { |
| | | // Fail if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | | return adaptEntry(requestState, entry); |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | @Override |
| | | public void queryCollection(final ServerContext context, final QueryRequest request, |
| | | final QueryResultHandler handler) { |
| | | final Context c = wrap(context); |
| | | final QueryResultHandler h = wrap(c, handler); |
| | | public Promise<QueryResponse, ResourceException> queryCollection( |
| | | final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) { |
| | | final RequestState requestState = wrap(context); |
| | | |
| | | /* |
| | | * Get the connection, then calculate the search filter, then perform the search. |
| | | */ |
| | | c.run(h, new Runnable() { |
| | | return requestState.getConnection() |
| | | .thenAsync(new AsyncFunction<Connection, QueryResponse, ResourceException>() { |
| | | @Override |
| | | public void run() { |
| | | public Promise<QueryResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | // Calculate the filter (this may require the connection). |
| | | getLDAPFilter(c, request.getQueryFilter(), new ResultHandler<Filter>() { |
| | | return getLDAPFilter(requestState, request.getQueryFilter()) |
| | | .thenAsync(runQuery(request, resourceHandler, requestState, connection)); |
| | | } |
| | | }) |
| | | .thenFinally(close(requestState)); |
| | | } |
| | | |
| | | private Promise<Filter, ResourceException> getLDAPFilter( |
| | | final RequestState requestState, final QueryFilter<JsonPointer> queryFilter) { |
| | | final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor = |
| | | new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() { |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitAndFilter(final Void unused, |
| | | final List<QueryFilter<JsonPointer>> subFilters) { |
| | | final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size()); |
| | | for (final QueryFilter<JsonPointer> subFilter : subFilters) { |
| | | promises.add(subFilter.accept(this, unused)); |
| | | } |
| | | |
| | | return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(final List<Filter> value) { |
| | | // Check for unmapped filter components and optimize. |
| | | final Iterator<Filter> i = value.iterator(); |
| | | while (i.hasNext()) { |
| | | final Filter f = i.next(); |
| | | if (f == alwaysFalse()) { |
| | | return alwaysFalse(); |
| | | } else if (f == alwaysTrue()) { |
| | | i.remove(); |
| | | } |
| | | } |
| | | switch (value.size()) { |
| | | case 0: |
| | | return alwaysTrue(); |
| | | case 1: |
| | | return value.get(0); |
| | | default: |
| | | return Filter.and(value); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitBooleanLiteralFilter( |
| | | final Void unused, final boolean value) { |
| | | return Promises.newResultPromise(toFilter(value)); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitContainsFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitEqualsFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused, |
| | | final JsonPointer field, final String operator, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitGreaterThanFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field, |
| | | FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitLessThanFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field, |
| | | FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitNotFilter( |
| | | final Void unused, final QueryFilter<JsonPointer> subFilter) { |
| | | return subFilter.accept(this, unused).then(new Function<Filter, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(final Filter value) { |
| | | if (value == null || value == alwaysFalse()) { |
| | | return alwaysTrue(); |
| | | } else if (value == alwaysTrue()) { |
| | | return alwaysFalse(); |
| | | } else { |
| | | return Filter.not(value); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitOrFilter(final Void unused, |
| | | final List<QueryFilter<JsonPointer>> subFilters) { |
| | | final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size()); |
| | | for (final QueryFilter<JsonPointer> subFilter : subFilters) { |
| | | promises.add(subFilter.accept(this, unused)); |
| | | } |
| | | |
| | | return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(final List<Filter> value) { |
| | | // Check for unmapped filter components and optimize. |
| | | final Iterator<Filter> i = value.iterator(); |
| | | while (i.hasNext()) { |
| | | final Filter f = i.next(); |
| | | if (f == alwaysFalse()) { |
| | | i.remove(); |
| | | } else if (f == alwaysTrue()) { |
| | | return alwaysTrue(); |
| | | } |
| | | } |
| | | switch (value.size()) { |
| | | case 0: |
| | | return alwaysFalse(); |
| | | case 1: |
| | | return value.get(0); |
| | | default: |
| | | return Filter.or(value); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitPresentFilter( |
| | | final Void unused, final JsonPointer field) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.PRESENT, null, null); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitStartsWithFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion); |
| | | } |
| | | |
| | | }; |
| | | // Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers. |
| | | return queryFilter.accept(visitor, null); |
| | | } |
| | | |
| | | private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request, |
| | | final QueryResourceHandler resourceHandler, final RequestState requestState, final Connection connection) { |
| | | return new AsyncFunction<Filter, QueryResponse, ResourceException>() { |
| | | /** |
| | | * The following fields are guarded by sequenceLock. In |
| | | * addition, the sequenceLock ensures that we send one JSON |
| | | * resource at a time back to the client. |
| | | * The following fields are guarded by sequenceLock. In addition, |
| | | * the sequenceLock ensures that we send one JSON resource at a time |
| | | * back to the client. |
| | | */ |
| | | private final Object sequenceLock = new Object(); |
| | | private String cookie; |
| | |
| | | private int totalResourceCount; |
| | | |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final Filter ldapFilter) { |
| | | /* |
| | | * Avoid performing a search if the filter could not be mapped or if it will never match. |
| | | */ |
| | | public Promise<QueryResponse, ResourceException> apply(final Filter ldapFilter) { |
| | | if (ldapFilter == null || ldapFilter == alwaysFalse()) { |
| | | h.handleResult(new QueryResult()); |
| | | } else { |
| | | // Avoid performing a search if the filter could not be mapped or if it will never match. |
| | | return Promises.newResultPromise(Responses.newQueryResponse()); |
| | | } |
| | | final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create(); |
| | | // Perform the search. |
| | | final String[] attributes = getLDAPAttributes(c, request.getFields()); |
| | | final Filter searchFilter = |
| | | ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent() : ldapFilter; |
| | | final SearchRequest searchRequest = |
| | | newSearchRequest(getBaseDN(c), SearchScope.SINGLE_LEVEL, searchFilter, attributes); |
| | | final String[] attributes = getLDAPAttributes(requestState, request.getFields()); |
| | | final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent() |
| | | : ldapFilter; |
| | | final SearchRequest searchRequest = newSearchRequest( |
| | | getBaseDN(requestState), SearchScope.SINGLE_LEVEL, searchFilter, attributes); |
| | | |
| | | /* |
| | | * Add the page results control. We can support the page offset by |
| | | * reading the next offset pages, or offset x page size resources. |
| | | */ |
| | | // Add the page results control. We can support the page offset by |
| | | // reading the next offset pages, or offset x page size resources. |
| | | final int pageResultStartIndex; |
| | | final int pageSize = request.getPageSize(); |
| | | if (request.getPageSize() > 0) { |
| | |
| | | pageResultStartIndex = 0; |
| | | pageResultEndIndex = pageSize; |
| | | } |
| | | final ByteString cookie = |
| | | request.getPagedResultsCookie() != null ? ByteString |
| | | .valueOfBase64(request.getPagedResultsCookie()) |
| | | : ByteString.empty(); |
| | | final ByteString cookie = request.getPagedResultsCookie() != null |
| | | ? ByteString.valueOfBase64(request.getPagedResultsCookie()) : ByteString.empty(); |
| | | final SimplePagedResultsControl control = |
| | | SimplePagedResultsControl.newControl(true, pageResultEndIndex, cookie); |
| | | searchRequest.addControl(control); |
| | |
| | | pageResultStartIndex = 0; |
| | | } |
| | | |
| | | c.getConnection().searchAsync(searchRequest, new SearchResultHandler() { |
| | | connection.searchAsync(searchRequest, new SearchResultHandler() { |
| | | @Override |
| | | public boolean handleEntry(final SearchResultEntry entry) { |
| | | /* |
| | | * Search result entries will be returned before the search result/error so the |
| | | * only reason pendingResult will be non-null is if a mapping error has occurred. |
| | | */ |
| | | // Search result entries will be returned before the search result/error so the only reason |
| | | // pendingResult will be non-null is if a mapping error has occurred. |
| | | synchronized (sequenceLock) { |
| | | if (pendingResult != null) { |
| | | return false; |
| | |
| | | } |
| | | |
| | | /* |
| | | * FIXME: secondary asynchronous searches will complete in a non-deterministic |
| | | * order and may cause the JSON resources to be returned in a different order to the |
| | | * order in which the primary LDAP search results were received. This is benign at |
| | | * the moment, but will need resolving when we implement server side sorting. A |
| | | * possible fix will be to use a queue of pending resources (promises?). However, |
| | | * the queue cannot be unbounded in case it grows very large, but it cannot be |
| | | * bounded either since that could cause a deadlock between rest2ldap and the LDAP |
| | | * server (imagine the case where the server has a single worker thread which is |
| | | * occupied processing the primary search). |
| | | * The best solution is probably to process the primary search results in batches |
| | | * using the paged results control. |
| | | * FIXME: secondary asynchronous searches will complete in a non-deterministic order and |
| | | * may cause the JSON resources to be returned in a different order to the order in which |
| | | * the primary LDAP search results were received. This is benign at the moment, but will |
| | | * need resolving when we implement server side sorting. A possible fix will be to use a |
| | | * queue of pending resources (promises?). However, the queue cannot be unbounded in case |
| | | * it grows very large, but it cannot be bounded either since that could cause a deadlock |
| | | * between rest2ldap and the LDAP server (imagine the case where the server has a single |
| | | * worker thread which is occupied processing the primary search). |
| | | * The best solution is probably to process the primary search results in batches using |
| | | * the paged results control. |
| | | */ |
| | | final String id = nameStrategy.getResourceId(c, entry); |
| | | final String id = nameStrategy.getResourceId(requestState, entry); |
| | | final String revision = getRevisionFromEntry(entry); |
| | | attributeMapper.read(c, new JsonPointer(), entry, new ResultHandler<JsonValue>() { |
| | | @Override |
| | | public void handleError(final ResourceException e) { |
| | | synchronized (sequenceLock) { |
| | | pendingResourceCount--; |
| | | completeIfNecessary(e); |
| | | } |
| | | } |
| | | |
| | | attributeMapper.read(requestState, new JsonPointer(), entry) |
| | | .thenOnResult(new ResultHandler<JsonValue>() { |
| | | @Override |
| | | public void handleResult(final JsonValue result) { |
| | | synchronized (sequenceLock) { |
| | | pendingResourceCount--; |
| | | if (!resultSent) { |
| | | h.handleResource(new Resource(id, revision, result)); |
| | | resourceHandler.handleResource( |
| | | Responses.newResourceResponse(id, revision, result)); |
| | | } |
| | | completeIfNecessary(); |
| | | completeIfNecessary(promise); |
| | | } |
| | | } |
| | | }).thenOnException(new ExceptionHandler<ResourceException>() { |
| | | @Override |
| | | public void handleException(ResourceException exception) { |
| | | synchronized (sequenceLock) { |
| | | pendingResourceCount--; |
| | | completeIfNecessary(exception, promise); |
| | | } |
| | | } |
| | | }); |
| | |
| | | |
| | | @Override |
| | | public boolean handleReference(final SearchResultReference reference) { |
| | | // TODO: should this be classed as an error since rest2ldap |
| | | // assumes entries are all colocated? |
| | | // TODO: should this be classed as an error since |
| | | // rest2ldap assumes entries are all colocated? |
| | | return true; |
| | | } |
| | | |
| | | }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() { |
| | | }).thenOnResult(new ResultHandler<Result>() { |
| | | @Override |
| | | public void handleResult(Result result) { |
| | | synchronized (sequenceLock) { |
| | | if (request.getPageSize() > 0) { |
| | | try { |
| | | final SimplePagedResultsControl control = |
| | | result.getControl(SimplePagedResultsControl.DECODER, |
| | | DECODE_OPTIONS); |
| | | result.getControl(SimplePagedResultsControl.DECODER, DECODE_OPTIONS); |
| | | if (control != null && !control.getCookie().isEmpty()) { |
| | | cookie = control.getCookie().toBase64String(); |
| | | } |
| | |
| | | // FIXME: need some logging. |
| | | } |
| | | } |
| | | completeIfNecessary(SUCCESS); |
| | | completeIfNecessary(SUCCESS, promise); |
| | | } |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>() { |
| | | @Override |
| | | public void handleException(LdapException exception) { |
| | | synchronized (sequenceLock) { |
| | | completeIfNecessary(asResourceException(exception)); |
| | | completeIfNecessary(asResourceException(exception), promise); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | return promise; |
| | | } |
| | | |
| | | /** |
| | | * This method must be invoked with the sequenceLock held. |
| | | */ |
| | | private void completeIfNecessary(final ResourceException e) { |
| | | /** This method must be invoked with the sequenceLock held. */ |
| | | private void completeIfNecessary( |
| | | final ResourceException e, final PromiseImpl<QueryResponse, ResourceException> handler) { |
| | | if (pendingResult == null) { |
| | | pendingResult = e; |
| | | } |
| | | completeIfNecessary(); |
| | | completeIfNecessary(handler); |
| | | } |
| | | |
| | | /** |
| | | * Close out the query result set if there are no more |
| | | * pending resources and the LDAP result has been received. |
| | | * Close out the query result set if there are no more pending |
| | | * resources and the LDAP result has been received. |
| | | * This method must be invoked with the sequenceLock held. |
| | | */ |
| | | private void completeIfNecessary() { |
| | | private void completeIfNecessary(final PromiseImpl<QueryResponse, ResourceException> handler) { |
| | | if (pendingResourceCount == 0 && pendingResult != null && !resultSent) { |
| | | if (pendingResult == SUCCESS) { |
| | | h.handleResult(new QueryResult(cookie, -1)); |
| | | handler.handleResult(Responses.newQueryResponse(cookie)); |
| | | } else { |
| | | h.handleError(pendingResult); |
| | | handler.handleException(pendingResult); |
| | | } |
| | | resultSent = true; |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public void readInstance(final ServerContext context, final String resourceId, final ReadRequest request, |
| | | final ResultHandler<Resource> handler) { |
| | | final Context c = wrap(context); |
| | | final ResultHandler<Resource> h = wrap(c, handler); |
| | | |
| | | // Get connection then perform the search. |
| | | c.run(h, new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | // Do the search. |
| | | final String[] attributes = getLDAPAttributes(c, request.getFields()); |
| | | final SearchRequest request = |
| | | nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(attributes); |
| | | |
| | | c.getConnection().searchSingleEntryAsync(request).thenOnResult( |
| | | new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() { |
| | | @Override |
| | | public void handleResult(final SearchResultEntry entry) { |
| | | adaptEntry(c, entry, h); |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>() { |
| | | @Override |
| | | public void handleException(final LdapException exception) { |
| | | h.handleError(asResourceException(exception)); |
| | | } |
| | | }); |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public void updateInstance(final ServerContext context, final String resourceId, final UpdateRequest request, |
| | | final ResultHandler<Resource> handler) { |
| | | /* |
| | | * Update operations are a bit awkward because there is no direct |
| | | * mapping to LDAP. We need to convert the update request into an LDAP |
| | | * modify operation which means reading the current LDAP entry, |
| | | * generating the new entry content, then comparing the two in order to |
| | | * obtain a set of changes. We also need to handle read-only fields |
| | | * correctly: if a read-only field is included with the new resource |
| | | * then it must match exactly the value of the existing field. |
| | | */ |
| | | final Context c = wrap(context); |
| | | final ResultHandler<Resource> h = wrap(c, handler); |
| | | public Promise<ResourceResponse, ResourceException> readInstance( |
| | | final Context context, final String resourceId, final ReadRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | |
| | | // Get connection then, search for the existing entry, then modify. |
| | | c.run(h, new Runnable() { |
| | | return requestState.getConnection() |
| | | .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void run() { |
| | | final String[] attributes = getLDAPAttributes(c, Collections.<JsonPointer> emptyList()); |
| | | final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId) |
| | | public Promise<ResourceResponse, ResourceException> apply(Connection connection) |
| | | throws ResourceException { |
| | | // Do the search. |
| | | final String[] attributes = getLDAPAttributes(requestState, request.getFields()); |
| | | final SearchRequest request = |
| | | nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId) |
| | | .addAttribute(attributes); |
| | | |
| | | c.getConnection().searchSingleEntryAsync(searchRequest) |
| | | .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() { |
| | | return connection.searchSingleEntryAsync(request) |
| | | .thenAsync( |
| | | new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | SearchResultEntry entry) throws ResourceException { |
| | | return adaptEntry(requestState, entry); |
| | | } |
| | | }, |
| | | ldapExceptionToResourceException()); |
| | | } |
| | | }) |
| | | .thenFinally(close(requestState)); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final SearchResultEntry entry) { |
| | | public Promise<ResourceResponse, ResourceException> updateInstance( |
| | | final Context context, final String resourceId, final UpdateRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | final AtomicReference<Connection> connectionHolder = new AtomicReference<>(); |
| | | |
| | | return requestState.getConnection().thenOnResult(saveConnection(connectionHolder)) |
| | | .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | final String[] attributes = getLDAPAttributes( |
| | | requestState, Collections.<JsonPointer> emptyList()); |
| | | final SearchRequest searchRequest = nameStrategy.createSearchRequest( |
| | | requestState, getBaseDN(requestState), resourceId).addAttribute(attributes); |
| | | |
| | | return connection.searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | final SearchResultEntry entry) { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | |
| | | // Create the modify request. |
| | | final ModifyRequest modifyRequest = newModifyRequest(entry.getName()); |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | final String[] attributes = getLDAPAttributes(c, request.getFields()); |
| | | modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes)); |
| | | final String[] attributes = |
| | | getLDAPAttributes(requestState, request.getFields()); |
| | | modifyRequest.addControl( |
| | | PostReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (config.usePermissiveModify()) { |
| | | modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true)); |
| | | modifyRequest.addControl( |
| | | PermissiveModifyRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | |
| | | /* |
| | | * Determine the set of changes that need to be performed. |
| | | */ |
| | | attributeMapper.update(c, new JsonPointer(), entry, request.getNewContent(), |
| | | new ResultHandler<List<Modification>>() { |
| | | // Determine the set of changes that need to be performed. |
| | | return attributeMapper.update( |
| | | requestState, new JsonPointer(), entry, request.getContent()) |
| | | .thenAsync(new AsyncFunction< |
| | | List<Modification>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | List<Modification> modifications) |
| | | throws ResourceException { |
| | | if (modifications.isEmpty()) { |
| | | // No changes to be performed so just return |
| | | // the entry that we read. |
| | | return adaptEntry(requestState, entry); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final List<Modification> result) { |
| | | // Perform the modify operation. |
| | | if (result.isEmpty()) { |
| | | /* |
| | | * No changes to be performed, so just return |
| | | * the entry that we read. |
| | | */ |
| | | adaptEntry(c, entry, h); |
| | | } else { |
| | | modifyRequest.getModifications().addAll(result); |
| | | c.getConnection().applyChangeAsync(modifyRequest) |
| | | .thenOnResult(postUpdateResultHandler(c, h)) |
| | | .thenOnException(postUpdateExceptionHandler(h)); |
| | | } |
| | | modifyRequest.getModifications().addAll(modifications); |
| | | return connection.applyChangeAsync(modifyRequest).thenAsync( |
| | | postUpdateResultAsyncFunction(requestState), |
| | | ldapExceptionToResourceException()); |
| | | } |
| | | }); |
| | | } catch (final Exception e) { |
| | | h.handleError(asResourceException(e)); |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>() { |
| | | @Override |
| | | public void handleException(final LdapException exception) { |
| | | h.handleError(asResourceException(exception)); |
| | | }, ldapExceptionToResourceException()); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | }).thenFinally(close(requestState)); |
| | | } |
| | | |
| | | private void adaptEntry(final Context c, final Entry entry, final ResultHandler<Resource> handler) { |
| | | final String actualResourceId = nameStrategy.getResourceId(c, entry); |
| | | private Promise<ResourceResponse, ResourceException> adaptEntry( |
| | | final RequestState requestState, final Entry entry) { |
| | | final String actualResourceId = nameStrategy.getResourceId(requestState, entry); |
| | | final String revision = getRevisionFromEntry(entry); |
| | | attributeMapper.read(c, new JsonPointer(), entry, transform( |
| | | new Function<JsonValue, Resource, NeverThrowsException>() { |
| | | return attributeMapper.read(requestState, new JsonPointer(), entry) |
| | | .then(new Function<JsonValue, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Resource apply(final JsonValue value) { |
| | | return new Resource(actualResourceId, revision, new JsonValue(value)); |
| | | public ResourceResponse apply(final JsonValue value) { |
| | | return Responses.newResourceResponse( |
| | | actualResourceId, revision, new JsonValue(value)); |
| | | } |
| | | }, handler)); |
| | | }); |
| | | } |
| | | |
| | | private void addAssertionControl(final ChangeRecord request, final String expectedRevision) |
| | |
| | | } |
| | | } |
| | | |
| | | private Runnable doUpdate(final Context c, final String resourceId, final String revision, |
| | | final ResultHandler<DN> updateHandler) { |
| | | return new Runnable() { |
| | | private AsyncFunction<Connection, DN, ResourceException> doUpdateFunction( |
| | | final RequestState requestState, final String resourceId, final String revision) { |
| | | return new AsyncFunction<Connection, DN, ResourceException>() { |
| | | @Override |
| | | public void run() { |
| | | public Promise<DN, ResourceException> apply(Connection connection) { |
| | | final String ldapAttribute = |
| | | (etagAttribute != null && revision != null) ? etagAttribute.toString() |
| | | : "1.1"; |
| | | (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1"; |
| | | final SearchRequest searchRequest = |
| | | nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute( |
| | | ldapAttribute); |
| | | nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId) |
| | | .addAttribute(ldapAttribute); |
| | | if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) { |
| | | // There's no point in doing a search because we already know the DN. |
| | | updateHandler.handleResult(searchRequest.getName()); |
| | | return Promises.newResultPromise(searchRequest.getName()); |
| | | } else { |
| | | c.getConnection().searchSingleEntryAsync(searchRequest) |
| | | .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() { |
| | | return connection.searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() { |
| | | @Override |
| | | public void handleResult(final SearchResultEntry entry) { |
| | | public Promise<DN, ResourceException> apply(SearchResultEntry entry) |
| | | throws ResourceException { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, revision); |
| | | |
| | | // Perform update operation. |
| | | updateHandler.handleResult(entry.getName()); |
| | | return Promises.newResultPromise(entry.getName()); |
| | | } catch (final Exception e) { |
| | | updateHandler.handleError(asResourceException(e)); |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>() { |
| | | }, new AsyncFunction<LdapException, DN, ResourceException>() { |
| | | @Override |
| | | public void handleException(final LdapException exception) { |
| | | updateHandler.handleError(asResourceException(exception)); |
| | | public Promise<DN, ResourceException> apply(LdapException ldapException) |
| | | throws ResourceException { |
| | | return Promises.newExceptionPromise(asResourceException(ldapException)); |
| | | } |
| | | }); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | private DN getBaseDN(final Context context) { |
| | | private DN getBaseDN(final RequestState requestState) { |
| | | return baseDN; |
| | | } |
| | | |
| | |
| | | * Determines the set of LDAP attributes to request in an LDAP read (search, |
| | | * post-read), based on the provided list of JSON pointers. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param requestedAttributes |
| | | * The list of resource attributes to be read. |
| | | * @return The set of LDAP attributes associated with the resource |
| | | * attributes. |
| | | */ |
| | | private String[] getLDAPAttributes(final Context c, |
| | | final Collection<JsonPointer> requestedAttributes) { |
| | | private String[] getLDAPAttributes( |
| | | final RequestState requestState, final Collection<JsonPointer> requestedAttributes) { |
| | | // Get all the LDAP attributes required by the attribute mappers. |
| | | final Set<String> requestedLDAPAttributes; |
| | | if (requestedAttributes.isEmpty()) { |
| | | // Full read. |
| | | requestedLDAPAttributes = new LinkedHashSet<>(); |
| | | attributeMapper.getLDAPAttributes(c, new JsonPointer(), new JsonPointer(), |
| | | attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), new JsonPointer(), |
| | | requestedLDAPAttributes); |
| | | } else { |
| | | // Partial read. |
| | | requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size()); |
| | | for (final JsonPointer requestedAttribute : requestedAttributes) { |
| | | attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedAttribute, |
| | | attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), requestedAttribute, |
| | | requestedLDAPAttributes); |
| | | } |
| | | } |
| | | |
| | | // Get the LDAP attributes required by the Etag and name stategies. |
| | | nameStrategy.getLDAPAttributes(c, requestedLDAPAttributes); |
| | | nameStrategy.getLDAPAttributes(requestState, requestedLDAPAttributes); |
| | | if (etagAttribute != null) { |
| | | requestedLDAPAttributes.add(etagAttribute.toString()); |
| | | } |
| | | return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]); |
| | | } |
| | | |
| | | private void getLDAPFilter(final Context c, final QueryFilter queryFilter, final ResultHandler<Filter> h) { |
| | | final QueryFilterVisitor<Void, ResultHandler<Filter>> visitor = |
| | | new QueryFilterVisitor<Void, ResultHandler<Filter>>() { |
| | | @Override |
| | | public Void visitAndFilter(final ResultHandler<Filter> p, final List<QueryFilter> subFilters) { |
| | | List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size()); |
| | | for (final QueryFilter subFilter : subFilters) { |
| | | final ResultHandlerFromPromise<Filter> handler = new ResultHandlerFromPromise<>(); |
| | | subFilter.accept(this, handler); |
| | | promises.add(handler.promise); |
| | | } |
| | | |
| | | Promises.when(promises) |
| | | .then(new org.forgerock.util.Function<List<Filter>, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(final List<Filter> value) { |
| | | // Check for unmapped filter components and optimize. |
| | | final Iterator<Filter> i = value.iterator(); |
| | | while (i.hasNext()) { |
| | | final Filter f = i.next(); |
| | | if (f == alwaysFalse()) { |
| | | return alwaysFalse(); |
| | | } else if (f == alwaysTrue()) { |
| | | i.remove(); |
| | | } |
| | | } |
| | | switch (value.size()) { |
| | | case 0: |
| | | return alwaysTrue(); |
| | | case 1: |
| | | return value.get(0); |
| | | default: |
| | | return Filter.and(value); |
| | | } |
| | | } |
| | | }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Filter>() { |
| | | @Override |
| | | public void handleResult(Filter result) { |
| | | p.handleResult(result); |
| | | } |
| | | }).thenOnException(new ExceptionHandler<ResourceException>() { |
| | | @Override |
| | | public void handleException(ResourceException exception) { |
| | | p.handleError(exception); |
| | | } |
| | | }); |
| | | |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitBooleanLiteralFilter(final ResultHandler<Filter> p, |
| | | final boolean value) { |
| | | p.handleResult(toFilter(value)); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitContainsFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.CONTAINS, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitEqualsFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.EQUAL_TO, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitExtendedMatchFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final String operator, |
| | | final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.EXTENDED, operator, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitGreaterThanFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.GREATER_THAN, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitGreaterThanOrEqualToFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitLessThanFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.LESS_THAN, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitLessThanOrEqualToFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitNotFilter(final ResultHandler<Filter> p, |
| | | final QueryFilter subFilter) { |
| | | subFilter.accept(this, transform(new Function<Filter, Filter, NeverThrowsException>() { |
| | | @Override |
| | | public Filter apply(final Filter value) { |
| | | if (value == null || value == alwaysFalse()) { |
| | | return alwaysTrue(); |
| | | } else if (value == alwaysTrue()) { |
| | | return alwaysFalse(); |
| | | } else { |
| | | return Filter.not(value); |
| | | } |
| | | } |
| | | }, p)); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitOrFilter(final ResultHandler<Filter> p, final List<QueryFilter> subFilters) { |
| | | List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size()); |
| | | for (final QueryFilter subFilter : subFilters) { |
| | | final ResultHandlerFromPromise<Filter> handler = new ResultHandlerFromPromise<>(); |
| | | subFilter.accept(this, handler); |
| | | promises.add(handler.promise); |
| | | } |
| | | |
| | | Promises.when(promises) |
| | | .then(new org.forgerock.util.Function<List<Filter>, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(final List<Filter> value) { |
| | | // Check for unmapped filter components and optimize. |
| | | final Iterator<Filter> i = value.iterator(); |
| | | while (i.hasNext()) { |
| | | final Filter f = i.next(); |
| | | if (f == alwaysFalse()) { |
| | | i.remove(); |
| | | } else if (f == alwaysTrue()) { |
| | | return alwaysTrue(); |
| | | } |
| | | } |
| | | switch (value.size()) { |
| | | case 0: |
| | | return alwaysFalse(); |
| | | case 1: |
| | | return value.get(0); |
| | | default: |
| | | return Filter.or(value); |
| | | } |
| | | } |
| | | }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Filter>() { |
| | | @Override |
| | | public void handleResult(Filter result) { |
| | | p.handleResult(result); |
| | | } |
| | | }).thenOnException(new ExceptionHandler<ResourceException>() { |
| | | @Override |
| | | public void handleException(ResourceException exception) { |
| | | p.handleError(exception); |
| | | } |
| | | }); |
| | | |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitPresentFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.PRESENT, null, null, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitStartsWithFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.STARTS_WITH, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | }; |
| | | /* |
| | | * Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers. |
| | | */ |
| | | queryFilter.accept(visitor, h); |
| | | } |
| | | |
| | | private String getRevisionFromEntry(final Entry entry) { |
| | | return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null; |
| | | } |
| | | |
| | | private org.forgerock.util.promise.ResultHandler<SearchResultEntry> postEmptyPatchResultHandler( |
| | | final Context c, final PatchRequest request, final ResultHandler<Resource> h) { |
| | | return new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() { |
| | | @Override |
| | | public void handleResult(final SearchResultEntry entry) { |
| | | try { |
| | | // Fail if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | | adaptEntry(c, entry, h); |
| | | } catch (final Exception e) { |
| | | h.handleError(asResourceException(e)); |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private ExceptionHandler<LdapException> postEmptyPatchExceptionHandler(final ResultHandler<Resource> h) { |
| | | return new ExceptionHandler<LdapException>() { |
| | | @Override |
| | | public void handleException(final LdapException exception) { |
| | | h.handleError(asResourceException(exception)); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private org.forgerock.util.promise.ResultHandler<Result> postUpdateResultHandler( |
| | | final Context c, final ResultHandler<Resource> handler) { |
| | | private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction( |
| | | final RequestState requestState) { |
| | | // The handler which will be invoked for the LDAP add result. |
| | | return new org.forgerock.util.promise.ResultHandler<Result>() { |
| | | return new AsyncFunction<Result, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void handleResult(final Result result) { |
| | | public Promise<ResourceResponse, ResourceException> apply(Result result) throws ResourceException { |
| | | // FIXME: handle USE_SEARCH policy. |
| | | Entry entry; |
| | | try { |
| | |
| | | entry = null; |
| | | } |
| | | if (entry != null) { |
| | | adaptEntry(c, entry, handler); |
| | | return adaptEntry(requestState, entry); |
| | | } else { |
| | | final Resource resource = new Resource(null, null, new JsonValue(Collections.emptyMap())); |
| | | handler.handleResult(resource); |
| | | return Promises.newResultPromise( |
| | | Responses.newResourceResponse(null, null, new JsonValue(Collections.emptyMap()))); |
| | | } |
| | | } |
| | | |
| | | }; |
| | | } |
| | | |
| | | private ExceptionHandler<LdapException> postUpdateExceptionHandler(final ResultHandler<Resource> handler) { |
| | | private AsyncFunction<LdapException, ResourceResponse, ResourceException> ldapExceptionToResourceException() { |
| | | // The handler which will be invoked for the LDAP add result. |
| | | return new ExceptionHandler<LdapException>() { |
| | | return new AsyncFunction<LdapException, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public void handleException(final LdapException exception) { |
| | | handler.handleError(asResourceException(exception)); |
| | | public Promise<ResourceResponse, ResourceException> apply(final LdapException ldapException) |
| | | throws ResourceException { |
| | | return Promises.newExceptionPromise(asResourceException(ldapException)); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private QueryResultHandler wrap(final Context c, final QueryResultHandler handler) { |
| | | return new QueryResultHandler() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | try { |
| | | handler.handleError(error); |
| | | } finally { |
| | | c.close(); |
| | | } |
| | | private RequestState wrap(final Context context) { |
| | | return new RequestState(config, context); |
| | | } |
| | | |
| | | private Runnable close(final RequestState requestState) { |
| | | return new Runnable() { |
| | | @Override |
| | | public boolean handleResource(final Resource resource) { |
| | | return handler.handleResource(resource); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final QueryResult result) { |
| | | try { |
| | | handler.handleResult(result); |
| | | } finally { |
| | | c.close(); |
| | | } |
| | | public void run() { |
| | | requestState.close(); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private <V> ResultHandler<V> wrap(final Context c, final ResultHandler<V> handler) { |
| | | return new ResultHandler<V>() { |
| | | private ResultHandler<Connection> saveConnection(final AtomicReference<Connection> connectionHolder) { |
| | | return new ResultHandler<Connection>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | try { |
| | | handler.handleError(error); |
| | | } finally { |
| | | c.close(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final V result) { |
| | | try { |
| | | handler.handleResult(result); |
| | | } finally { |
| | | c.close(); |
| | | } |
| | | public void handleResult(Connection connection) { |
| | | connectionHolder.set(connection); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private Context wrap(final ServerContext context) { |
| | | return new Context(config, context); |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013 ForgeRock AS. |
| | | * Copyright 2013-2015 ForgeRock AS. |
| | | */ |
| | | |
| | | package org.forgerock.opendj.rest2ldap; |
| | |
| | | * Returns a search request which can be used to obtain the specified REST |
| | | * resource. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param baseDN |
| | | * The search base DN. |
| | | * @param resourceId |
| | |
| | | * @return A search request which can be used to obtain the specified REST |
| | | * resource. |
| | | */ |
| | | abstract SearchRequest createSearchRequest(Context c, DN baseDN, String resourceId); |
| | | abstract SearchRequest createSearchRequest(RequestState requestState, DN baseDN, String resourceId); |
| | | |
| | | /** |
| | | * Adds the name of any LDAP attribute required by this name strategy to the |
| | | * provided set. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param ldapAttributes |
| | | * The set into which any required LDAP attribute name should be |
| | | * put. |
| | | */ |
| | | abstract void getLDAPAttributes(Context c, Set<String> ldapAttributes); |
| | | abstract void getLDAPAttributes(RequestState requestState, Set<String> ldapAttributes); |
| | | |
| | | /** |
| | | * Retrieves the resource ID from the provided LDAP entry. Implementations |
| | | * may use the entry DN as well as any attributes in order to determine the |
| | | * resource ID. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param entry |
| | | * The LDAP entry from which the resource ID should be obtained. |
| | | * @return The resource ID. |
| | | */ |
| | | abstract String getResourceId(Context c, Entry entry); |
| | | abstract String getResourceId(RequestState requestState, Entry entry); |
| | | |
| | | /** |
| | | * Sets the resource ID in the provided LDAP entry. Implementations are |
| | | * responsible for setting the entry DN as well as any attributes associated |
| | | * with the resource ID. |
| | | * |
| | | * @param c |
| | | * The context. |
| | | * @param requestState |
| | | * The request state. |
| | | * @param baseDN |
| | | * The baseDN to use when constructing the entry's DN. |
| | | * @param resourceId |
| | |
| | | * @throws ResourceException |
| | | * If the resource ID cannot be determined. |
| | | */ |
| | | abstract void setResourceId(Context c, DN baseDN, String resourceId, Entry entry) |
| | | abstract void setResourceId(RequestState requestState, DN baseDN, String resourceId, Entry entry) |
| | | throws ResourceException; |
| | | |
| | | } |
| | |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.json.resource.PatchOperation.operation; |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysFalse; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase; |
| | | |
| | | import java.util.AbstractMap.SimpleImmutableEntry; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | |
| | | import static org.forgerock.json.resource.PatchOperation.*; |
| | | import static org.forgerock.opendj.ldap.Filter.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.*; |
| | | |
| | | /** |
| | | * An attribute mapper which maps JSON objects to LDAP attributes. |
| | | */ |
| | | /** An attribute mapper which maps JSON objects to LDAP attributes. */ |
| | | public final class ObjectAttributeMapper extends AttributeMapper { |
| | | |
| | | private static final class Mapping { |
| | |
| | | } |
| | | |
| | | @Override |
| | | void create(final Context c, final JsonPointer path, final JsonValue v, |
| | | final ResultHandler<List<Attribute>> h) { |
| | | Promise<List<Attribute>, ResourceException> create( |
| | | final RequestState requestState, final JsonPointer path, final JsonValue v) { |
| | | try { |
| | | /* |
| | | * First check that the JSON value is an object and that the fields |
| | |
| | | final Map<String, Mapping> missingMappings = checkMapping(path, v); |
| | | |
| | | // Accumulate the results of the subordinate mappings. |
| | | final ResultHandler<List<Attribute>> handler = accumulator(h); |
| | | final List<Promise<List<Attribute>, ResourceException>> promises = new ArrayList<>(); |
| | | |
| | | // Invoke mappings for which there are values provided. |
| | | if (v != null && !v.isNull()) { |
| | | for (final Map.Entry<String, Object> me : v.asMap().entrySet()) { |
| | | final Mapping mapping = getMapping(me.getKey()); |
| | | final JsonValue subValue = new JsonValue(me.getValue()); |
| | | mapping.mapper.create(c, path.child(me.getKey()), subValue, handler); |
| | | promises.add(mapping.mapper.create(requestState, path.child(me.getKey()), subValue)); |
| | | } |
| | | } |
| | | |
| | | // Invoke mappings for which there were no values provided. |
| | | for (final Mapping mapping : missingMappings.values()) { |
| | | mapping.mapper.create(c, path.child(mapping.name), null, handler); |
| | | promises.add(mapping.mapper.create(requestState, path.child(mapping.name), null)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | | .then(this.<Attribute> accumulateResults()); |
| | | } catch (final Exception e) { |
| | | h.handleError(asResourceException(e)); |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | if (subPath.isEmpty()) { |
| | | // Request all subordinate mappings. |
| | | for (final Mapping mapping : mappings.values()) { |
| | | mapping.mapper.getLDAPAttributes(c, path.child(mapping.name), subPath, |
| | | ldapAttributes); |
| | | mapping.mapper.getLDAPAttributes(requestState, path.child(mapping.name), subPath, ldapAttributes); |
| | | } |
| | | } else { |
| | | // Request single subordinate mapping. |
| | | final Mapping mapping = getMapping(subPath); |
| | | if (mapping != null) { |
| | | mapping.mapper.getLDAPAttributes(c, path.child(subPath.get(0)), subPath |
| | | .relativePointer(), ldapAttributes); |
| | | mapping.mapper.getLDAPAttributes( |
| | | requestState, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final FilterType type, final String operator, final Object valueAssertion, |
| | | final ResultHandler<Filter> h) { |
| | | Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) { |
| | | final Mapping mapping = getMapping(subPath); |
| | | if (mapping != null) { |
| | | mapping.mapper.getLDAPFilter(c, path.child(subPath.get(0)), subPath.relativePointer(), |
| | | type, operator, valueAssertion, h); |
| | | return mapping.mapper.getLDAPFilter(requestState, path.child(subPath.get(0)), |
| | | subPath.relativePointer(), type, operator, valueAssertion); |
| | | } else { |
| | | /* |
| | | * Either the filter targeted the entire object (i.e. it was "/"), |
| | | * or it targeted an unrecognized attribute within the object. |
| | | * Either way, the filter will never match. |
| | | */ |
| | | h.handleResult(alwaysFalse()); |
| | | return Promises.newResultPromise(alwaysFalse()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void patch(final Context c, final JsonPointer path, final PatchOperation operation, |
| | | final ResultHandler<List<Modification>> h) { |
| | | Promise<List<Modification>, ResourceException> patch( |
| | | final RequestState requestState, final JsonPointer path, final PatchOperation operation) { |
| | | try { |
| | | final JsonPointer field = operation.getField(); |
| | | final JsonValue v = operation.getValue(); |
| | |
| | | * by allowing the JSON value to be a partial object and |
| | | * add/remove/replace only the provided values. |
| | | */ |
| | | final Map<String, Mapping> missingMappings = checkMapping(path, v); |
| | | checkMapping(path, v); |
| | | |
| | | // Accumulate the results of the subordinate mappings. |
| | | final ResultHandler<List<Modification>> handler = |
| | | accumulator(mappings.size() - missingMappings.size(), h); |
| | | final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>(); |
| | | |
| | | // Invoke mappings for which there are values provided. |
| | | if (!v.isNull()) { |
| | |
| | | final JsonValue subValue = new JsonValue(me.getValue()); |
| | | final PatchOperation subOperation = |
| | | operation(operation.getOperation(), field /* empty */, subValue); |
| | | mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler); |
| | | promises.add(mapping.mapper.patch(requestState, path.child(me.getKey()), subOperation)); |
| | | } |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | | .then(this.<Modification> accumulateResults()); |
| | | } else { |
| | | /* |
| | | * The patch operation targets a subordinate field. Create a new |
| | |
| | | } |
| | | final PatchOperation subOperation = |
| | | operation(operation.getOperation(), field.relativePointer(), v); |
| | | mapping.mapper.patch(c, path.child(fieldName), subOperation, h); |
| | | return mapping.mapper.patch(requestState, path.child(fieldName), subOperation); |
| | | } |
| | | } catch (final Exception ex) { |
| | | h.handleError(asResourceException(ex)); |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void read(final Context c, final JsonPointer path, final Entry e, |
| | | final ResultHandler<JsonValue> h) { |
| | | Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) { |
| | | /* |
| | | * Use an accumulator which will aggregate the results from the |
| | | * subordinate mappers into a single list. On completion, the |
| | | * accumulator combines the results into a single JSON map object. |
| | | */ |
| | | final ResultHandler<Map.Entry<String, JsonValue>> handler = |
| | | accumulate(mappings.size(), transform( |
| | | new Function<List<Map.Entry<String, JsonValue>>, JsonValue, NeverThrowsException>() { |
| | | final List<Promise<Map.Entry<String, JsonValue>, ResourceException>> promises = |
| | | new ArrayList<>(mappings.size()); |
| | | |
| | | for (final Mapping mapping : mappings.values()) { |
| | | promises.add(mapping.mapper.read(requestState, path.child(mapping.name), e) |
| | | .then(new Function<JsonValue, Map.Entry<String, JsonValue>, ResourceException>() { |
| | | @Override |
| | | public Map.Entry<String, JsonValue> apply(final JsonValue value) { |
| | | return value != null ? new SimpleImmutableEntry<String, JsonValue>(mapping.name, value) |
| | | : null; |
| | | } |
| | | })); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | | .then(new Function<List<Map.Entry<String, JsonValue>>, JsonValue, ResourceException>() { |
| | | @Override |
| | | public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) { |
| | | if (value.isEmpty()) { |
| | | /* |
| | | * No subordinate attributes, so omit the |
| | | * entire JSON object from the resource. |
| | | * No subordinate attributes, so omit the entire |
| | | * JSON object from the resource. |
| | | */ |
| | | return null; |
| | | } else { |
| | | // Combine the sub-attributes into a single JSON object. |
| | | final Map<String, Object> result = new LinkedHashMap<>(value.size()); |
| | | for (final Map.Entry<String, JsonValue> e : value) { |
| | | if (e != null) { |
| | | result.put(e.getKey(), e.getValue().getObject()); |
| | | } |
| | | } |
| | | return new JsonValue(result); |
| | | } |
| | | } |
| | | }, h)); |
| | | |
| | | for (final Mapping mapping : mappings.values()) { |
| | | mapping.mapper.read(c, path.child(mapping.name), e, transform( |
| | | new Function<JsonValue, Map.Entry<String, JsonValue>, NeverThrowsException>() { |
| | | @Override |
| | | public Map.Entry<String, JsonValue> apply(final JsonValue value) { |
| | | return value != null ? new SimpleImmutableEntry<String, JsonValue>( |
| | | mapping.name, value) : null; |
| | | } |
| | | }, handler)); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v, |
| | | final ResultHandler<List<Modification>> h) { |
| | | Promise<List<Modification>, ResourceException> update( |
| | | final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | try { |
| | | /* |
| | | * First check that the JSON value is an object and that the fields |
| | | * it contains are known by this mapper. |
| | | */ |
| | | // First check that the JSON value is an object and that the fields |
| | | // it contains are known by this mapper. |
| | | final Map<String, Mapping> missingMappings = checkMapping(path, v); |
| | | |
| | | // Accumulate the results of the subordinate mappings. |
| | | final ResultHandler<List<Modification>> handler = accumulator(h); |
| | | final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>(); |
| | | |
| | | // Invoke mappings for which there are values provided. |
| | | if (v != null && !v.isNull()) { |
| | | for (final Map.Entry<String, Object> me : v.asMap().entrySet()) { |
| | | final Mapping mapping = getMapping(me.getKey()); |
| | | final JsonValue subValue = new JsonValue(me.getValue()); |
| | | mapping.mapper.update(c, path.child(me.getKey()), e, subValue, handler); |
| | | promises.add(mapping.mapper.update(requestState, path.child(me.getKey()), e, subValue)); |
| | | } |
| | | } |
| | | |
| | | // Invoke mappings for which there were no values provided. |
| | | for (final Mapping mapping : missingMappings.values()) { |
| | | mapping.mapper.update(c, path.child(mapping.name), e, null, handler); |
| | | promises.add(mapping.mapper.update(requestState, path.child(mapping.name), e, null)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | | .then(this.<Modification> accumulateResults()); |
| | | } catch (final Exception ex) { |
| | | h.handleError(asResourceException(ex)); |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | } |
| | | } |
| | | |
| | | private <T> ResultHandler<List<T>> accumulator(final ResultHandler<List<T>> h) { |
| | | return accumulator(mappings.size(), h); |
| | | } |
| | | |
| | | private <T> ResultHandler<List<T>> accumulator(final int size, final ResultHandler<List<T>> h) { |
| | | return accumulate(size, transform(new Function<List<List<T>>, List<T>, NeverThrowsException>() { |
| | | private <T> Function<List<List<T>>, List<T>, ResourceException> accumulateResults() { |
| | | return new Function<List<List<T>>, List<T>, ResourceException>() { |
| | | @Override |
| | | public List<T> apply(final List<List<T>> value) { |
| | | switch (value.size()) { |
| | |
| | | return attributes; |
| | | } |
| | | } |
| | | }, h)); |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Fail immediately if the JSON value has the wrong type or contains unknown |
| | | * attributes. |
| | | */ |
| | | /** Fail immediately if the JSON value has the wrong type or contains unknown attributes. */ |
| | | private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v) |
| | | throws ResourceException { |
| | | final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings); |
| | |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.ldap.LdapException.newLdapException; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.LinkedList; |
| | |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.EntryNotFoundException; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.LinkedAttribute; |
| | | import org.forgerock.opendj.ldap.MultipleEntriesFoundException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultReference; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.ExceptionHandler; |
| | | |
| | | import static org.forgerock.opendj.ldap.LdapException.*; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.*; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.PromiseImpl; |
| | | import org.forgerock.util.promise.Promises; |
| | | import org.forgerock.util.promise.ResultHandler; |
| | | |
| | | /** |
| | | * An attribute mapper which provides a mapping from a JSON value to a single DN |
| | |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, final FilterType type, |
| | | final String operator, final Object valueAssertion, final ResultHandler<Filter> h) { |
| | | // Construct a filter which can be used to find referenced resources. |
| | | mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion, new ResultHandler<Filter>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); // Propagate. |
| | | } |
| | | Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) { |
| | | |
| | | return mapper.getLDAPFilter(requestState, path, subPath, type, operator, valueAssertion) |
| | | .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() { |
| | | @Override |
| | | public void handleResult(final Filter result) { |
| | | public Promise<Filter, ResourceException> apply(final Filter result) { |
| | | // Search for all referenced entries and construct a filter. |
| | | final SearchRequest request = createSearchRequest(result); |
| | | final List<Filter> subFilters = new LinkedList<>(); |
| | | |
| | | final ExceptionHandler<LdapException> exceptionHandler = new ExceptionHandler<LdapException>() { |
| | | return requestState.getConnection().thenAsync( |
| | | new AsyncFunction<Connection, Filter, ResourceException>() { |
| | | @Override |
| | | public void handleException(LdapException exception) { |
| | | h.handleError(asResourceException(exception)); // Propagate. |
| | | } |
| | | }; |
| | | |
| | | c.getConnection().searchAsync(request, new SearchResultHandler() { |
| | | public Promise<Filter, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | return connection.searchAsync(request, new SearchResultHandler() { |
| | | @Override |
| | | public boolean handleEntry(final SearchResultEntry entry) { |
| | | if (subFilters.size() < SEARCH_MAX_CANDIDATES) { |
| | | subFilters.add(Filter.equality(ldapAttributeName.toString(), entry.getName())); |
| | | subFilters.add(Filter.equality( |
| | | ldapAttributeName.toString(), entry.getName())); |
| | | return true; |
| | | } else { |
| | | // No point in continuing - maximum candidates reached. |
| | |
| | | // Ignore references. |
| | | return true; |
| | | } |
| | | }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() { |
| | | }).then(new Function<Result, Filter, ResourceException>() { |
| | | @Override |
| | | public void handleResult(Result result) { |
| | | public Filter apply(Result result) throws ResourceException { |
| | | if (subFilters.size() >= SEARCH_MAX_CANDIDATES) { |
| | | exceptionHandler.handleException(newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED)); |
| | | throw asResourceException( |
| | | newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED)); |
| | | } else if (subFilters.size() == 1) { |
| | | h.handleResult(subFilters.get(0)); |
| | | return subFilters.get(0); |
| | | } else { |
| | | h.handleResult(Filter.or(subFilters)); |
| | | return Filter.or(subFilters); |
| | | } |
| | | } |
| | | }).thenOnException(exceptionHandler); |
| | | }, new Function<LdapException, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(LdapException exception) throws ResourceException { |
| | | throw asResourceException(exception); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | void getNewLDAPAttributes(final Context c, final JsonPointer path, final List<Object> newValues, |
| | | final ResultHandler<Attribute> h) { |
| | | Promise<Attribute, ResourceException> getNewLDAPAttributes( |
| | | final RequestState requestState, final JsonPointer path, final List<Object> newValues) { |
| | | /* |
| | | * For each value use the subordinate mapper to obtain the LDAP primary |
| | | * key, the perform a search for each one to find the corresponding entries. |
| | |
| | | final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName); |
| | | final AtomicInteger pendingSearches = new AtomicInteger(newValues.size()); |
| | | final AtomicReference<ResourceException> exception = new AtomicReference<>(); |
| | | final PromiseImpl<Attribute, ResourceException> promise = PromiseImpl.create(); |
| | | |
| | | for (final Object value : newValues) { |
| | | mapper.create(c, path, new JsonValue(value), new ResultHandler<List<Attribute>>() { |
| | | |
| | | mapper.create(requestState, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final List<Attribute> result) { |
| | | public void handleResult(List<Attribute> result) { |
| | | Attribute primaryKeyAttribute = null; |
| | | for (final Attribute attribute : result) { |
| | | if (attribute.getAttributeDescription().equals(primaryKey)) { |
| | |
| | | } |
| | | |
| | | if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because the reference " |
| | | + "field '%s' contains a value which does not contain " + "a primary key", path))); |
| | | return; |
| | | promise.handleException(new BadRequestException( |
| | | i18n("The request cannot be processed because the reference field '%s' contains " |
| | | + "a value which does not contain a primary key", path))); |
| | | } |
| | | |
| | | if (primaryKeyAttribute.size() > 1) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because the reference " |
| | | + "field '%s' contains a value which contains multiple " + "primary keys", path))); |
| | | return; |
| | | promise.handleException(new BadRequestException( |
| | | i18n("The request cannot be processed because the reference field '%s' contains " |
| | | + "a value which contains multiple primary keys", path))); |
| | | } |
| | | |
| | | // Now search for the referenced entry in to get its DN. |
| | | final ByteString primaryKeyValue = primaryKeyAttribute.firstValue(); |
| | | final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue); |
| | | final SearchRequest search = createSearchRequest(filter); |
| | | c.getConnection().searchSingleEntryAsync(search).thenOnResult( |
| | | new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() { |
| | | requestState.getConnection().thenOnResult(new ResultHandler<Connection>() { |
| | | @Override |
| | | public void handleResult(Connection connection) { |
| | | connection.searchSingleEntryAsync(search) |
| | | .thenOnResult(new ResultHandler<SearchResultEntry>() { |
| | | @Override |
| | | public void handleResult(final SearchResultEntry result) { |
| | | synchronized (newLDAPAttribute) { |
| | |
| | | throw error; |
| | | } catch (final EntryNotFoundException e) { |
| | | re = new BadRequestException(i18n( |
| | | "The request cannot be processed " + "because the resource '%s' " |
| | | + "referenced in field '%s' does " + "not exist", |
| | | "The request cannot be processed because the resource " |
| | | + "'%s' referenced in field '%s' does not exist", |
| | | primaryKeyValue.toString(), path)); |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | re = new BadRequestException(i18n( |
| | | "The request cannot be processed " + "because the resource '%s' " |
| | | + "referenced in field '%s' is " + "ambiguous", |
| | | "The request cannot be processed because the resource " |
| | | + "'%s' referenced in field '%s' is ambiguous", |
| | | primaryKeyValue.toString(), path)); |
| | | } catch (final LdapException e) { |
| | | re = asResourceException(e); |
| | |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private void completeIfNecessary() { |
| | | if (pendingSearches.decrementAndGet() == 0) { |
| | | if (exception.get() != null) { |
| | | h.handleError(exception.get()); |
| | | promise.handleException(exception.get()); |
| | | } else { |
| | | h.handleResult(newLDAPAttribute); |
| | | promise.handleResult(newLDAPAttribute); |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | return promise; |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Override |
| | | void read(final Context c, final JsonPointer path, final Entry e, final ResultHandler<JsonValue> h) { |
| | | Promise<JsonValue, ResourceException> read(final RequestState c, final JsonPointer path, final Entry e) { |
| | | final Attribute attribute = e.getAttribute(ldapAttributeName); |
| | | if (attribute == null || attribute.isEmpty()) { |
| | | h.handleResult(null); |
| | | return Promises.newResultPromise(null); |
| | | } else if (attributeIsSingleValued()) { |
| | | try { |
| | | final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN(); |
| | | readEntry(c, path, dn, h); |
| | | return readEntry(c, path, dn); |
| | | } catch (final Exception ex) { |
| | | // The LDAP attribute could not be decoded. |
| | | h.handleError(asResourceException(ex)); |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | } |
| | | } else { |
| | | try { |
| | | final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN(); |
| | | final ResultHandler<JsonValue> handler = |
| | | accumulate(dns.size(), transform(new Function<List<JsonValue>, JsonValue, NeverThrowsException>() { |
| | | |
| | | final List<Promise<JsonValue, ResourceException>> promises = new ArrayList<>(dns.size()); |
| | | for (final DN dn : dns) { |
| | | promises.add(readEntry(c, path, dn)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | | .then(new Function<List<JsonValue>, JsonValue, ResourceException>() { |
| | | @Override |
| | | public JsonValue apply(final List<JsonValue> value) { |
| | | if (value.isEmpty()) { |
| | | /* |
| | | * No values, so omit the entire JSON object |
| | | * from the resource. |
| | | */ |
| | | // No values, so omit the entire JSON object from the resource. |
| | | return null; |
| | | } else { |
| | | // Combine values into a single JSON array. |
| | |
| | | return new JsonValue(result); |
| | | } |
| | | } |
| | | }, h)); |
| | | for (final DN dn : dns) { |
| | | readEntry(c, path, dn, handler); |
| | | } |
| | | }); |
| | | } catch (final Exception ex) { |
| | | // The LDAP attribute could not be decoded. |
| | | h.handleError(asResourceException(ex)); |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | } |
| | | } |
| | | } |
| | |
| | | return newSearchRequest(baseDN, scope, searchFilter, "1.1"); |
| | | } |
| | | |
| | | private void readEntry(final Context c, final JsonPointer path, final DN dn, |
| | | final ResultHandler<JsonValue> handler) { |
| | | private Promise<JsonValue, ResourceException> readEntry( |
| | | final RequestState requestState, final JsonPointer path, final DN dn) { |
| | | final Set<String> requestedLDAPAttributes = new LinkedHashSet<>(); |
| | | mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes); |
| | | c.getConnection().readEntryAsync(dn, requestedLDAPAttributes) |
| | | .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() { |
| | | mapper.getLDAPAttributes(requestState, path, new JsonPointer(), requestedLDAPAttributes); |
| | | return requestState.getConnection().thenAsync(new AsyncFunction<Connection, JsonValue, ResourceException>() { |
| | | @Override |
| | | public void handleResult(final SearchResultEntry result) { |
| | | mapper.read(c, path, result, handler); |
| | | public Promise<JsonValue, ResourceException> apply(Connection connection) throws ResourceException { |
| | | return connection.readEntryAsync(dn, requestedLDAPAttributes) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() { |
| | | @Override |
| | | public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) { |
| | | return mapper.read(requestState, path, result); |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>() { |
| | | }, new AsyncFunction<LdapException, JsonValue, ResourceException>() { |
| | | @Override |
| | | public void handleException(final LdapException error) { |
| | | public Promise<JsonValue, ResourceException> apply(final LdapException error) { |
| | | if (!(error instanceof EntryNotFoundException)) { |
| | | handler.handleError(asResourceException(error)); |
| | | return Promises.newExceptionPromise(asResourceException(error)); |
| | | } else { |
| | | /* |
| | | * The referenced entry does not exist so ignore it |
| | | * since it cannot be mapped. |
| | | */ |
| | | handler.handleResult(null); |
| | | // The referenced entry does not exist so ignore it since it cannot be mapped. |
| | | return Promises.newResultPromise(null); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | }); |
| | | } |
| | | } |
| File was renamed from opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java |
| | |
| | | import java.util.concurrent.ConcurrentLinkedQueue; |
| | | import java.util.concurrent.CountDownLatch; |
| | | |
| | | import org.forgerock.http.Context; |
| | | import org.forgerock.json.resource.InternalServerErrorException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.SecurityContext; |
| | | import org.forgerock.json.resource.ServerContext; |
| | | import org.forgerock.opendj.ldap.AbstractAsynchronousConnection; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionEventListener; |
| | |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultReference; |
| | | import org.forgerock.util.promise.ExceptionHandler; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.PromiseImpl; |
| | | import org.forgerock.util.promise.Promises; |
| | | import org.forgerock.util.promise.ResultHandler; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.*; |
| | | |
| | | /** |
| | | * Common context information passed to containers and mappers. A new context is |
| | | * allocated for each REST request. |
| | | * Common request information passed to containers and mappers. |
| | | * A new @{code RequestState} is allocated for each REST request. |
| | | */ |
| | | final class Context implements Closeable { |
| | | final class RequestState implements Closeable { |
| | | |
| | | /** |
| | | * A cached read request - see cachedReads for more information. |
| | | */ |
| | | /** A cached read request - see cachedReads for more information. */ |
| | | private static final class CachedRead implements SearchResultHandler, LdapResultHandler<Result> { |
| | | private SearchResultEntry cachedEntry; |
| | | private final String cachedFilterString; |
| | |
| | | } |
| | | |
| | | LdapPromise<Result> getPromise() { |
| | | /* |
| | | * Perform uninterrupted wait since this method is unlikely to block |
| | | * for a long time. |
| | | */ |
| | | // Perform uninterrupted wait since this method is unlikely to block for a long time. |
| | | boolean wasInterrupted = false; |
| | | while (true) { |
| | | try { |
| | |
| | | }; |
| | | |
| | | private final Config config; |
| | | private final ServerContext context; |
| | | private final Context context; |
| | | private Connection connection; |
| | | private Control proxiedAuthzControl; |
| | | |
| | | Context(final Config config, final ServerContext context) { |
| | | RequestState(final Config config, final Context context) { |
| | | this.config = config; |
| | | this.context = context; |
| | | |
| | |
| | | return config; |
| | | } |
| | | |
| | | Connection getConnection() { |
| | | return connection; |
| | | } |
| | | |
| | | ServerContext getServerContext() { |
| | | Context getContext() { |
| | | return context; |
| | | } |
| | | |
| | | /** |
| | | * Performs common processing required before handling an HTTP request, |
| | | * including calculating the proxied authorization request control, and |
| | | * obtaining an LDAP connection. |
| | | * including calculating the proxied authorization request control. Then |
| | | * return a promise containing a valid LDAP connection or a |
| | | * {@link ResourceException} if an error is detected. |
| | | * <p> |
| | | * This method should be called at most once per request. |
| | | * |
| | | * @param handler |
| | | * The result handler which should be invoked if an error is |
| | | * detected. |
| | | * @param runnable |
| | | * The runnable which will be invoked once the common processing |
| | | * has completed. Implementations will be able to call |
| | | * {@link #getConnection()} to get the LDAP connection for use |
| | | * with subsequent LDAP requests. |
| | | * @return A {@link Promise} containing a valid {@link Connection} |
| | | */ |
| | | void run(final org.forgerock.json.resource.ResultHandler<?> handler, final Runnable runnable) { |
| | | Promise<Connection, ResourceException> getConnection() { |
| | | /* |
| | | * Compute the proxied authorization control from the content of the |
| | | * security context if present. Only do this if we are not using a |
| | |
| | | securityContext.getAuthorizationId(), config.schema()); |
| | | proxiedAuthzControl = ProxiedAuthV2RequestControl.newControl(authzId); |
| | | } catch (final ResourceException e) { |
| | | handler.handleError(e); |
| | | return; |
| | | return Promises.newExceptionPromise(e); |
| | | } |
| | | } else { |
| | | handler.handleError(new InternalServerErrorException( |
| | | return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException( |
| | | i18n("The request could not be authorized because it did not contain a security context"))); |
| | | return; |
| | | } |
| | | } |
| | | |
| | |
| | | * to re-use the LDAP connection which was used for authentication. |
| | | */ |
| | | if (connection != null) { |
| | | // Invoke the handler immediately since a connection is available. |
| | | runnable.run(); |
| | | return Promises.newResultPromise(connection); |
| | | } else if (config.connectionFactory() != null) { |
| | | final PromiseImpl<Connection, ResourceException> promise = PromiseImpl.create(); |
| | | config.connectionFactory().getConnectionAsync().thenOnResult(new ResultHandler<Connection>() { |
| | | @Override |
| | | public final void handleResult(final Connection result) { |
| | | connection = wrap(result); |
| | | runnable.run(); |
| | | promise.handleResult(connection); |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>() { |
| | | @Override |
| | | public final void handleException(final LdapException exception) { |
| | | handler.handleError(asResourceException(exception)); |
| | | promise.handleException(asResourceException(exception)); |
| | | } |
| | | }); |
| | | return promise; |
| | | } else { |
| | | handler.handleError(new InternalServerErrorException( |
| | | return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException( |
| | | i18n("The request could not be processed because there was no LDAP connection available for use"))); |
| | | } |
| | | } |
| | |
| | | import java.util.Set; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.fluent.JsonValueException; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.JsonValueException; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.ResourceException; |
| | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.EntryNotFoundException; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.FailoverLoadBalancingAlgorithm; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LDAPConnectionFactory; |
| | | import org.forgerock.opendj.ldap.LDAPOptions; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.LinkedAttribute; |
| | | import org.forgerock.opendj.ldap.MultipleEntriesFoundException; |
| | | import org.forgerock.opendj.ldap.RDN; |
| | |
| | | * collections. |
| | | */ |
| | | public final class Rest2LDAP { |
| | | |
| | | /** |
| | | * Indicates whether or not LDAP client connections should use SSL or |
| | | * StartTLS. |
| | |
| | | } |
| | | |
| | | @Override |
| | | SearchRequest createSearchRequest(final Context c, final DN baseDN, final String resourceId) { |
| | | SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) { |
| | | return newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter.equality(idAttribute |
| | | .toString(), resourceId)); |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final Context c, final Set<String> ldapAttributes) { |
| | | void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(idAttribute.toString()); |
| | | } |
| | | |
| | | @Override |
| | | String getResourceId(final Context c, final Entry entry) { |
| | | String getResourceId(final RequestState requestState, final Entry entry) { |
| | | return entry.parseAttribute(idAttribute).asString(); |
| | | } |
| | | |
| | | @Override |
| | | void setResourceId(final Context c, final DN baseDN, final String resourceId, |
| | | void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId, |
| | | final Entry entry) throws ResourceException { |
| | | if (isServerProvided) { |
| | | if (resourceId != null) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | SearchRequest createSearchRequest(final Context c, final DN baseDN, final String resourceId) { |
| | | SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) { |
| | | return newSearchRequest(baseDN.child(rdn(resourceId)), SearchScope.BASE_OBJECT, Filter |
| | | .objectClassPresent()); |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final Context c, final Set<String> ldapAttributes) { |
| | | void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(attribute.toString()); |
| | | } |
| | | |
| | | @Override |
| | | String getResourceId(final Context c, final Entry entry) { |
| | | String getResourceId(final RequestState requestState, final Entry entry) { |
| | | return entry.parseAttribute(attribute).asString(); |
| | | } |
| | | |
| | | @Override |
| | | void setResourceId(final Context c, final DN baseDN, final String resourceId, |
| | | void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId, |
| | | final Entry entry) throws ResourceException { |
| | | if (resourceId != null) { |
| | | entry.setName(baseDN.child(rdn(resourceId))); |
| | |
| | | return simple(AttributeDescription.valueOf(attribute)); |
| | | } |
| | | |
| | | private static ConnectionFactory configureConnectionFactory(final JsonValue configuration) { |
| | | return configureConnectionFactory(configuration, (ClassLoader) null); |
| | | } |
| | | |
| | | private static ConnectionFactory configureConnectionFactory(final JsonValue configuration, |
| | | final ClassLoader providerClassLoader) { |
| | | // Parse pool parameters, |
| New file |
| | |
| | | /* |
| | | * 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 2015 ForgeRock AS. |
| | | */ |
| | | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.http.util.Json.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory; |
| | | import static org.forgerock.util.Utils.*; |
| | | |
| | | import java.io.Closeable; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.net.URL; |
| | | |
| | | import org.forgerock.http.Context; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.HttpApplication; |
| | | import org.forgerock.http.HttpApplicationException; |
| | | import org.forgerock.http.handler.Handlers; |
| | | import org.forgerock.http.io.Buffer; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.json.resource.http.CrestHttp; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.util.Factory; |
| | | import org.forgerock.util.Reject; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | /** Rest2ldap HTTP application. */ |
| | | public final class Rest2LDAPHttpApplication implements HttpApplication { |
| | | private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class); |
| | | |
| | | private static final class HttpHandler implements Handler, Closeable { |
| | | private final ConnectionFactory ldapConnectionFactory; |
| | | private final Handler delegate; |
| | | |
| | | HttpHandler(final JsonValue configuration) { |
| | | ldapConnectionFactory = createLdapConnectionFactory(configuration); |
| | | try { |
| | | delegate = CrestHttp.newHttpHandler(createRouter(configuration, ldapConnectionFactory)); |
| | | } catch (final RuntimeException e) { |
| | | closeSilently(ldapConnectionFactory); |
| | | throw e; |
| | | } |
| | | } |
| | | |
| | | private static RequestHandler createRouter( |
| | | final JsonValue configuration, final ConnectionFactory ldapConnectionFactory) { |
| | | final AuthorizationPolicy authzPolicy = configuration.get("servlet") |
| | | .get("authorizationPolicy") |
| | | .required() |
| | | .asEnum(AuthorizationPolicy.class); |
| | | final String proxyAuthzTemplate = configuration.get("servlet").get("proxyAuthzIdTemplate").asString(); |
| | | final JsonValue mappings = configuration.get("servlet").get("mappings").required(); |
| | | |
| | | final Router router = new Router(); |
| | | for (final String mappingUrl : mappings.keys()) { |
| | | final JsonValue mapping = mappings.get(mappingUrl); |
| | | final CollectionResourceProvider provider = Rest2LDAP.builder() |
| | | .ldapConnectionFactory(ldapConnectionFactory) |
| | | .authorizationPolicy(authzPolicy) |
| | | .proxyAuthzIdTemplate(proxyAuthzTemplate) |
| | | .configureMapping(mapping) |
| | | .build(); |
| | | router.addRoute(Router.uriTemplate(mappingUrl), provider); |
| | | } |
| | | return router; |
| | | } |
| | | |
| | | private static ConnectionFactory createLdapConnectionFactory(final JsonValue configuration) { |
| | | final String ldapFactoryName = configuration.get("servlet").get("ldapConnectionFactory").asString(); |
| | | if (ldapFactoryName != null) { |
| | | return configureConnectionFactory( |
| | | configuration.get("ldapConnectionFactories").required(), ldapFactoryName); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public void close() { |
| | | closeSilently(ldapConnectionFactory); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) { |
| | | return delegate.handle(context, request); |
| | | } |
| | | } |
| | | |
| | | private final URL configurationUrl; |
| | | private HttpHandler handler; |
| | | private HttpAuthenticationFilter filter; |
| | | |
| | | /** |
| | | * Default constructor called by the HTTP Framework which will use the |
| | | * default configuration file location. |
| | | */ |
| | | public Rest2LDAPHttpApplication() { |
| | | this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json"); |
| | | } |
| | | |
| | | /** |
| | | * Creates a new Rest2LDAP HTTP application using the provided configuration URL. |
| | | * |
| | | * @param configurationURL |
| | | * The URL to the JSON configuration file. |
| | | */ |
| | | public Rest2LDAPHttpApplication(final URL configurationURL) { |
| | | Reject.ifNull(configurationURL, "The configuration URL must not be null"); |
| | | this.configurationUrl = configurationURL; |
| | | } |
| | | |
| | | private static JsonValue readJson(final URL resource) throws IOException { |
| | | try (InputStream in = resource.openStream()) { |
| | | return new JsonValue(readJsonLenient(in)); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public Handler start() throws HttpApplicationException { |
| | | try { |
| | | final JsonValue configuration = readJson(configurationUrl); |
| | | handler = new HttpHandler(configuration); |
| | | filter = new HttpAuthenticationFilter(configuration); |
| | | return Handlers.chainOf(handler, filter); |
| | | } catch (final Exception e) { |
| | | // TODO i18n, once supported in opendj-rest2ldap |
| | | final String errorMsg = "Unable to start Rest2Ldap Http Application"; |
| | | LOG.error(errorMsg, e); |
| | | stop(); |
| | | throw new HttpApplicationException(errorMsg, e); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public Factory<Buffer> getBufferFactory() { |
| | | // Use container default buffer factory. |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public void stop() { |
| | | closeSilently(handler, filter); |
| | | handler = null; |
| | | filter = null; |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2014 ForgeRock AS. |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | import static java.util.Collections.*; |
| | | |
| | | import static org.forgerock.opendj.ldap.Filter.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.*; |
| | | import static org.forgerock.util.promise.Promises.newExceptionPromise; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | /** |
| | | * An attribute mapper which provides a simple mapping from a JSON value to a |
| | |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final FilterType type, final String operator, final Object valueAssertion, |
| | | final ResultHandler<Filter> h) { |
| | | Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) { |
| | | if (subPath.isEmpty()) { |
| | | try { |
| | | final ByteString va = |
| | | valueAssertion != null ? encoder().apply(valueAssertion) : null; |
| | | h.handleResult(toFilter(type, ldapAttributeName.toString(), va)); |
| | | return newResultPromise(toFilter(type, ldapAttributeName.toString(), va)); |
| | | } catch (final Exception e) { |
| | | // Invalid assertion value - bad request. |
| | | h.handleError(new BadRequestException(i18n( |
| | | return newExceptionPromise((ResourceException) new BadRequestException(i18n( |
| | | "The request cannot be processed because it contained an " |
| | | + "illegal filter assertion value '%s' for field '%s'", String |
| | | .valueOf(valueAssertion), path), e)); |
| | | + "illegal filter assertion value '%s' for field '%s'", |
| | | String.valueOf(valueAssertion), path), e)); |
| | | } |
| | | } else { |
| | | // This attribute mapper does not support partial filtering. |
| | | h.handleResult(alwaysFalse()); |
| | | return newResultPromise(alwaysFalse()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void getNewLDAPAttributes(final Context c, final JsonPointer path, |
| | | final List<Object> newValues, final ResultHandler<Attribute> h) { |
| | | Promise<Attribute, ResourceException> getNewLDAPAttributes( |
| | | final RequestState requestState, final JsonPointer path, final List<Object> newValues) { |
| | | try { |
| | | h.handleResult(jsonToAttribute(newValues, ldapAttributeName, encoder())); |
| | | return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder())); |
| | | } catch (final Exception ex) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | return newExceptionPromise((ResourceException) new BadRequestException(i18n( |
| | | "The request cannot be processed because an error occurred while " |
| | | + "encoding the values for the field '%s': %s", path, ex.getMessage()))); |
| | | } |
| | |
| | | } |
| | | |
| | | @Override |
| | | void read(final Context c, final JsonPointer path, final Entry e, |
| | | final ResultHandler<JsonValue> h) { |
| | | Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) { |
| | | try { |
| | | final Object value; |
| | | if (attributeIsSingleValued()) { |
| | |
| | | } else { |
| | | final Set<Object> s = |
| | | e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJSONValues); |
| | | value = s.isEmpty() ? null : new ArrayList<Object>(s); |
| | | value = s.isEmpty() ? null : new ArrayList<>(s); |
| | | } |
| | | h.handleResult(value != null ? new JsonValue(value) : null); |
| | | return newResultPromise(value != null ? new JsonValue(value) : null); |
| | | } catch (final Exception ex) { |
| | | // The LDAP attribute could not be decoded. |
| | | h.handleError(asResourceException(ex)); |
| | | return newExceptionPromise(asResourceException(ex)); |
| | | } |
| | | } |
| | | |
| | | private Function<ByteString, ? extends Object, NeverThrowsException> decoder() { |
| | | private Function<ByteString, ?, NeverThrowsException> decoder() { |
| | | return decoder == null ? byteStringToJson(ldapAttributeName) : decoder; |
| | | } |
| | | |
| | |
| | | import static org.forgerock.opendj.ldap.schema.CoreSchema.getBooleanSyntax; |
| | | import static org.forgerock.opendj.ldap.schema.CoreSchema.getGeneralizedTimeSyntax; |
| | | import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | |
| | | * Internal utility methods. |
| | | */ |
| | | final class Utils { |
| | | /** |
| | | * Implementation class for {@link #accumulate}. |
| | | * |
| | | * @param <V> |
| | | * The type of result. |
| | | */ |
| | | private static final class AccumulatingResultHandler<V> implements ResultHandler<V> { |
| | | /** Guarded by latch. */ |
| | | private ResourceException exception; |
| | | private final ResultHandler<List<V>> handler; |
| | | private final AtomicInteger latch; |
| | | private final List<V> results; |
| | | |
| | | private AccumulatingResultHandler(final int size, final ResultHandler<List<V>> handler) { |
| | | if (size < 0) { |
| | | throw new IllegalStateException(); |
| | | } |
| | | this.latch = new AtomicInteger(size); |
| | | this.results = new ArrayList<>(size); |
| | | this.handler = handler; |
| | | if (size == 0) { |
| | | // Invoke immediately. |
| | | handler.handleResult(results); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void handleError(final ResourceException e) { |
| | | exception = e; |
| | | latch(); // Volatile write publishes exception. |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final V result) { |
| | | if (result != null) { |
| | | synchronized (results) { |
| | | results.add(result); |
| | | } |
| | | } |
| | | latch(); |
| | | } |
| | | |
| | | private void latch() { |
| | | /* |
| | | * Invoke the handler once all results have been received. Avoid |
| | | * failing-fast when an error occurs because some in-flight tasks |
| | | * may depend on resources (e.g. connections) which are |
| | | * automatically closed on completion. |
| | | */ |
| | | if (latch.decrementAndGet() == 0) { |
| | | if (exception != null) { |
| | | handler.handleError(exception); |
| | | } else { |
| | | handler.handleResult(results); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static final Function<Object, ByteString, NeverThrowsException> BASE64_TO_BYTESTRING = |
| | | new Function<Object, ByteString, NeverThrowsException>() { |
| | |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * Returns a result handler which can be used to collect the results of |
| | | * {@code size} asynchronous operations. Once all results have been received |
| | | * {@code handler} will be invoked with a list containing the results. |
| | | * Accumulation ignores {@code null} results, so the result list may be |
| | | * smaller than {@code size}. The returned result handler does not |
| | | * fail-fast: it will wait until all results have been received even if an |
| | | * error has been detected. This ensures that asynchronous operations can |
| | | * use resources such as connections which are automatically released |
| | | * (closed) upon completion of the final operation. |
| | | * |
| | | * @param <V> |
| | | * The type of result to be collected. |
| | | * @param size |
| | | * The number of expected results. |
| | | * @param handler |
| | | * The result handler to be invoked when all results have been |
| | | * received. |
| | | * @return A result handler which can be used to collect the results of |
| | | * {@code size} asynchronous operations. |
| | | */ |
| | | static <V> ResultHandler<V> accumulate(final int size, final ResultHandler<List<V>> handler) { |
| | | return new AccumulatingResultHandler<>(size, handler); |
| | | } |
| | | |
| | | static Object attributeToJson(final Attribute a) { |
| | | final Function<ByteString, Object, NeverThrowsException> f = byteStringToJson(a.getAttributeDescription()); |
| | |
| | | return s != null ? s.toLowerCase(Locale.ENGLISH) : null; |
| | | } |
| | | |
| | | /** |
| | | * Returns a result handler which accepts results of type {@code M}, applies |
| | | * the function {@code f} in order to convert the result to an object of |
| | | * type {@code N}, and subsequently invokes {@code handler}. If an |
| | | * unexpected error occurs while performing the transformation, the |
| | | * exception is converted to a {@code ResourceException} before invoking |
| | | * {@code handler.handleError()}. |
| | | * |
| | | * @param <M> |
| | | * The type of result expected by the returned handler. |
| | | * @param <N> |
| | | * The type of result expected by {@code handler}. |
| | | * @param f |
| | | * A function which converts the result of type {@code M} to type |
| | | * {@code N}. |
| | | * @param handler |
| | | * A result handler which accepts results of type {@code N}. |
| | | * @return A result handler which accepts results of type {@code M}. |
| | | */ |
| | | static <M, N> ResultHandler<M> transform(final Function<M, N, NeverThrowsException> f, |
| | | final ResultHandler<N> handler) { |
| | | return new ResultHandler<M>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | handler.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final M result) { |
| | | try { |
| | | handler.handleResult(f.apply(result)); |
| | | } catch (final Throwable t) { |
| | | handler.handleError(asResourceException(t)); |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private static <T> List<T> asList(final Collection<T> c) { |
| | | if (c instanceof List) { |
| | | return (List<T>) c; |
| | |
| | | |
| | | import static java.util.Arrays.asList; |
| | | import static org.fest.assertions.Assertions.assertThat; |
| | | import static org.fest.assertions.Fail.fail; |
| | | import static org.forgerock.json.fluent.JsonValue.field; |
| | | import static org.forgerock.json.fluent.JsonValue.json; |
| | | import static org.forgerock.json.fluent.JsonValue.object; |
| | | import static org.forgerock.json.JsonValue.field; |
| | | import static org.forgerock.json.JsonValue.json; |
| | | import static org.forgerock.json.JsonValue.object; |
| | | import static org.forgerock.json.resource.PatchOperation.add; |
| | | import static org.forgerock.json.resource.PatchOperation.increment; |
| | | import static org.forgerock.json.resource.PatchOperation.remove; |
| | |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.Connection; |
| | | import org.forgerock.json.resource.NotFoundException; |
| | | import org.forgerock.json.resource.NotSupportedException; |
| | | import org.forgerock.json.resource.PreconditionFailedException; |
| | | import org.forgerock.json.resource.QueryFilter; |
| | | import org.forgerock.json.resource.QueryResult; |
| | | import org.forgerock.json.resource.QueryResponse; |
| | | import org.forgerock.json.resource.Requests; |
| | | import org.forgerock.json.resource.Resource; |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.IntermediateResponseHandler; |
| | | import org.forgerock.opendj.ldap.LdapResultHandler; |
| | | import org.forgerock.opendj.ldap.MemoryBackend; |
| | | import org.forgerock.opendj.ldap.RequestContext; |
| | | import org.forgerock.opendj.ldap.RequestHandler; |
| | | import org.forgerock.opendj.ldap.LdapResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.requests.AddRequest; |
| | | import org.forgerock.opendj.ldap.requests.BindRequest; |
| | |
| | | import org.forgerock.opendj.ldif.LDIFEntryReader; |
| | | import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.forgerock.util.query.QueryFilter; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Tests that CREST requests are correctly mapped to LDAP. |
| | | */ |
| | | /** Tests that CREST requests are correctly mapped to LDAP. */ |
| | | @SuppressWarnings({ "javadoc" }) |
| | | @Test |
| | | public final class BasicRequestsTest extends ForgeRockTestCase { |
| | |
| | | // so that we can check that the request handler is returning everything. |
| | | // FIXME: factor out test for re-use as common test suite (e.g. for InMemoryBackend). |
| | | |
| | | private static final QueryFilter<JsonPointer> NO_FILTER = QueryFilter.alwaysTrue(); |
| | | |
| | | @Test |
| | | public void testQueryAll() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final List<Resource> resources = new LinkedList<>(); |
| | | final QueryResult result = |
| | | connection.query(ctx(), Requests.newQueryRequest("").setQueryFilter( |
| | | QueryFilter.alwaysTrue()), resources); |
| | | final List<ResourceResponse> resources = new LinkedList<>(); |
| | | final QueryResponse result = connection.query( |
| | | ctx(), Requests.newQueryRequest("").setQueryFilter(NO_FILTER), resources); |
| | | assertThat(resources).hasSize(5); |
| | | assertThat(result.getPagedResultsCookie()).isNull(); |
| | | assertThat(result.getRemainingPagedResults()).isEqualTo(-1); |
| | | assertThat(result.getTotalPagedResults()).isEqualTo(-1); |
| | | } |
| | | |
| | | @Test |
| | | public void testQueryNone() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final List<Resource> resources = new LinkedList<>(); |
| | | final QueryResult result = |
| | | connection.query(ctx(), Requests.newQueryRequest("").setQueryFilter( |
| | | QueryFilter.alwaysFalse()), resources); |
| | | final List<ResourceResponse> resources = new LinkedList<>(); |
| | | final QueryResponse result = connection.query( |
| | | ctx(), Requests.newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer>alwaysFalse()), resources); |
| | | assertThat(resources).hasSize(0); |
| | | assertThat(result.getPagedResultsCookie()).isNull(); |
| | | assertThat(result.getRemainingPagedResults()).isEqualTo(-1); |
| | | assertThat(result.getTotalPagedResults()).isEqualTo(-1); |
| | | } |
| | | |
| | | @Test |
| | | public void testQueryPageResultsCookie() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final List<Resource> resources = new ArrayList<>(); |
| | | final List<ResourceResponse> resources = new ArrayList<>(); |
| | | |
| | | // Read first page. |
| | | QueryResult result = |
| | | connection.query(ctx(), newQueryRequest("") |
| | | .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2), resources); |
| | | QueryResponse result = connection.query( |
| | | ctx(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources); |
| | | assertThat(result.getPagedResultsCookie()).isNotNull(); |
| | | assertThat(resources).hasSize(2); |
| | | assertThat(resources.get(0).getId()).isEqualTo("test1"); |
| | |
| | | resources.clear(); |
| | | |
| | | // Read second page. |
| | | result = |
| | | connection.query(ctx(), newQueryRequest("") |
| | | .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2) |
| | | .setPagedResultsCookie(cookie), resources); |
| | | result = connection.query(ctx(), |
| | | newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources); |
| | | assertThat(result.getPagedResultsCookie()).isNotNull(); |
| | | assertThat(resources).hasSize(2); |
| | | assertThat(resources.get(0).getId()).isEqualTo("test3"); |
| | |
| | | resources.clear(); |
| | | |
| | | // Read third page. |
| | | result = |
| | | connection.query(ctx(), newQueryRequest("") |
| | | .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2) |
| | | .setPagedResultsCookie(cookie), resources); |
| | | result = connection.query(ctx(), |
| | | newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources); |
| | | assertThat(result.getPagedResultsCookie()).isNull(); |
| | | assertThat(resources).hasSize(1); |
| | | assertThat(resources.get(0).getId()).isEqualTo("test5"); |
| | |
| | | @Test |
| | | public void testQueryPageResultsIndexed() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final List<Resource> resources = new ArrayList<>(); |
| | | QueryResult result = |
| | | connection.query(ctx(), newQueryRequest("") |
| | | .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2) |
| | | .setPagedResultsOffset(1), resources); |
| | | final List<ResourceResponse> resources = new ArrayList<>(); |
| | | QueryResponse result = connection.query(ctx(), |
| | | newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources); |
| | | assertThat(result.getPagedResultsCookie()).isNotNull(); |
| | | assertThat(resources).hasSize(2); |
| | | assertThat(resources.get(0).getId()).isEqualTo("test3"); |
| | | assertThat(resources.get(1).getId()).isEqualTo("test4"); |
| | | } |
| | | |
| | | @Test |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testDelete() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1")); |
| | | final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | try { |
| | | connection.read(ctx(), newReadRequest("/test1")); |
| | | fail("Read succeeded unexpectedly"); |
| | | } catch (final NotFoundException e) { |
| | | // Expected. |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testDeleteMVCCMatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Resource resource = |
| | | connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345")); |
| | | final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | try { |
| | | connection.read(ctx(), newReadRequest("/test1")); |
| | | fail("Read succeeded unexpectedly"); |
| | | } catch (final NotFoundException e) { |
| | | // Expected. |
| | | } |
| | | } |
| | | |
| | | @Test(expectedExceptions = PreconditionFailedException.class) |
| | |
| | | @Test |
| | | public void testPatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Resource resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", |
| | | "changed"))); |
| | | final ResourceResponse resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed"))); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | |
| | | public void testPatchEmpty() throws Exception { |
| | | final List<Request> requests = new LinkedList<>(); |
| | | final Connection connection = newConnection(requests); |
| | | final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1")); |
| | | final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1")); |
| | | checkResourcesAreEqual(resource1, getTestUser1(12345)); |
| | | |
| | | /* |
| | |
| | | assertThat(requests).hasSize(1); |
| | | assertThat(requests.get(0)).isInstanceOf(SearchRequest.class); |
| | | |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1(12345)); |
| | | } |
| | | |
| | |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1(12345); |
| | | newContent.put("description", asList("one", "two")); |
| | | final Resource resource1 = |
| | | final ResourceResponse resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", |
| | | "two")))); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1(12345); |
| | | newContent.put("description", asList("one", "two")); |
| | | final Resource resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description/-", "one"), |
| | | add("/description/-", "two"))); |
| | | final ResourceResponse resource1 = connection.patch( |
| | | ctx(), newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two"))); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchConstantAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk")))); |
| | | newConnection().patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk")))); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchDeleteOptionalAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), |
| | | newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | final Resource resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", remove("/description"))); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", remove("/description"))); |
| | | checkResourcesAreEqual(resource1, getTestUser1(12345)); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1(12345)); |
| | | } |
| | | |
| | |
| | | newContent.put("singleNumber", 100); |
| | | newContent.put("multiNumber", asList(200, 300)); |
| | | |
| | | final Resource resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/singleNumber", 0), add( |
| | | "/multiNumber", asList(100, 200)), increment("/singleNumber", 100), |
| | | final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", |
| | | add("/singleNumber", 0), |
| | | add("/multiNumber", asList(100, 200)), |
| | | increment("/singleNumber", 100), |
| | | increment("/multiNumber", 100))); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchMissingRequiredAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", remove("/name/surname"))); |
| | | newConnection().patch(ctx(), newPatchRequest("/test1", remove("/name/surname"))); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchModifyOptionalAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), |
| | | newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | final Resource resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", |
| | | asList("three")))); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | final ResourceResponse resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("three")))); |
| | | final JsonValue newContent = getTestUser1(12345); |
| | | newContent.put("description", asList("one", "two", "three")); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | |
| | | @Test |
| | | public void testPatchMVCCMatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Resource resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", |
| | | add("/name/displayName", "changed")).setRevision("12345")); |
| | | final ResourceResponse resource1 = connection.patch( |
| | | ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345")); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | | @Test(expectedExceptions = PreconditionFailedException.class) |
| | | public void testPatchMVCCNoMatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")) |
| | | .setRevision("12346")); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346")); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testPatchNotFound() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed"))); |
| | | newConnection().patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed"))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchReadOnlyAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | // Etag is read-only. |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("_rev", "99999"))); |
| | | newConnection().patch(ctx(), newPatchRequest("/test1", add("_rev", "99999"))); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchReplacePartialObject() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final JsonValue expected = |
| | | json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id", |
| | | "test1"), field("_rev", "12345"), field("name", object(field("displayName", |
| | | "Humpty"), field("surname", "Dumpty"))))); |
| | | final Resource resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", replace("/name", object(field( |
| | | "displayName", "Humpty"), field("surname", "Dumpty"))))); |
| | | final JsonValue expected = json(object( |
| | | field("schemas", asList("urn:scim:schemas:core:1.0")), |
| | | field("_id", "test1"), |
| | | field("_rev", "12345"), |
| | | field("name", object(field("displayName", "Humpty"), |
| | | field("surname", "Dumpty"))))); |
| | | final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", |
| | | replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty"))))); |
| | | checkResourcesAreEqual(resource1, expected); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, expected); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchReplaceWholeObject() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = |
| | | json(object(field("name", object(field("displayName", "Humpty"), field("surname", |
| | | "Dumpty"))))); |
| | | final JsonValue expected = |
| | | json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id", |
| | | "test1"), field("_rev", "12345"), field("name", object(field("displayName", |
| | | "Humpty"), field("surname", "Dumpty"))))); |
| | | final Resource resource1 = |
| | | final JsonValue newContent = json(object( |
| | | field("name", object(field("displayName", "Humpty"), |
| | | field("surname", "Dumpty"))))); |
| | | final JsonValue expected = json(object( |
| | | field("schemas", asList("urn:scim:schemas:core:1.0")), |
| | | field("_id", "test1"), |
| | | field("_rev", "12345"), |
| | | field("name", object(field("displayName", "Humpty"), |
| | | field("surname", "Dumpty"))))); |
| | | final ResourceResponse resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", replace("/", newContent))); |
| | | checkResourcesAreEqual(resource1, expected); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, expected); |
| | | } |
| | | |
| | |
| | | |
| | | @Test |
| | | public void testRead() throws Exception { |
| | | final Resource resource = newConnection().read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | } |
| | | |
| | |
| | | |
| | | @Test |
| | | public void testReadSelectAllFields() throws Exception { |
| | | final Resource resource = |
| | | newConnection().read(ctx(), newReadRequest("/test1").addField("/")); |
| | | final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1").addField("/")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | } |
| | | |
| | | @Test |
| | | public void testReadSelectPartial() throws Exception { |
| | | final Resource resource = |
| | | newConnection().read(ctx(), newReadRequest("/test1").addField("/name/surname")); |
| | | final ResourceResponse resource = newConnection().read( |
| | | ctx(), newReadRequest("/test1").addField("/name/surname")); |
| | | assertThat(resource.getId()).isEqualTo("test1"); |
| | | assertThat(resource.getRevision()).isEqualTo("12345"); |
| | | assertThat(resource.getContent().get("_id").asString()).isNull(); |
| | |
| | | /** Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) */ |
| | | @Test(enabled = false) |
| | | public void testReadSelectPartialInsensitive() throws Exception { |
| | | final Resource resource = |
| | | newConnection().read(ctx(), newReadRequest("/test1").addField("/name/SURNAME")); |
| | | final ResourceResponse resource = newConnection().read( |
| | | ctx(), newReadRequest("/test1").addField("/name/SURNAME")); |
| | | assertThat(resource.getId()).isEqualTo("test1"); |
| | | assertThat(resource.getRevision()).isEqualTo("12345"); |
| | | assertThat(resource.getContent().get("_id").asString()).isNull(); |
| | |
| | | @Test |
| | | public void testUpdate() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Resource resource1 = |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))); |
| | | final ResourceResponse resource1 = connection.update( |
| | | ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | |
| | | public void testUpdateNoChange() throws Exception { |
| | | final List<Request> requests = new LinkedList<>(); |
| | | final Connection connection = newConnection(requests); |
| | | final Resource resource1 = |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345))); |
| | | final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345))); |
| | | |
| | | /* |
| | | * Check that no modify operation was sent (only a single search should |
| | | * be sent in order to get the current resource). |
| | | */ |
| | | // Check that no modify operation was sent |
| | | // (only a single search should be sent in order to get the current resource). |
| | | assertThat(requests).hasSize(1); |
| | | assertThat(requests.get(0)).isInstanceOf(SearchRequest.class); |
| | | |
| | | checkResourcesAreEqual(resource1, getTestUser1(12345)); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1(12345)); |
| | | } |
| | | |
| | |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1Updated(12345); |
| | | newContent.put("description", asList("one", "two")); |
| | | final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | |
| | | newContent.put("description", asList("one", "two")); |
| | | connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | newContent.remove("description"); |
| | | final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | |
| | | newContent.put("description", asList("one", "two")); |
| | | connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | newContent.put("description", asList("three")); |
| | | final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | | @Test |
| | | public void testUpdateMVCCMatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Resource resource1 = |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)) |
| | | .setRevision("12345")); |
| | | final ResourceResponse resource1 = |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345")); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | |
| | | .attribute("multiNumber", simple("multiNumber").decoder(byteStringToInteger()))); |
| | | } |
| | | |
| | | private void checkResourcesAreEqual(final Resource actual, final JsonValue expected) { |
| | | final Resource expectedResource = asResource(expected); |
| | | private void checkResourcesAreEqual(final ResourceResponse actual, final JsonValue expected) { |
| | | final ResourceResponse expectedResource = asResource(expected); |
| | | assertThat(actual.getId()).isEqualTo(expectedResource.getId()); |
| | | assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision()); |
| | | assertThat(actual.getContent().getObject()).isEqualTo( |
| | |
| | | } |
| | | |
| | | private JsonValue getTestUser1(final int rev) { |
| | | return content(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id", |
| | | "test1"), field("_rev", String.valueOf(rev)), field("name", object(field( |
| | | "displayName", "test user 1"), field("surname", "user 1"))))); |
| | | return content(object( |
| | | field("schemas", asList("urn:scim:schemas:core:1.0")), |
| | | field("_id", "test1"), |
| | | field("_rev", String.valueOf(rev)), |
| | | field("name", object(field("displayName", "test user 1"), |
| | | field("surname", "user 1"))))); |
| | | } |
| | | |
| | | private JsonValue getTestUser1Updated(final int rev) { |
| | | return content(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id", |
| | | "test1"), field("_rev", String.valueOf(rev)), field("name", object(field( |
| | | "displayName", "changed"), field("surname", "user 1"))))); |
| | | return content(object( |
| | | field("schemas", asList("urn:scim:schemas:core:1.0")), |
| | | field("_id", "test1"), |
| | | field("_rev", String.valueOf(rev)), |
| | | field("name", object(field("displayName", "changed"), |
| | | field("surname", "user 1"))))); |
| | | } |
| | | } |
| | |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.json.fluent.JsonValue.json; |
| | | import static org.forgerock.json.JsonValue.json; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.Resource; |
| | | import org.forgerock.json.resource.RootContext; |
| | | import org.forgerock.http.context.RootContext; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.json.resource.Responses; |
| | | |
| | | /** |
| | | * Unit test utility methods, including fluent methods for creating JSON |
| | | * objects. |
| | | */ |
| | | /** Unit test utility methods, including fluent methods for creating JSON objects. */ |
| | | public final class TestUtils { |
| | | |
| | | /** |
| | |
| | | * The JSON content. |
| | | * @return A {@code Resource} containing the provided JSON content. |
| | | */ |
| | | public static Resource asResource(final JsonValue content) { |
| | | return new Resource(content.get("_id").asString(), content.get("_rev").asString(), content); |
| | | public static ResourceResponse asResource(final JsonValue content) { |
| | | return Responses.newResourceResponse(content.get("_id").asString(), content.get("_rev").asString(), content); |
| | | } |
| | | |
| | | /** |
| | |
| | | <!-- ForgeRock libraries --> |
| | | <dependency> |
| | | <groupId>org.forgerock.opendj</groupId> |
| | | <artifactId>opendj-rest2ldap-servlet</artifactId> |
| | | <artifactId>opendj-rest2ldap</artifactId> |
| | | <version>${project.version}</version> |
| | | </dependency> |
| | | <dependency> |
| | |
| | | <artifactId>slf4j-jdk14</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.forgerock.commons</groupId> |
| | | <artifactId>json-resource</artifactId> |
| | | <version>${forgerockRestVersion}</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.forgerock.commons</groupId> |
| | | <artifactId>json-resource-http</artifactId> |
| | | <version>${forgerockRestVersion}</version> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>org.forgerock.http</groupId> |
| | | <artifactId>chf-http-core</artifactId> |
| | | <version>${forgerockHttpVersion}</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.forgerock.http</groupId> |
| | | <artifactId>chf-http-servlet</artifactId> |
| | | <version>${forgerockHttpVersion}</version> |
| | | </dependency> |
| | | |
| | | <!-- servlet and mail --> |
| | | <dependency> |
| | | <groupId>javax.servlet</groupId> |
| | |
| | | <outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping> |
| | | <excludes> |
| | | <exclude>javax.activation:activation</exclude> |
| | | <exclude>org.forgerock.commons:json-resource-servlet:war</exclude> |
| | | <exclude>org.forgerock.opendj:opendj-server-legacy</exclude> |
| | | </excludes> |
| | | </dependencySet> |
| | |
| | | */ |
| | | package org.opends.server.protocols.http; |
| | | |
| | | import java.io.Closeable; |
| | | import java.io.IOException; |
| | | import java.net.InetAddress; |
| | | import java.net.UnknownHostException; |
| | |
| | | import java.text.ParseException; |
| | | import java.util.Collection; |
| | | |
| | | import javax.servlet.*; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import javax.servlet.http.HttpServletResponseWrapper; |
| | | |
| | | import org.forgerock.http.Context; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.http.protocol.Status; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.*; |
| | | import org.forgerock.opendj.adapter.server3x.Adapters; |
| | | import org.forgerock.opendj.ldap.AddressMask; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.requests.BindRequest; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.responses.BindResult; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext; |
| | | import org.forgerock.opendj.rest2ldap.Rest2LDAP; |
| | | import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.promise.ExceptionHandler; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.ResultHandler; |
| | | import org.forgerock.util.promise.Promises; |
| | | import org.opends.server.admin.std.server.ConnectionHandlerCfg; |
| | | import org.opends.server.schema.SchemaConstants; |
| | | import org.opends.server.types.DisconnectReason; |
| | | import org.opends.server.util.Base64; |
| | | |
| | | import static org.forgerock.opendj.adapter.server3x.Adapters.*; |
| | | import static org.forgerock.opendj.ldap.LdapException.*; |
| | | import static org.forgerock.util.promise.Promises.*; |
| | | import static org.opends.messages.ProtocolMessages.*; |
| | | import static org.opends.server.loggers.AccessLogger.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | /** |
| | | * Servlet {@link Filter} that collects information about client connections. |
| | | */ |
| | | final class CollectClientConnectionsFilter implements javax.servlet.Filter |
| | | /** Servlet {@link Filter} that collects information about client connections. */ |
| | | final class CollectClientConnectionsFilter implements org.forgerock.http.Filter, Closeable |
| | | { |
| | | |
| | | /** This class holds all the necessary data to complete an HTTP request. */ |
| | | private static final class HTTPRequestContext |
| | | { |
| | | private AsyncContext asyncContext; |
| | | private HttpServletRequest request; |
| | | private HttpServletResponse response; |
| | | private FilterChain chain; |
| | | |
| | | private HTTPClientConnection clientConnection; |
| | | private Connection connection; |
| | | |
| | | /** Whether to pretty print the resulting JSON. */ |
| | | private boolean prettyPrint; |
| | | /** Used for the bind request when credentials are specified. */ |
| | | private String userName; |
| | | /** |
| | | * Used for the bind request when credentials are specified. For security |
| | | * reasons, the password must be discarded as soon as possible after it's |
| | | * been used. |
| | | */ |
| | | private String password; |
| | | } |
| | | |
| | | /** HTTP Header sent by the client with HTTP basic authentication. */ |
| | | static final String HTTP_BASIC_AUTH_HEADER = "Authorization"; |
| | | |
| | |
| | | * authentication |
| | | */ |
| | | public CollectClientConnectionsFilter( |
| | | HTTPConnectionHandler connectionHandler, |
| | | HTTPAuthenticationConfig authenticationConfig) |
| | | HTTPConnectionHandler connectionHandler, HTTPAuthenticationConfig authenticationConfig) |
| | | { |
| | | this.connectionHandler = connectionHandler; |
| | | this.authConfig = authenticationConfig; |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void init(FilterConfig filterConfig) throws ServletException |
| | | public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) |
| | | { |
| | | // nothing to do |
| | | } |
| | | final HTTPClientConnection clientConnection = new HTTPClientConnection(this.connectionHandler, context, request); |
| | | connectionHandler.addClientConnection(clientConnection); |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void doFilter(ServletRequest req, ServletResponse resp, |
| | | FilterChain chain) |
| | | if (connectionHandler.keepStats()) |
| | | { |
| | | final HttpServletRequest request = (HttpServletRequest) req; |
| | | final HttpServletResponse response = (HttpServletResponse) resp; |
| | | |
| | | final HTTPRequestContext ctx = new HTTPRequestContext(); |
| | | |
| | | ctx.request = request; |
| | | ctx.response = new HttpServletResponseWrapper(response) |
| | | { |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void setStatus(int sc) |
| | | { |
| | | ctx.clientConnection.log(sc); |
| | | super.setStatus(sc); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @SuppressWarnings("deprecation") |
| | | @Override |
| | | public void setStatus(int sc, String sm) |
| | | { |
| | | ctx.clientConnection.log(sc); |
| | | super.setStatus(sc, sm); |
| | | } |
| | | }; |
| | | ctx.chain = chain; |
| | | ctx.prettyPrint = |
| | | Boolean.parseBoolean(request.getParameter("_prettyPrint")); |
| | | |
| | | final HTTPClientConnection clientConnection = |
| | | new HTTPClientConnection(this.connectionHandler, request); |
| | | this.connectionHandler.addClientConnection(clientConnection); |
| | | |
| | | ctx.clientConnection = clientConnection; |
| | | |
| | | if (this.connectionHandler.keepStats()) { |
| | | this.connectionHandler.getStatTracker().addRequest( |
| | | ctx.clientConnection.getMethod()); |
| | | connectionHandler.getStatTracker().addRequest(request.getMethod()); |
| | | } |
| | | |
| | | try |
| | | { |
| | | if (!canProcessRequest(request, clientConnection)) |
| | | if (!canProcessRequest(clientConnection)) |
| | | { |
| | | return; |
| | | return resourceExceptionToPromise(ResourceException.getException(ResourceException.INTERNAL_ERROR)); |
| | | } |
| | | // logs the connect after all the possible disconnect reasons have been |
| | | // checked. |
| | | // Logs the connect after all the possible disconnect reasons have been checked. |
| | | logConnect(clientConnection); |
| | | |
| | | ctx.connection = new SdkConnectionAdapter(clientConnection); |
| | | final Connection connection = new SdkConnectionAdapter(clientConnection); |
| | | |
| | | final String[] userCredentials = extractUsernamePassword(request); |
| | | if (userCredentials != null && userCredentials.length == 2) |
| | | { |
| | | ctx.userName = userCredentials[0]; |
| | | ctx.password = userCredentials[1]; |
| | | ctx.asyncContext = getAsyncContext(request); |
| | | final String userName = userCredentials[0]; |
| | | final String password = userCredentials[1]; |
| | | |
| | | newRootConnection().searchSingleEntryAsync(buildSearchRequest(ctx.userName)).thenAsync( |
| | | new AsyncFunction<SearchResultEntry, BindResult, LdapException>() { |
| | | return Adapters.newRootConnection() |
| | | .searchSingleEntryAsync(buildSearchRequest(userName)) |
| | | .thenAsync(doBindAfterSearch(context, request, next, userName, password, clientConnection, connection), |
| | | returnErrorAfterFailedSearch(clientConnection)); |
| | | } |
| | | else if (this.connectionHandler.acceptUnauthenticatedRequests()) |
| | | { |
| | | // Use unauthenticated user |
| | | return doFilter(context, request, next, connection); |
| | | } |
| | | else |
| | | { |
| | | return authenticationFailure(clientConnection); |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | return asErrorResponse(e, clientConnection); |
| | | } |
| | | } |
| | | |
| | | private boolean canProcessRequest(final HTTPClientConnection connection) throws UnknownHostException |
| | | { |
| | | final InetAddress clientAddr = connection.getRemoteAddress(); |
| | | |
| | | // Check to see if the core server rejected the connection (e.g. already too many connections established). |
| | | if (connection.getConnectionID() < 0) |
| | | { |
| | | connection.disconnect( |
| | | DisconnectReason.ADMIN_LIMIT_EXCEEDED, true, ERR_CONNHANDLER_REJECTED_BY_SERVER.get()); |
| | | return false; |
| | | } |
| | | |
| | | // Check to see if the client is on the denied list. If so, then reject it immediately. |
| | | final ConnectionHandlerCfg config = this.connectionHandler.getCurrentConfig(); |
| | | final Collection<AddressMask> deniedClients = config.getDeniedClient(); |
| | | if (!deniedClients.isEmpty() |
| | | && AddressMask.matchesAny(deniedClients, clientAddr)) |
| | | { |
| | | connection.disconnect(DisconnectReason.CONNECTION_REJECTED, false, |
| | | ERR_CONNHANDLER_DENIED_CLIENT.get(connection.getClientHostPort(), connection.getServerHostPort())); |
| | | return false; |
| | | } |
| | | |
| | | // Check to see if there is an allowed list and if there is whether the client is on that list. |
| | | // If not, then reject the connection. |
| | | final Collection<AddressMask> allowedClients = config.getAllowedClient(); |
| | | if (!allowedClients.isEmpty() |
| | | && !AddressMask.matchesAny(allowedClients, clientAddr)) |
| | | { |
| | | connection.disconnect(DisconnectReason.CONNECTION_REJECTED, false, |
| | | ERR_CONNHANDLER_DISALLOWED_CLIENT.get(connection.getClientHostPort(), connection.getServerHostPort())); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private SearchRequest buildSearchRequest(String userName) |
| | | { |
| | | // Use configured rights to find the user DN |
| | | final Filter filter = Filter.format(authConfig.getSearchFilterTemplate(), userName); |
| | | return Requests.newSearchRequest( |
| | | authConfig.getSearchBaseDN(), authConfig.getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES); |
| | | } |
| | | |
| | | private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch( |
| | | final Context context, final Request request, final Handler next, final String userName, final String password, |
| | | final HTTPClientConnection clientConnection, final Connection connection) |
| | | { |
| | | return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>() |
| | | { |
| | | @Override |
| | | public Promise<BindResult, LdapException> apply(SearchResultEntry resultEntry) throws LdapException |
| | | public Promise<Response, NeverThrowsException> apply(final SearchResultEntry resultEntry) |
| | | { |
| | | final DN bindDN = resultEntry.getName(); |
| | | if (bindDN == null) |
| | | { |
| | | sendAuthenticationFailure(ctx); |
| | | return newExceptionPromise(newLdapException(ResultCode.CANCELLED)); |
| | | } |
| | | else |
| | | { |
| | | final BindRequest bindRequest = |
| | | Requests.newSimpleBindRequest(bindDN.toString(), ctx.password.getBytes(Charset.forName("UTF-8"))); |
| | | // We are done with the password at this stage, |
| | | // wipe it from memory for security reasons |
| | | ctx.password = null; |
| | | return ctx.connection.bindAsync(bindRequest); |
| | | } |
| | | return authenticationFailure(clientConnection); |
| | | } |
| | | |
| | | final BindRequest bindRequest = |
| | | Requests.newSimpleBindRequest(bindDN.toString(), password.getBytes(Charset.forName("UTF-8"))); |
| | | return connection.bindAsync(bindRequest) |
| | | .thenAsync(doChain(context, request, next, userName, clientConnection, connection), |
| | | returnErrorAfterFailedBind(clientConnection)); |
| | | } |
| | | ).thenOnResult(new ResultHandler<BindResult>() { |
| | | @Override |
| | | public void handleResult(BindResult result) |
| | | }; |
| | | } |
| | | |
| | | private AsyncFunction<BindResult, Response, NeverThrowsException> doChain( |
| | | final Context context, final Request request, final Handler next, final String userName, |
| | | final HTTPClientConnection clientConnection, final Connection connection) |
| | | { |
| | | ctx.clientConnection.setAuthUser(ctx.userName); |
| | | return new AsyncFunction<BindResult, Response, NeverThrowsException>() |
| | | { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(BindResult value) throws NeverThrowsException |
| | | { |
| | | clientConnection.setAuthUser(userName); |
| | | try |
| | | { |
| | | doFilter(ctx); |
| | | return doFilter(context, request, next, connection); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | onException(e, ctx); |
| | | return asErrorResponse(e, clientConnection); |
| | | } |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>(){ |
| | | }; |
| | | } |
| | | |
| | | private Promise<Response, NeverThrowsException> doFilter( |
| | | final Context context, final Request request, final Handler next, final Connection connection) throws Exception |
| | | { |
| | | final Context forwardedContext = new AuthenticatedConnectionContext(context, connection); |
| | | // Send the request further down the filter chain or pass to servlet |
| | | return next.handle(forwardedContext, request); |
| | | } |
| | | |
| | | private AsyncFunction<? super LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch( |
| | | final HTTPClientConnection clientConnection) |
| | | { |
| | | return new AsyncFunction<LdapException, Response, NeverThrowsException>() |
| | | { |
| | | @Override |
| | | public void handleException(LdapException exception) |
| | | public Promise<Response, NeverThrowsException> apply(final LdapException exception) |
| | | { |
| | | final ResultCode rc = exception.getResult().getResultCode(); |
| | | if (ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED.equals(rc) |
| | |
| | | { |
| | | // Avoid information leak: |
| | | // do not hint to the user that it is the username that is invalid |
| | | sendAuthenticationFailure(ctx); |
| | | return authenticationFailure(clientConnection); |
| | | } |
| | | else |
| | | { |
| | | onException(exception, ctx); |
| | | return asErrorResponse(exception, clientConnection); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | else if (this.connectionHandler.acceptUnauthenticatedRequests()) |
| | | { |
| | | // use unauthenticated user |
| | | doFilter(ctx); |
| | | } |
| | | else |
| | | { |
| | | sendAuthenticationFailure(ctx); |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | onException(e, ctx); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private void doFilter(HTTPRequestContext ctx) |
| | | throws Exception |
| | | private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind( |
| | | final HTTPClientConnection clientConnection) |
| | | { |
| | | /* |
| | | * WARNING: This action triggers 3-4 others: Set the connection for use with |
| | | * this request on the HttpServletRequest. It will make |
| | | * Rest2LDAPContextFactory create an AuthenticatedConnectionContext which |
| | | * will in turn ensure Rest2LDAP uses the supplied Connection object. |
| | | */ |
| | | ctx.request.setAttribute( |
| | | Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, ctx.connection); |
| | | |
| | | // send the request further down the filter chain or pass to servlet |
| | | ctx.chain.doFilter(ctx.request, ctx.response); |
| | | return new AsyncFunction<LdapException, Response, NeverThrowsException>() |
| | | { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(final LdapException e) |
| | | { |
| | | return asErrorResponse(e, clientConnection); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private void sendAuthenticationFailure(HTTPRequestContext ctx) |
| | | private Promise<Response, NeverThrowsException> authenticationFailure(final HTTPClientConnection clientConnection) |
| | | { |
| | | final int statusCode = HttpServletResponse.SC_UNAUTHORIZED; |
| | | return asErrorResponse(ResourceException.getException(401, "Invalid Credentials"), clientConnection, |
| | | DisconnectReason.INVALID_CREDENTIALS, false); |
| | | } |
| | | |
| | | private Promise<Response, NeverThrowsException> asErrorResponse( |
| | | final Throwable t, final HTTPClientConnection clientConnection) |
| | | { |
| | | return asErrorResponse(t, clientConnection, DisconnectReason.SERVER_ERROR, true); |
| | | } |
| | | |
| | | private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t, |
| | | final HTTPClientConnection clientConnection, final DisconnectReason reason, final boolean logError) |
| | | { |
| | | final ResourceException ex = Rest2LDAP.asResourceException(t); |
| | | try |
| | | { |
| | | // The user could not be authenticated. Send an HTTP Basic authentication |
| | | // challenge if HTTP Basic authentication is enabled. |
| | | ResourceException unauthorizedException = |
| | | ResourceException.getException(statusCode, "Invalid Credentials"); |
| | | sendErrorReponse(ctx.response, ctx.prettyPrint, unauthorizedException); |
| | | |
| | | ctx.clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS, |
| | | false, null); |
| | | } |
| | | finally |
| | | LocalizableMessage message = null; |
| | | if (logError) |
| | | { |
| | | ctx.clientConnection.log(statusCode); |
| | | |
| | | if (ctx.asyncContext != null) |
| | | { |
| | | ctx.asyncContext.complete(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void onException(Exception e, HTTPRequestContext ctx) |
| | | { |
| | | ResourceException ex = Rest2LDAP.asResourceException(e); |
| | | try |
| | | { |
| | | logger.traceException(e); |
| | | |
| | | sendErrorReponse(ctx.response, ctx.prettyPrint, ex); |
| | | |
| | | LocalizableMessage message = |
| | | INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(ctx.clientConnection |
| | | .getClientHostPort(), ctx.clientConnection.getServerHostPort(), |
| | | getExceptionMessage(e)); |
| | | logger.traceException(ex); |
| | | message = INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get( |
| | | clientConnection.getClientHostPort(), clientConnection.getServerHostPort(), getExceptionMessage(ex)); |
| | | logger.debug(message); |
| | | |
| | | ctx.clientConnection.disconnect(DisconnectReason.SERVER_ERROR, false, |
| | | message); |
| | | } |
| | | clientConnection.disconnect(reason, false, message); |
| | | } |
| | | finally |
| | | { |
| | | ctx.clientConnection.log(ex.getCode()); |
| | | |
| | | if (ctx.asyncContext != null) |
| | | { |
| | | ctx.asyncContext.complete(); |
| | | } |
| | | } |
| | | clientConnection.log(ex.getCode()); |
| | | } |
| | | |
| | | private boolean canProcessRequest(HttpServletRequest request, |
| | | final HTTPClientConnection clientConnection) throws UnknownHostException |
| | | { |
| | | InetAddress clientAddr = InetAddress.getByName(request.getRemoteAddr()); |
| | | |
| | | // Check to see if the core server rejected the |
| | | // connection (e.g., already too many connections |
| | | // established). |
| | | if (clientConnection.getConnectionID() < 0) |
| | | { |
| | | clientConnection.disconnect(DisconnectReason.ADMIN_LIMIT_EXCEEDED, true, |
| | | ERR_CONNHANDLER_REJECTED_BY_SERVER.get()); |
| | | return false; |
| | | return resourceExceptionToPromise(ex); |
| | | } |
| | | |
| | | // Check to see if the client is on the denied list. |
| | | // If so, then reject it immediately. |
| | | ConnectionHandlerCfg config = this.connectionHandler.getCurrentConfig(); |
| | | Collection<AddressMask> allowedClients = config.getAllowedClient(); |
| | | Collection<AddressMask> deniedClients = config.getDeniedClient(); |
| | | if (!deniedClients.isEmpty() |
| | | && AddressMask.matchesAny(deniedClients, clientAddr)) |
| | | Promise<Response, NeverThrowsException> resourceExceptionToPromise(final ResourceException e) |
| | | { |
| | | clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED, false, |
| | | ERR_CONNHANDLER_DENIED_CLIENT.get(clientConnection |
| | | .getClientHostPort(), clientConnection.getServerHostPort())); |
| | | return false; |
| | | } |
| | | // Check to see if there is an allowed list and if |
| | | // there is whether the client is on that list. If |
| | | // not, then reject the connection. |
| | | if (!allowedClients.isEmpty() |
| | | && !AddressMask.matchesAny(allowedClients, clientAddr)) |
| | | final Response response = new Response().setStatus(Status.valueOf(e.getCode())) |
| | | .setEntity(e.toJsonValue().getObject()); |
| | | if (e.getCode() == 401 && authConfig.isBasicAuthenticationSupported()) |
| | | { |
| | | clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED, false, |
| | | ERR_CONNHANDLER_DISALLOWED_CLIENT.get(clientConnection |
| | | .getClientHostPort(), clientConnection.getServerHostPort())); |
| | | return false; |
| | | response.getHeaders().add("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\""); |
| | | } |
| | | return true; |
| | | return Promises.newResultPromise(response); |
| | | } |
| | | |
| | | /** |
| | |
| | | * @throws ResourceException |
| | | * if any error occur |
| | | */ |
| | | String[] extractUsernamePassword(HttpServletRequest request) |
| | | throws ResourceException |
| | | String[] extractUsernamePassword(Request request) throws ResourceException |
| | | { |
| | | // TODO Use session to reduce hits with search + bind? |
| | | // Use proxied authorization control for session. |
| | |
| | | // Security: How can we remove the password held in the request headers? |
| | | if (authConfig.isCustomHeadersAuthenticationSupported()) |
| | | { |
| | | final String userName = |
| | | request.getHeader(authConfig.getCustomHeaderUsername()); |
| | | final String password = |
| | | request.getHeader(authConfig.getCustomHeaderPassword()); |
| | | final String userName = request.getHeaders().getFirst(authConfig.getCustomHeaderUsername()); |
| | | final String password = request.getHeaders().getFirst(authConfig.getCustomHeaderPassword()); |
| | | if (userName != null && password != null) |
| | | { |
| | | return new String[] { userName, password }; |
| | |
| | | |
| | | if (authConfig.isBasicAuthenticationSupported()) |
| | | { |
| | | String httpBasicAuthHeader = request.getHeader(HTTP_BASIC_AUTH_HEADER); |
| | | String httpBasicAuthHeader = request.getHeaders().getFirst(HTTP_BASIC_AUTH_HEADER); |
| | | if (httpBasicAuthHeader != null) |
| | | { |
| | | String[] userCredentials = parseUsernamePassword(httpBasicAuthHeader); |
| | |
| | | } |
| | | |
| | | /** |
| | | * Sends an error response back to the client. If the error response is |
| | | * "Unauthorized", then it will send a challenge for HTTP Basic authentication |
| | | * if HTTP Basic authentication is enabled. |
| | | * |
| | | * @param response |
| | | * where to send the Unauthorized status code. |
| | | * @param prettyPrint |
| | | * whether to format the JSON document output |
| | | * @param re |
| | | * the resource exception with the error response content |
| | | */ |
| | | void sendErrorReponse(HttpServletResponse response, boolean prettyPrint, |
| | | ResourceException re) |
| | | { |
| | | response.setStatus(re.getCode()); |
| | | |
| | | if (re.getCode() == HttpServletResponse.SC_UNAUTHORIZED |
| | | && authConfig.isBasicAuthenticationSupported()) |
| | | { |
| | | response.setHeader("WWW-Authenticate", |
| | | "Basic realm=\"org.forgerock.opendj\""); |
| | | } |
| | | |
| | | try |
| | | { |
| | | // Send error JSON document out |
| | | response.setHeader("Content-Type", "application/json"); |
| | | response.getOutputStream().println(toJSON(prettyPrint, re)); |
| | | } |
| | | catch (IOException ignore) |
| | | { |
| | | // nothing else we can do in this case |
| | | logger.traceException(ignore); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Returns a JSON representation of the {@link ResourceException}. |
| | | * |
| | | * @param prettyPrint |
| | | * whether to format the resulting JSON document |
| | | * @param re |
| | | * the resource exception to convert to a JSON document |
| | | * @return a String containing the JSON representation of the |
| | | * {@link ResourceException}. |
| | | */ |
| | | private String toJSON(boolean prettyPrint, ResourceException re) |
| | | { |
| | | final String indent = "\n "; |
| | | final StringBuilder sb = new StringBuilder(); |
| | | sb.append("{"); |
| | | if (prettyPrint) { |
| | | sb.append(indent); |
| | | } |
| | | sb.append("\"code\": ").append(re.getCode()).append(","); |
| | | if (prettyPrint) { |
| | | sb.append(indent); |
| | | } |
| | | sb.append("\"message\": \"").append(re.getMessage()).append("\","); |
| | | if (prettyPrint) { |
| | | sb.append(indent); |
| | | } |
| | | sb.append("\"reason\": \"").append(re.getReason()).append("\""); |
| | | if (prettyPrint) { |
| | | sb.append("\n"); |
| | | } |
| | | sb.append("}"); |
| | | return sb.toString(); |
| | | } |
| | | |
| | | /** |
| | | * Parses username and password from the authentication header used in HTTP |
| | | * basic authentication. |
| | | * |
| | |
| | | // We received authentication info |
| | | // Example received header: |
| | | // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" |
| | | String base64UserPassword = authHeader.substring("basic".length() + 1); |
| | | String base64UserCredentials = authHeader.substring("basic".length() + 1); |
| | | try |
| | | { |
| | | // Example usage of base64: |
| | | // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==" |
| | | String userCredentials = new String(Base64.decode(base64UserPassword)); |
| | | String userCredentials = new String(Base64.decode(base64UserCredentials)); |
| | | String[] split = userCredentials.split(":"); |
| | | if (split.length == 2) |
| | | { |
| | |
| | | return null; |
| | | } |
| | | |
| | | private AsyncContext getAsyncContext(ServletRequest request) |
| | | { |
| | | return request.isAsyncStarted() ? request.getAsyncContext() : request |
| | | .startAsync(); |
| | | } |
| | | |
| | | private SearchRequest buildSearchRequest(String userName) |
| | | { |
| | | // use configured rights to find the user DN |
| | | final Filter filter = |
| | | Filter.format(authConfig.getSearchFilterTemplate(), userName); |
| | | return Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig |
| | | .getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES); |
| | | } |
| | | |
| | | /** {@inheritDoc} */ |
| | | @Override |
| | | public void destroy() |
| | | { |
| | | // nothing to do |
| | | } |
| | | public void close() throws IOException {} |
| | | } |
| | |
| | | */ |
| | | package org.opends.server.protocols.http; |
| | | |
| | | import static org.forgerock.opendj.adapter.server3x.Converters.from; |
| | | import static org.forgerock.opendj.adapter.server3x.Converters.getResponseResult; |
| | | import static org.forgerock.opendj.ldap.LdapException.newLdapException; |
| | | import static org.opends.messages.ProtocolMessages.WARN_CLIENT_DISCONNECT_IN_PROGRESS; |
| | | import static org.opends.server.loggers.AccessLogger.logDisconnect; |
| | | |
| | | import java.net.InetAddress; |
| | | import java.net.UnknownHostException; |
| | | import java.util.ArrayList; |
| | |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.concurrent.atomic.AtomicLong; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | |
| | | import org.forgerock.http.Context; |
| | | import org.forgerock.http.MutableUri; |
| | | import org.forgerock.http.context.ClientContext; |
| | | import org.forgerock.http.context.AttributesContext; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.LocalizableMessageBuilder; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.spi.LdapPromiseImpl; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldap.spi.LdapPromiseImpl; |
| | | import org.opends.server.api.ClientConnection; |
| | | import org.opends.server.core.AddOperation; |
| | | import org.opends.server.core.BindOperation; |
| | |
| | | import org.opends.server.types.SearchResultEntry; |
| | | import org.opends.server.types.SearchResultReference; |
| | | |
| | | import static org.forgerock.opendj.adapter.server3x.Converters.*; |
| | | import static org.forgerock.opendj.ldap.LdapException.*; |
| | | import static org.opends.messages.ProtocolMessages.*; |
| | | import static org.opends.server.loggers.AccessLogger.*; |
| | | |
| | | /** |
| | | * This class defines an HTTP client connection, which is a type of client |
| | | * connection that will be accepted by an instance of the HTTP connection |
| | |
| | | private final InetAddress localAddress; |
| | | |
| | | /** Whether this connection is secure. */ |
| | | private final boolean isSecure; |
| | | private boolean isSecure; |
| | | |
| | | /** Security-Strength Factor extracted from the request attribute. */ |
| | | private final int securityStrengthFactor; |
| | |
| | | * |
| | | * @param connectionHandler |
| | | * the connection handler that accepted this connection |
| | | * @param request |
| | | * represents this client connection. |
| | | * @param context |
| | | * represents the context of this client connection. |
| | | */ |
| | | public HTTPClientConnection(HTTPConnectionHandler connectionHandler, HttpServletRequest request) |
| | | public HTTPClientConnection(HTTPConnectionHandler connectionHandler, Context context, Request request) |
| | | { |
| | | this.connectionHandler = connectionHandler; |
| | | |
| | | final ClientContext clientCtx = context.asContext(ClientContext.class); |
| | | // Memorize all the fields we need from the request before Grizzly decides to recycle it |
| | | this.clientAddress = request.getRemoteAddr(); |
| | | this.clientPort = request.getRemotePort(); |
| | | this.serverAddress = request.getLocalAddr(); |
| | | this.serverPort = request.getLocalPort(); |
| | | this.remoteAddress = toInetAddress(request.getRemoteAddr()); |
| | | this.localAddress = toInetAddress(request.getLocalAddr()); |
| | | this.isSecure = request.isSecure(); |
| | | this.securityStrengthFactor = calcSSF(request.getAttribute(SERVLET_SSF_CONSTANT)); |
| | | this.clientAddress = clientCtx.getRemoteAddress(); |
| | | this.remoteAddress = toInetAddress(clientAddress); |
| | | this.clientPort = clientCtx.getRemotePort(); |
| | | this.isSecure = clientCtx.isSecure(); |
| | | |
| | | final MutableUri uri = request.getUri(); |
| | | this.serverAddress = uri.getHost(); |
| | | this.localAddress = toInetAddress(serverAddress); |
| | | this.serverPort = uri.getPort(); |
| | | this.securityStrengthFactor = calcSSF( |
| | | context.asContext(AttributesContext.class).getAttributes().get(SERVLET_SSF_CONSTANT)); |
| | | this.method = request.getMethod(); |
| | | this.query = computeQuery(request); |
| | | this.protocol = request.getProtocol(); |
| | | this.userAgent = request.getHeader("User-Agent"); |
| | | this.query = uri.getQuery(); |
| | | this.protocol = request.getVersion(); |
| | | this.userAgent = clientCtx.getUserAgent(); |
| | | |
| | | this.statTracker = this.connectionHandler.getStatTracker(); |
| | | |
| | |
| | | this.connectionID = DirectoryServer.newConnectionAccepted(this); |
| | | } |
| | | |
| | | private String computeQuery(HttpServletRequest request) |
| | | { |
| | | if (request.getQueryString() != null) |
| | | { |
| | | return request.getRequestURI() + "?" + request.getQueryString(); |
| | | } |
| | | return request.getRequestURI(); |
| | | } |
| | | |
| | | @Override |
| | | public String getAuthUser() |
| | | { |
| | |
| | | */ |
| | | package org.opends.server.protocols.http; |
| | | |
| | | import static org.opends.messages.ConfigMessages.*; |
| | | import static org.opends.messages.ConfigMessages.WARN_CONFIG_LOGGER_NO_ACTIVE_HTTP_ACCESS_LOGGERS; |
| | | import static org.opends.messages.ProtocolMessages.*; |
| | | import static org.opends.server.util.ServerConstants.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | import static org.opends.server.util.ServerConstants.ALERT_DESCRIPTION_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES; |
| | | import static org.opends.server.util.ServerConstants.ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES; |
| | | import static org.opends.server.util.StaticUtils.getExceptionMessage; |
| | | import static org.opends.server.util.StaticUtils.isAddressInUse; |
| | | import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; |
| | | |
| | | import java.io.File; |
| | | import javax.net.ssl.KeyManager; |
| | | import javax.net.ssl.SSLContext; |
| | | import javax.net.ssl.SSLEngine; |
| | | import java.io.IOException; |
| | | import java.net.InetAddress; |
| | | import java.util.*; |
| | | import java.util.Arrays; |
| | | import java.util.Collection; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.logging.Level; |
| | | import java.util.logging.Logger; |
| | | |
| | | import javax.net.ssl.KeyManager; |
| | | import javax.net.ssl.SSLContext; |
| | | import javax.net.ssl.SSLEngine; |
| | | import javax.servlet.DispatcherType; |
| | | import javax.servlet.Filter; |
| | | import javax.servlet.ServletException; |
| | | |
| | | import org.codehaus.jackson.JsonParseException; |
| | | import org.codehaus.jackson.JsonParser; |
| | | import org.codehaus.jackson.map.JsonMappingException; |
| | | import org.codehaus.jackson.map.ObjectMapper; |
| | | import org.forgerock.http.servlet.HttpFrameworkServlet; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.ConnectionFactory; |
| | | import org.forgerock.json.resource.Resources; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.json.resource.servlet.HttpServlet; |
| | | import org.forgerock.opendj.config.server.ConfigChangeResult; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.rest2ldap.AuthorizationPolicy; |
| | | import org.forgerock.opendj.rest2ldap.Rest2LDAP; |
| | | import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory; |
| | | import org.glassfish.grizzly.http.HttpProbe; |
| | | import org.glassfish.grizzly.http.server.HttpServer; |
| | | import org.glassfish.grizzly.http.server.NetworkListener; |
| | |
| | | import org.opends.server.admin.server.ConfigurationChangeListener; |
| | | import org.opends.server.admin.std.server.ConnectionHandlerCfg; |
| | | import org.opends.server.admin.std.server.HTTPConnectionHandlerCfg; |
| | | import org.opends.server.api.*; |
| | | import org.opends.server.api.AlertGenerator; |
| | | import org.opends.server.api.ClientConnection; |
| | | import org.opends.server.api.ConnectionHandler; |
| | | import org.opends.server.api.KeyManagerProvider; |
| | | import org.opends.server.api.ServerShutdownListener; |
| | | import org.opends.server.api.TrustManagerProvider; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.extensions.NullKeyManagerProvider; |
| | | import org.opends.server.extensions.NullTrustManagerProvider; |
| | | import org.opends.server.loggers.HTTPAccessLogger; |
| | | import org.opends.server.monitors.ClientConnectionMonitorProvider; |
| | | import org.opends.server.types.*; |
| | | import org.opends.server.types.DN; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.HostPort; |
| | | import org.opends.server.types.InitializationException; |
| | | import org.opends.server.util.SelectableCertificateKeyManager; |
| | | import org.opends.server.util.StaticUtils; |
| | | |
| | |
| | | /** SSL instance name used in context creation. */ |
| | | private static final String SSL_CONTEXT_INSTANCE_NAME = "TLS"; |
| | | |
| | | private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(JsonParser.Feature.ALLOW_COMMENTS, true); |
| | | |
| | | /** The initialization configuration. */ |
| | | private HTTPConnectionHandlerCfg initConfig; |
| | | |
| | |
| | | |
| | | private void createAndRegisterServlet(final String servletName, final String... urlPatterns) throws Exception |
| | | { |
| | | // Parse and use JSON config |
| | | File jsonConfigFile = getFileForPath(this.currentConfig.getConfigFile()); |
| | | final JsonValue configuration = parseJsonConfiguration(jsonConfigFile).recordKeyAccesses(); |
| | | final HTTPAuthenticationConfig authenticationConfig = getAuthenticationConfig(configuration); |
| | | final ConnectionFactory connFactory = getConnectionFactory(configuration); |
| | | configuration.verifyAllKeysAccessed(); |
| | | |
| | | Filter filter = new CollectClientConnectionsFilter(this, authenticationConfig); |
| | | // Used for hooking our HTTPClientConnection in Rest2LDAP |
| | | final HttpServlet servlet = new HttpServlet(connFactory, Rest2LDAPContextFactory.getHttpServletContextFactory()); |
| | | |
| | | // Create and deploy the Web app context |
| | | final WebappContext ctx = new WebappContext(servletName); |
| | | ctx.addFilter("collectClientConnections", filter) |
| | | .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, urlPatterns); |
| | | ctx.addServlet(servletName, servlet).addMapping(urlPatterns); |
| | | ctx.addServlet(servletName, new HttpFrameworkServlet(new LdapHttpApplication(this))).addMapping(urlPatterns); |
| | | ctx.deploy(this.httpServer); |
| | | } |
| | | |
| | | private HTTPAuthenticationConfig getAuthenticationConfig(final JsonValue configuration) |
| | | { |
| | | final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig(); |
| | | |
| | | final JsonValue val = configuration.get("authenticationFilter"); |
| | | result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication")); |
| | | result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication")); |
| | | result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader").asString()); |
| | | result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader").asString()); |
| | | |
| | | final String searchBaseDN = asString(val, "searchBaseDN"); |
| | | result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN)); |
| | | result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope"))); |
| | | result.setSearchFilterTemplate(asString(val, "searchFilterTemplate")); |
| | | |
| | | return result; |
| | | } |
| | | |
| | | private String asString(JsonValue value, String key) |
| | | { |
| | | return value.get(key).required().asString(); |
| | | } |
| | | |
| | | private boolean asBool(JsonValue value, String key) |
| | | { |
| | | return value.get(key).defaultTo(false).asBoolean(); |
| | | } |
| | | |
| | | private ConnectionFactory getConnectionFactory(final JsonValue configuration) |
| | | { |
| | | final Router router = new Router(); |
| | | final JsonValue mappings = configuration.get("servlet").get("mappings").required(); |
| | | for (final String mappingUrl : mappings.keys()) |
| | | { |
| | | final JsonValue mapping = mappings.get(mappingUrl); |
| | | final CollectionResourceProvider provider = Rest2LDAP.builder() |
| | | .authorizationPolicy(AuthorizationPolicy.REUSE) |
| | | .configureMapping(mapping).build(); |
| | | router.addRoute(mappingUrl, provider); |
| | | } |
| | | return Resources.newInternalConnectionFactory(router); |
| | | } |
| | | |
| | | private JsonValue parseJsonConfiguration(File configFile) |
| | | throws IOException, JsonParseException, JsonMappingException, ServletException |
| | | { |
| | | // Parse the config file. |
| | | final Object content = JSON_MAPPER.readValue(configFile, Object.class); |
| | | if (!(content instanceof Map)) |
| | | { |
| | | throw new ServletException( |
| | | "Servlet configuration file '" + configFile + "' does not contain a valid JSON configuration"); |
| | | } |
| | | return new JsonValue(content); |
| | | } |
| | | |
| | | private void stopHttpServer() |
| | | { |
| | | if (this.httpServer != null) |
| New file |
| | |
| | | /* |
| | | * CDDL HEADER START |
| | | * |
| | | * The contents of this file are subject to the terms of the |
| | | * Common Development and Distribution License, Version 1.0 only |
| | | * (the "License"). You may not use this file except in compliance |
| | | * with the License. |
| | | * |
| | | * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt |
| | | * or http://forgerock.org/license/CDDLv1.0.html. |
| | | * See the License for the specific language governing permissions |
| | | * and limitations under the License. |
| | | * |
| | | * When distributing Covered Code, include this CDDL HEADER in each |
| | | * file and include the License file at legal-notices/CDDLv1_0.txt. |
| | | * If applicable, add the following below this CDDL HEADER, with the |
| | | * fields enclosed by brackets "[]" replaced with your own identifying |
| | | * information: |
| | | * Portions Copyright [yyyy] [name of copyright owner] |
| | | * |
| | | * CDDL HEADER END |
| | | * |
| | | * |
| | | * Copyright 2015 ForgeRock AS |
| | | */ |
| | | package org.opends.server.protocols.http; |
| | | |
| | | import static org.forgerock.util.Utils.closeSilently; |
| | | import static org.opends.server.util.StaticUtils.getFileForPath; |
| | | |
| | | import java.io.File; |
| | | import java.io.FileReader; |
| | | import java.util.Map; |
| | | |
| | | import org.forgerock.http.Context; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.HttpApplication; |
| | | import org.forgerock.http.HttpApplicationException; |
| | | import org.forgerock.http.handler.Handlers; |
| | | import org.forgerock.http.io.Buffer; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.http.util.Json; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.json.resource.http.CrestHttp; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.rest2ldap.AuthorizationPolicy; |
| | | import org.forgerock.opendj.rest2ldap.Rest2LDAP; |
| | | import org.forgerock.util.Factory; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.opends.messages.ProtocolMessages; |
| | | |
| | | /** Main class of the HTTP Connection Handler web application */ |
| | | class LdapHttpApplication implements HttpApplication |
| | | { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** Http Handler which sets a connection to an OpenDJ server. */ |
| | | private static class LdapHttpHandler implements Handler |
| | | { |
| | | |
| | | private final Handler delegate; |
| | | |
| | | /** |
| | | * Build a new {@code LdapHttpHandler}. |
| | | * |
| | | * @param configuration |
| | | * The configuration which will be used to set |
| | | * the connection and the mappings to the OpenDJ server. |
| | | */ |
| | | public LdapHttpHandler(final JsonValue configuration) |
| | | { |
| | | delegate = CrestHttp.newHttpHandler(createRouter(configuration)); |
| | | } |
| | | |
| | | private RequestHandler createRouter(final JsonValue configuration) |
| | | { |
| | | final JsonValue mappings = configuration.get("servlet").get("mappings").required(); |
| | | final Router router = new Router(); |
| | | |
| | | for (final String mappingUrl : mappings.keys()) { |
| | | final JsonValue mapping = mappings.get(mappingUrl); |
| | | final CollectionResourceProvider provider = Rest2LDAP.builder() |
| | | .authorizationPolicy(AuthorizationPolicy.REUSE) |
| | | .configureMapping(mapping) |
| | | .build(); |
| | | router.addRoute(Router.uriTemplate(mappingUrl), provider); |
| | | } |
| | | return router; |
| | | } |
| | | @Override |
| | | public final Promise<Response, NeverThrowsException> handle(final Context context, final Request request) |
| | | { |
| | | return delegate.handle(context, request); |
| | | } |
| | | |
| | | } |
| | | |
| | | private HTTPConnectionHandler connectionHandler; |
| | | private LdapHttpHandler handler; |
| | | private CollectClientConnectionsFilter filter; |
| | | |
| | | LdapHttpApplication(HTTPConnectionHandler connectionHandler) |
| | | { |
| | | this.connectionHandler = connectionHandler; |
| | | } |
| | | |
| | | @Override |
| | | public Handler start() throws HttpApplicationException |
| | | { |
| | | try |
| | | { |
| | | final File configFile = getFileForPath(connectionHandler.getCurrentConfig().getConfigFile()); |
| | | final Map<String, Object> jsonElems = Json.readJsonLenient(new FileReader(configFile)); |
| | | final JsonValue configuration = new JsonValue(jsonElems).recordKeyAccesses(); |
| | | handler = new LdapHttpHandler(configuration); |
| | | filter = new CollectClientConnectionsFilter(connectionHandler, getAuthenticationConfig(configuration)); |
| | | configuration.verifyAllKeysAccessed(); |
| | | return Handlers.chainOf(handler, filter); |
| | | } |
| | | catch (final Exception e) |
| | | { |
| | | final LocalizableMessage errorMsg = ProtocolMessages.ERR_INITIALIZE_HTTP_CONNECTION_HANDLER.get(); |
| | | logger.error(errorMsg, e); |
| | | stop(); |
| | | throw new HttpApplicationException(errorMsg.toString(), e); |
| | | } |
| | | } |
| | | |
| | | private HTTPAuthenticationConfig getAuthenticationConfig(final JsonValue configuration) |
| | | { |
| | | final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig(); |
| | | |
| | | final JsonValue val = configuration.get("authenticationFilter"); |
| | | result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication")); |
| | | result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication")); |
| | | result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader").asString()); |
| | | result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader").asString()); |
| | | |
| | | final String searchBaseDN = asString(val, "searchBaseDN"); |
| | | result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN)); |
| | | result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope"))); |
| | | result.setSearchFilterTemplate(asString(val, "searchFilterTemplate")); |
| | | |
| | | return result; |
| | | } |
| | | |
| | | private String asString(JsonValue value, String key) |
| | | { |
| | | return value.get(key).required().asString(); |
| | | } |
| | | |
| | | private boolean asBool(JsonValue value, String key) |
| | | { |
| | | return value.get(key).defaultTo(false).asBoolean(); |
| | | } |
| | | |
| | | @Override |
| | | public Factory<Buffer> getBufferFactory() |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public void stop() |
| | | { |
| | | closeSilently(filter); |
| | | handler = null; |
| | | filter = null; |
| | | } |
| | | } |
| | |
| | | Verify that the keystore is properly configured |
| | | ERR_INVALID_KEYSTORE_1527=No usable key was found for '%s'. Verify the keystore content |
| | | INFO_DISABLE_CONNECTION_1528=Disabling %s |
| | | ERR_INITIALIZE_HTTP_CONNECTION_HANDLER_1529=Failed to initialize Http Connection Handler |
| | |
| | | package org.opends.server.protocols.http; |
| | | |
| | | import static org.assertj.core.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | import static org.opends.server.protocols.http.CollectClientConnectionsFilter.*; |
| | | |
| | | import java.io.IOException; |
| | | |
| | | import javax.servlet.ServletOutputStream; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | |
| | | import org.assertj.core.api.SoftAssertions; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.util.Base64; |
| | | import org.testng.annotations.BeforeMethod; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | |
| | | private static final String PASSWORD = "open sesame"; |
| | | private static final String BASE64_USERPASS = Base64.encode((USERNAME + ":" + PASSWORD).getBytes()); |
| | | |
| | | private HTTPAuthenticationConfig authConfig = new HTTPAuthenticationConfig(); |
| | | private HTTPAuthenticationConfig authConfig; |
| | | private CollectClientConnectionsFilter filter; |
| | | |
| | | private CollectClientConnectionsFilter filter = new CollectClientConnectionsFilter(null, authConfig); |
| | | @BeforeMethod |
| | | private void createConfigAndFilter() |
| | | { |
| | | authConfig = new HTTPAuthenticationConfig(); |
| | | filter = new CollectClientConnectionsFilter(null, authConfig); |
| | | } |
| | | |
| | | @DataProvider(name = "Invalid HTTP basic auth strings") |
| | | public Object[][] getInvalidHttpBasicAuthStrings() |
| | |
| | | public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent() throws Exception |
| | | { |
| | | authConfig.setBasicAuthenticationSupported(true); |
| | | final Response response = sendUnauthorizedResponseWithHTTPBasicAuthChallenge(); |
| | | |
| | | ServletOutputStream oStream = mock(ServletOutputStream.class); |
| | | HttpServletResponse response = mock(HttpServletResponse.class); |
| | | when(response.getOutputStream()).thenReturn(oStream); |
| | | sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response); |
| | | |
| | | verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
| | | verify(response).setHeader("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\""); |
| | | verifyUnauthorizedOutputMessage(response, oStream); |
| | | assertThat(response.getHeaders().getFirst("WWW-Authenticate")).isEqualTo("Basic realm=\"org.forgerock.opendj\""); |
| | | verifyUnauthorizedOutputMessage(response); |
| | | } |
| | | |
| | | @Test |
| | | public void sendUnauthorizedResponseWithoutHttpBasicAuthWillNotChallengeUserAgent() throws Exception |
| | | { |
| | | authConfig.setBasicAuthenticationSupported(true); |
| | | authConfig.setBasicAuthenticationSupported(false); |
| | | final Response response = sendUnauthorizedResponseWithHTTPBasicAuthChallenge(); |
| | | |
| | | HttpServletResponse response = mock(HttpServletResponse.class); |
| | | ServletOutputStream oStream = mock(ServletOutputStream.class); |
| | | when(response.getOutputStream()).thenReturn(oStream); |
| | | sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response); |
| | | |
| | | verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
| | | verifyUnauthorizedOutputMessage(response, oStream); |
| | | assertThat(response.getHeaders().getFirst("WWW-Authenticate")).isNull(); |
| | | verifyUnauthorizedOutputMessage(response); |
| | | } |
| | | |
| | | private void sendUnauthorizedResponseWithHTTPBasicAuthChallenge(HttpServletResponse response) |
| | | private Response sendUnauthorizedResponseWithHTTPBasicAuthChallenge() throws Exception |
| | | { |
| | | filter.sendErrorReponse( |
| | | response, true, ResourceException.getException(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials")); |
| | | return filter.resourceExceptionToPromise(ResourceException.getException(401, "Invalid Credentials")).get(); |
| | | } |
| | | |
| | | private void verifyUnauthorizedOutputMessage(HttpServletResponse response, ServletOutputStream oStream) |
| | | throws IOException |
| | | private void verifyUnauthorizedOutputMessage(Response response) throws IOException |
| | | { |
| | | verify(response).getOutputStream(); |
| | | verify(oStream).println( |
| | | "{\n" + " \"code\": 401,\n" |
| | | + " \"message\": \"Invalid Credentials\",\n" |
| | | + " \"reason\": \"Unauthorized\"\n" + "}"); |
| | | final SoftAssertions softly = new SoftAssertions(); |
| | | softly.assertThat(response.getStatus().getCode()).isEqualTo(401); |
| | | softly.assertThat(response.getStatus().getReasonPhrase()).isEqualTo("Unauthorized"); |
| | | softly.assertThat(response.getEntity().getJson().toString()).isEqualTo( |
| | | "{code=401, reason=Unauthorized, message=Invalid Credentials}"); |
| | | softly.assertAll(); |
| | | } |
| | | |
| | | @Test |
| | |
| | | { |
| | | authConfig.setBasicAuthenticationSupported(true); |
| | | |
| | | HttpServletRequest request = mock(HttpServletRequest.class); |
| | | when(request.getHeader(HTTP_BASIC_AUTH_HEADER)).thenReturn("Basic " + BASE64_USERPASS); |
| | | |
| | | final Request request = new Request(); |
| | | request.getHeaders().add(HTTP_BASIC_AUTH_HEADER, "Basic " + BASE64_USERPASS); |
| | | assertThat(filter.extractUsernamePassword(request)).containsExactly(USERNAME, PASSWORD); |
| | | } |
| | | |
| | |
| | | authConfig.setCustomHeaderUsername(customHeaderUsername); |
| | | authConfig.setCustomHeaderPassword(customHeaderPassword); |
| | | |
| | | HttpServletRequest request = mock(HttpServletRequest.class); |
| | | when(request.getHeader(customHeaderUsername)).thenReturn(USERNAME); |
| | | when(request.getHeader(customHeaderPassword)).thenReturn(PASSWORD); |
| | | final Request request = new Request(); |
| | | request.getHeaders().add(customHeaderUsername, USERNAME); |
| | | request.getHeaders().add(customHeaderPassword, PASSWORD); |
| | | |
| | | assertThat(filter.extractUsernamePassword(request)).containsExactly(USERNAME, PASSWORD); |
| | | } |
| | |
| | | <i18nFrameworkVersion>1.4.2-SNAPSHOT</i18nFrameworkVersion> |
| | | <grizzlyFrameworkVersion>2.3.14</grizzlyFrameworkVersion> |
| | | <slf4jVersion>1.7.5</slf4jVersion> |
| | | <forgerockRestVersion>2.1.0-SNAPSHOT</forgerockRestVersion> |
| | | <forgerockRestVersion>3.0.0-SNAPSHOT</forgerockRestVersion> |
| | | <forgerockHttpVersion>0.0.1-SNAPSHOT</forgerockHttpVersion> |
| | | <frDocPluginVersion>3.1.0-SNAPSHOT</frDocPluginVersion> |
| | | |
| | | <!-- OSGi bundles properties --> |