/*
|
* 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 ForgeRock AS.
|
*/
|
|
package org.forgerock.opendj.rest2ldap;
|
|
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
|
import static org.forgerock.opendj.ldap.schema.CoreSchema.getEntryUUIDAttributeType;
|
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.USE_READ_ENTRY_CONTROLS;
|
import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
|
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.LinkedList;
|
import java.util.List;
|
import java.util.Set;
|
import java.util.concurrent.TimeUnit;
|
|
import org.forgerock.json.fluent.JsonValue;
|
import org.forgerock.json.resource.BadRequestException;
|
import org.forgerock.json.resource.CollectionResourceProvider;
|
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.ConnectionFactory;
|
import org.forgerock.opendj.ldap.Connections;
|
import org.forgerock.opendj.ldap.DN;
|
import org.forgerock.opendj.ldap.Entry;
|
import org.forgerock.opendj.ldap.FailoverLoadBalancingAlgorithm;
|
import org.forgerock.opendj.ldap.Filter;
|
import org.forgerock.opendj.ldap.LDAPConnectionFactory;
|
import org.forgerock.opendj.ldap.LinkedAttribute;
|
import org.forgerock.opendj.ldap.RDN;
|
import org.forgerock.opendj.ldap.RoundRobinLoadBalancingAlgorithm;
|
import org.forgerock.opendj.ldap.SearchScope;
|
import org.forgerock.opendj.ldap.requests.SearchRequest;
|
import org.forgerock.opendj.ldap.schema.AttributeType;
|
import org.forgerock.opendj.ldap.schema.Schema;
|
|
/**
|
* Provides core factory methods and builders for constructing LDAP resource
|
* collections.
|
*/
|
public final class Rest2LDAP {
|
// @Checkstyle:off
|
|
/**
|
* A builder for incrementally constructing LDAP resource collections.
|
*/
|
public static final class Builder {
|
private final List<Attribute> additionalLDAPAttributes = new LinkedList<Attribute>();
|
private DN baseDN; // TODO: support template variables.
|
private ConnectionFactory factory;
|
private MVCCStrategy mvccStrategy;
|
private NameStrategy nameStrategy;
|
private ReadOnUpdatePolicy readOnUpdatePolicy = USE_READ_ENTRY_CONTROLS;
|
private final ObjectAttributeMapper rootMapper = new ObjectAttributeMapper();
|
private Schema schema = Schema.getDefaultSchema();
|
|
Builder() {
|
useEtagAttribute();
|
useClientDNNaming("uid");
|
}
|
|
public Builder additionalLDAPAttribute(final Attribute attribute) {
|
additionalLDAPAttributes.add(attribute);
|
return this;
|
}
|
|
public Builder additionalLDAPAttribute(final String attribute, final Object... values) {
|
additionalLDAPAttributes.add(new LinkedAttribute(attribute, values));
|
return this;
|
}
|
|
/**
|
* Creates a mapping for the named JSON attribute.
|
*
|
* @param name
|
* The name of the JSON attribute to be mapped.
|
* @param mapper
|
* The attribute mapper responsible for mapping the JSON
|
* attribute to LDAP attribute(s).
|
* @return A reference to this builder.
|
*/
|
public Builder attribute(final String name, final AttributeMapper mapper) {
|
rootMapper.attribute(name, mapper);
|
return this;
|
}
|
|
public Builder baseDN(final DN dn) {
|
ensureNotNull(dn);
|
this.baseDN = dn;
|
return this;
|
}
|
|
public Builder baseDN(final String dn) {
|
ensureNotNull(dn);
|
this.baseDN = DN.valueOf(dn);
|
return this;
|
}
|
|
public CollectionResourceProvider build() {
|
ensureNotNull(factory);
|
ensureNotNull(baseDN);
|
if (rootMapper.isEmpty()) {
|
throw new IllegalStateException("No mappings provided");
|
}
|
return new LDAPCollectionResourceProvider(baseDN, rootMapper, factory, nameStrategy,
|
mvccStrategy, new Config(readOnUpdatePolicy, schema), additionalLDAPAttributes);
|
}
|
|
/**
|
* Configures the JSON to LDAP mapping using the provided JSON
|
* configuration. The caller is still required to set the connection
|
* factory. The configuration should look like this, excluding the
|
* C-like comments:
|
*
|
* <pre>
|
* {
|
* // The base DN beneath which LDAP entries are to be found.
|
* "baseDN" : "ou=people,dc=example,dc=com",
|
*
|
* // The mechanism which should be used for read resources during updates, must be
|
* // one of "disabled", "useReadEntryControls", or "useSearch".
|
* "readOnUpdatePolicy" : "useReadEntryControls",
|
*
|
* // Additional LDAP attributes which should be included with entries during add (create) operations.
|
* "additionalLDAPAttributes" : [
|
* {
|
* "type" : "objectClass",
|
* "values" : [
|
* "top",
|
* "person"
|
* ]
|
* }
|
* ],
|
*
|
* // The strategy which should be used for deriving LDAP entry names from JSON resources.
|
* "namingStrategy" : {
|
* // Option 1) the RDN and resource ID are both derived from a single user attribute in the entry.
|
* "strategy" : "clientDNNaming",
|
* "dnAttribute" : "uid"
|
*
|
* // Option 2) the RDN and resource ID are derived from separate user attributes in the entry.
|
* "strategy" : "clientNaming",
|
* "dnAttribute" : "uid",
|
* "idAttribute" : "mail"
|
*
|
* // Option 3) the RDN and is derived from a user attribute and the resource ID from an operational
|
* // attribute in the entry.
|
* "strategy" : "serverNaming",
|
* "dnAttribute" : "uid",
|
* "idAttribute" : "entryUUID"
|
* },
|
*
|
* // The attribute which will be used for performing MVCC.
|
* "etagAttribute" : "etag",
|
*
|
* // The JSON to LDAP attribute mappings.
|
* "attributes" : [
|
* "schemas" : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
|
* "id" : { "simple" : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
|
* "rev" : { "simple" : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
|
* "userName" : { "simple" : { "ldapAttribute" : "mail", "isSingleValued" : true, "writability" : "readOnly" } },
|
* "displayName" : { "simple" : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true } },
|
* "name" : { "object" : [
|
* "givenName" : { "simple" : { "ldapAttribute" : "givenName", "isSingleValued" : true } },
|
* "familyName" : { "simple" : { "ldapAttribute" : "sn", "isSingleValued" : true, "isRequired" : true } },
|
* ],
|
* ...
|
* ]
|
* }
|
* </pre>
|
*
|
* @param configuration
|
* The JSON configuration.
|
* @return A reference to this builder.
|
* @throws IllegalArgumentException
|
* If the configuration is invalid.
|
*/
|
public Builder configureMapping(final JsonValue configuration)
|
throws IllegalArgumentException {
|
return this;
|
}
|
|
public Builder factory(final ConnectionFactory factory) {
|
ensureNotNull(factory);
|
this.factory = factory;
|
return this;
|
}
|
|
/**
|
* Sets the policy which should be used in order to read an entry before
|
* it is deleted, or after it is added or modified.
|
*
|
* @param policy
|
* The policy which should be used in order to read an entry
|
* before it is deleted, or after it is added or modified.
|
* @return A reference to this builder.
|
*/
|
public Builder readOnUpdatePolicy(final ReadOnUpdatePolicy policy) {
|
this.readOnUpdatePolicy = ensureNotNull(policy);
|
return this;
|
}
|
|
/**
|
* Sets the schema which should be used when attribute types and
|
* controls.
|
*
|
* @param schema
|
* The schema which should be used when attribute types and
|
* controls.
|
* @return A reference to this builder.
|
*/
|
public Builder schema(final Schema schema) {
|
this.schema = ensureNotNull(schema);
|
return this;
|
}
|
|
public Builder useClientDNNaming(final AttributeType attribute) {
|
this.nameStrategy = new DNNameStrategy(attribute);
|
return this;
|
}
|
|
public Builder useClientDNNaming(final String attribute) {
|
return useClientDNNaming(Schema.getDefaultSchema().getAttributeType(attribute));
|
}
|
|
public Builder useClientNaming(final AttributeType dnAttribute,
|
final AttributeDescription idAttribute) {
|
this.nameStrategy = new AttributeNameStrategy(dnAttribute, idAttribute, false);
|
return this;
|
}
|
|
public Builder useClientNaming(final String dnAttribute, final String idAttribute) {
|
return useClientNaming(Schema.getDefaultSchema().getAttributeType(dnAttribute),
|
AttributeDescription.valueOf(idAttribute));
|
}
|
|
public Builder useEtagAttribute() {
|
return useEtagAttribute("etag");
|
}
|
|
public Builder useEtagAttribute(final AttributeDescription attribute) {
|
this.mvccStrategy = new AttributeMVCCStrategy(attribute);
|
return this;
|
}
|
|
public Builder useEtagAttribute(final String attribute) {
|
return useEtagAttribute(AttributeDescription.valueOf(attribute));
|
}
|
|
public Builder useServerEntryUUIDNaming(final AttributeType dnAttribute) {
|
return useServerNaming(dnAttribute, AttributeDescription
|
.create(getEntryUUIDAttributeType()));
|
}
|
|
public Builder useServerEntryUUIDNaming(final String dnAttribute) {
|
return useServerEntryUUIDNaming(Schema.getDefaultSchema().getAttributeType(dnAttribute));
|
}
|
|
public Builder useServerNaming(final AttributeType dnAttribute,
|
final AttributeDescription idAttribute) {
|
this.nameStrategy = new AttributeNameStrategy(dnAttribute, idAttribute, true);
|
return this;
|
}
|
|
public Builder useServerNaming(final String dnAttribute, final String idAttribute) {
|
return useServerNaming(Schema.getDefaultSchema().getAttributeType(dnAttribute),
|
AttributeDescription.valueOf(idAttribute));
|
}
|
}
|
|
private static final class AttributeMVCCStrategy extends MVCCStrategy {
|
private final AttributeDescription ldapAttribute;
|
|
private AttributeMVCCStrategy(final AttributeDescription ldapAttribute) {
|
this.ldapAttribute = ldapAttribute;
|
}
|
|
@Override
|
void getLDAPAttributes(final Context c, final Set<String> ldapAttributes) {
|
ldapAttributes.add(ldapAttribute.toString());
|
}
|
|
@Override
|
String getRevisionFromEntry(final Context c, final Entry entry) {
|
return entry.parseAttribute(ldapAttribute).asString();
|
}
|
}
|
|
private static final class AttributeNameStrategy extends NameStrategy {
|
private final AttributeDescription dnAttribute;
|
private final AttributeDescription idAttribute;
|
private final boolean isServerProvided;
|
|
private AttributeNameStrategy(final AttributeType dnAttribute,
|
final AttributeDescription idAttribute, final boolean isServerProvided) {
|
this.dnAttribute = AttributeDescription.create(dnAttribute);
|
if (dnAttribute.equals(idAttribute)) {
|
throw new IllegalArgumentException("DN and ID attributes must be different");
|
}
|
this.idAttribute = ensureNotNull(idAttribute);
|
this.isServerProvided = isServerProvided;
|
}
|
|
@Override
|
SearchRequest createSearchRequest(final Context c, 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) {
|
ldapAttributes.add(idAttribute.toString());
|
}
|
|
@Override
|
String getResourceId(final Context c, final Entry entry) {
|
return entry.parseAttribute(idAttribute).asString();
|
}
|
|
@Override
|
void setResourceId(final Context c, final DN baseDN, final String resourceId,
|
final Entry entry) throws ResourceException {
|
if (isServerProvided) {
|
if (resourceId != null) {
|
throw new BadRequestException("Resources cannot be created with a "
|
+ "client provided resource ID");
|
}
|
} else {
|
entry.addAttribute(new LinkedAttribute(idAttribute, ByteString.valueOf(resourceId)));
|
}
|
final String rdnValue = entry.parseAttribute(dnAttribute).asString();
|
final RDN rdn = new RDN(dnAttribute.getAttributeType(), rdnValue);
|
entry.setName(baseDN.child(rdn));
|
|
}
|
}
|
|
private static final class DNNameStrategy extends NameStrategy {
|
private final AttributeDescription attribute;
|
|
private DNNameStrategy(final AttributeType attribute) {
|
this.attribute = AttributeDescription.create(attribute);
|
}
|
|
@Override
|
SearchRequest createSearchRequest(final Context c, 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) {
|
ldapAttributes.add(attribute.toString());
|
}
|
|
@Override
|
String getResourceId(final Context c, final Entry entry) {
|
return entry.parseAttribute(attribute).asString();
|
}
|
|
@Override
|
void setResourceId(final Context c, final DN baseDN, final String resourceId,
|
final Entry entry) throws ResourceException {
|
if (resourceId != null) {
|
entry.setName(baseDN.child(rdn(resourceId)));
|
entry.addAttribute(new LinkedAttribute(attribute, ByteString.valueOf(resourceId)));
|
} else if (entry.getAttribute(attribute) != null) {
|
entry.setName(baseDN.child(rdn(entry.parseAttribute(attribute).asString())));
|
} else {
|
throw new BadRequestException("Resources cannot be created without a "
|
+ "client provided resource ID");
|
}
|
}
|
|
private RDN rdn(final String resourceId) {
|
return new RDN(attribute.getAttributeType(), resourceId);
|
}
|
|
}
|
|
public static Builder builder() {
|
return new Builder();
|
}
|
|
/**
|
* Creates a new connection factory using the provided JSON configuration.
|
* The configuration should look like this, excluding the C-like comments:
|
*
|
* <pre>
|
* {
|
* // The primary data center, must contain at least one LDAP server.
|
* "primaryLDAPServers" : [
|
* {
|
* "hostname" : "host1.example.com",
|
* "port" : 389
|
* },
|
* {
|
* "hostname" : "host2.example.com",
|
* "port" : 389
|
* },
|
* ],
|
*
|
* // The optional secondary (fail-over) data center.
|
* "secondaryLDAPServers" : [
|
* {
|
* "hostname" : "host3.example.com",
|
* "port" : 389
|
* },
|
* {
|
* "hostname" : "host4.example.com",
|
* "port" : 389
|
* },
|
* ],
|
*
|
* // Connection pool configuration.
|
* "connectionPoolSize" : 10,
|
* "heartBeatIntervalSeconds" : 30,
|
*
|
* // SSL/TLS configuration (optional and TBD).
|
* "useSSL" : {
|
* // Elect to use StartTLS instead of SSL.
|
* "useStartTLS" : true,
|
* ...
|
* },
|
*
|
* // Authentication configuration (mandatory and TBD).
|
* "authentication" : {
|
* ...
|
* },
|
* }
|
* </pre>
|
*
|
* @param configuration
|
* The JSON configuration.
|
* @return A new connection factory using the provided JSON configuration.
|
* @throws IllegalArgumentException
|
* If the configuration is invalid.
|
*/
|
public static ConnectionFactory connectionFactory(final JsonValue configuration)
|
throws IllegalArgumentException {
|
// Parse pool parameters,
|
final int connectionPoolSize =
|
Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1);
|
final int heartBeatIntervalSeconds =
|
Math.max(configuration.get("heartBeatIntervalSeconds").defaultTo(30).asInteger(), 1);
|
|
// Parse primary data center.
|
final JsonValue primaryLDAPServers = configuration.get("primaryLDAPServers");
|
if (primaryLDAPServers == null || !primaryLDAPServers.isList()
|
|| primaryLDAPServers.size() == 0) {
|
throw new IllegalArgumentException("No primaryLDAPServers");
|
}
|
final ConnectionFactory primary =
|
parseLDAPServers(primaryLDAPServers, connectionPoolSize, heartBeatIntervalSeconds);
|
|
// Parse secondary data center(s).
|
final JsonValue secondaryLDAPServers = configuration.get("secondaryLDAPServers");
|
final ConnectionFactory secondary;
|
if (secondaryLDAPServers != null && secondaryLDAPServers.isList()
|
&& secondaryLDAPServers.size() != 0) {
|
secondary =
|
parseLDAPServers(secondaryLDAPServers, connectionPoolSize,
|
heartBeatIntervalSeconds);
|
} else if (secondaryLDAPServers != null && !secondaryLDAPServers.isList()) {
|
throw new IllegalArgumentException("Invalid secondaryLDAPServers configuration");
|
} else {
|
secondary = null;
|
}
|
|
// Create fail-over.
|
if (secondary != null) {
|
return Connections.newLoadBalancer(new FailoverLoadBalancingAlgorithm(Arrays.asList(
|
primary, secondary), heartBeatIntervalSeconds, TimeUnit.SECONDS));
|
} else {
|
return primary;
|
}
|
}
|
|
public static AttributeMapper constant(final Object value) {
|
return new JSONConstantAttributeMapper(value);
|
}
|
|
public static ObjectAttributeMapper object() {
|
return new ObjectAttributeMapper();
|
}
|
|
public static SimpleAttributeMapper simple(final AttributeDescription attribute) {
|
return new SimpleAttributeMapper(attribute);
|
}
|
|
public static SimpleAttributeMapper simple(final String attribute) {
|
return simple(AttributeDescription.valueOf(attribute));
|
}
|
|
private static ConnectionFactory parseLDAPServers(final JsonValue config,
|
final int connectionPoolSize, final int heartBeatIntervalSeconds) {
|
final List<ConnectionFactory> servers = new ArrayList<ConnectionFactory>(config.size());
|
for (final JsonValue server : config) {
|
final String host = server.get("hostname").required().asString();
|
final int port = server.get("port").required().asInteger();
|
ConnectionFactory factory = new LDAPConnectionFactory(host, port);
|
if (connectionPoolSize > 1) {
|
factory =
|
Connections.newHeartBeatConnectionFactory(factory,
|
heartBeatIntervalSeconds, TimeUnit.SECONDS);
|
factory = Connections.newFixedConnectionPool(factory, connectionPoolSize);
|
}
|
servers.add(factory);
|
}
|
if (servers.size() > 1) {
|
return Connections.newLoadBalancer(new RoundRobinLoadBalancingAlgorithm(servers,
|
heartBeatIntervalSeconds, TimeUnit.SECONDS));
|
} else {
|
return servers.get(0);
|
}
|
}
|
|
private Rest2LDAP() {
|
// Prevent instantiation.
|
}
|
|
}
|