| | |
| | | </adm:defined> |
| | | </adm:default-behavior> |
| | | </adm:property-override> |
| | | <adm:property name="config-url" mandatory="true"> |
| | | <adm:property name="config-directory" mandatory="true"> |
| | | <adm:synopsis> |
| | | URL of the REST2LDAP configuration file. |
| | | The directory containing the Rest2Ldap configuration file(s) for this specific endpoint. |
| | | </adm:synopsis> |
| | | <adm:description> |
| | | The directory must be readable by the server and may contain multiple configuration files, one for each |
| | | supported version of the REST endpoint. If a relative path is used then it will be resolved against the server's |
| | | instance directory. |
| | | </adm:description> |
| | | <adm:syntax> |
| | | <adm:string> |
| | | <adm:pattern> |
| | | <adm:regex>.*</adm:regex> |
| | | <adm:usage>URL</adm:usage> |
| | | <adm:usage>DIRECTORY</adm:usage> |
| | | <adm:synopsis> |
| | | A URL to an existing file that is readable by the server. |
| | | A directory that is readable by the server. |
| | | </adm:synopsis> |
| | | </adm:pattern> |
| | | </adm:string> |
| | | </adm:syntax> |
| | | <adm:profile name="ldap"> |
| | | <ldap:attribute> |
| | | <ldap:name>ds-cfg-config-url</ldap:name> |
| | | <ldap:name>ds-cfg-config-directory</ldap:name> |
| | | </ldap:attribute> |
| | | </adm:profile> |
| | | </adm:property> |
| | | </adm:managed-object> |
| File was renamed from opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json |
| | |
| | | "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com" |
| | | } |
| | | } |
| | | }, |
| | | |
| | | |
| | | // The REST APIs and their LDAP attribute mappings. |
| | | "mappings": { |
| | | "/users": { |
| | | "baseDN": "ou=people,dc=example,dc=com", |
| | | "readOnUpdatePolicy": "controls", |
| | | "useSubtreeDelete": false, |
| | | "usePermissiveModify": true, |
| | | "etagAttribute": "etag", |
| | | "namingStrategy": { |
| | | "strategy": "clientDNNaming", |
| | | "dnAttribute": "uid" |
| | | }, |
| | | "additionalLDAPAttributes": [{ |
| | | "type": "objectClass", |
| | | "values": [ |
| | | "top", |
| | | "person", |
| | | "organizationalPerson", |
| | | "inetOrgPerson" |
| | | ] |
| | | }], |
| | | "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 |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "manager": { |
| | | "reference": { |
| | | "ldapAttribute": "manager", |
| | | "baseDN": "ou=people,dc=example,dc=com", |
| | | "primaryKey": "uid", |
| | | "mapper": { |
| | | "object": { |
| | | "_id": { |
| | | "simple": { |
| | | "ldapAttribute": "uid", |
| | | "isSingleValued": true, |
| | | "isRequired": true |
| | | } |
| | | }, |
| | | "displayName": { |
| | | "simple": { |
| | | "ldapAttribute": "cn", |
| | | "isSingleValued": true, |
| | | "writability": "readOnlyDiscardWrites" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "groups": { |
| | | "reference": { |
| | | "ldapAttribute": "isMemberOf", |
| | | "baseDN": "ou=groups,dc=example,dc=com", |
| | | "writability": "readOnly", |
| | | "primaryKey": "cn", |
| | | "mapper": { |
| | | "object": { |
| | | "_id": { |
| | | "simple": { |
| | | "ldapAttribute": "cn", |
| | | "isSingleValued": true |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "contactInformation": { |
| | | "object": { |
| | | "telephoneNumber": { |
| | | "simple": { |
| | | "ldapAttribute": "telephoneNumber", |
| | | "isSingleValued": true |
| | | } |
| | | }, |
| | | "emailAddress": { |
| | | "simple": { |
| | | "ldapAttribute": "mail", |
| | | "isSingleValued": true |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "meta": { |
| | | "object": { |
| | | "created": { |
| | | "simple": { |
| | | "ldapAttribute": "createTimestamp", |
| | | "isSingleValued": true, |
| | | "writability": "readOnly" |
| | | } |
| | | }, |
| | | "lastModified": { |
| | | "simple": { |
| | | "ldapAttribute": "modifyTimestamp", |
| | | "isSingleValued": true, |
| | | "writability": "readOnly" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "/groups": { |
| | | "baseDN": "ou=groups,dc=example,dc=com", |
| | | "readOnUpdatePolicy": "controls", |
| | | "useSubtreeDelete": false, |
| | | "usePermissiveModify": true, |
| | | "etagAttribute": "etag", |
| | | "namingStrategy": { |
| | | "strategy": "clientDNNaming", |
| | | "dnAttribute": "cn" |
| | | }, |
| | | "additionalLDAPAttributes": [{ |
| | | "type": "objectClass", |
| | | "values": [ |
| | | "top", |
| | | "groupOfUniqueNames" |
| | | ] |
| | | }], |
| | | "attributes": { |
| | | "schemas": { |
| | | "constant": ["urn:scim:schemas:core:1.0"] |
| | | }, |
| | | "_id": { |
| | | "simple": { |
| | | "ldapAttribute": "cn", |
| | | "isSingleValued": true, |
| | | "isRequired": true, |
| | | "writability": "createOnly" |
| | | } |
| | | }, |
| | | "_rev": { |
| | | "simple": { |
| | | "ldapAttribute": "etag", |
| | | "isSingleValued": true, |
| | | "writability": "readOnly" |
| | | } |
| | | }, |
| | | "displayName": { |
| | | "simple": { |
| | | "ldapAttribute": "cn", |
| | | "isSingleValued": true, |
| | | "isRequired": true, |
| | | "writability": "readOnly" |
| | | } |
| | | }, |
| | | "members": { |
| | | "reference": { |
| | | "ldapAttribute": "uniqueMember", |
| | | "baseDN": "dc=example,dc=com", |
| | | "primaryKey": "uid", |
| | | "mapper": { |
| | | "object": { |
| | | "_id": { |
| | | "simple": { |
| | | "ldapAttribute": "uid", |
| | | "isSingleValued": true, |
| | | "isRequired": true |
| | | } |
| | | }, |
| | | "displayName": { |
| | | "simple": { |
| | | "ldapAttribute": "cn", |
| | | "isSingleValued": true, |
| | | "writability": "readOnlyDiscardWrites" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "meta": { |
| | | "object": { |
| | | "created": { |
| | | "simple": { |
| | | "ldapAttribute": "createTimestamp", |
| | | "isSingleValued": true, |
| | | "writability": "readOnly" |
| | | } |
| | | }, |
| | | "lastModified": { |
| | | "simple": { |
| | | "ldapAttribute": "modifyTimestamp", |
| | | "isSingleValued": true, |
| | | "writability": "readOnly" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | // This file defines an example Rest2Ldap API mapping exposing a multi-tenant deployment exposing users, |
| | | // POSIX users, and groups, as follows: |
| | | // |
| | | // /api/{tenant}/users/{uid} - users for a given tenant, e.g. "/api/example/users/bjensen" |
| | | // /api/{tenant}/groups/{cn} - groups for a given tenant, e.g. "/api/example/groups/administrators" |
| | | // |
| | | "version": "1.0", |
| | | |
| | | // This section defines all of the resources, their inheritance, and relationships. |
| | | "resourceTypes": { |
| | | // This resource represents the entry point into the user/group API. It only defines sub-resources and |
| | | // does not have any properties itself. The URL and DN templates include a template variable allowing |
| | | // this API to support multi-tenancy. Multiple template variables are permitted. |
| | | "users-and-groups-v1": { |
| | | "subResources": { |
| | | "{tenant}/users": { |
| | | "type": "collection", |
| | | "dnTemplate": "ou=people,dc={tenant},dc=com", |
| | | "resource": "frapi:opendj:rest2ldap:user:1.0", |
| | | "namingStrategy": { |
| | | "type": "clientDnNaming", |
| | | "dnAttribute": "uid" |
| | | } |
| | | }, |
| | | "{tenant}/groups": { |
| | | "type": "collection", |
| | | "dnTemplate": "ou=groups,dc={tenant},dc=com", |
| | | "resource": "frapi:opendj:rest2ldap:group:1.0", |
| | | "namingStrategy": { |
| | | "type": "clientDNNaming", |
| | | "dnAttribute": "cn" |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | // This resource will act as the common parent of all resources that have a JSON representation. |
| | | "frapi:opendj:rest2ldap:object:1.0": { |
| | | "isAbstract": true, |
| | | "objectClasses": [ "top" ], |
| | | // This property will store type information in a resource's JSON representation. It is the |
| | | // equivalent of the "objectClass" attribute, except that it is single valued and will contain |
| | | // the resource name, e.g. "frapi:opendj:rest2ldap:user:1.0" or "frapi:opendj:rest2ldap:group:1.0". |
| | | "resourceTypeProperty": "_schema", |
| | | "properties": { |
| | | // Resource type property mappers store the resource's type and don't have any configuration. |
| | | "_schema": { |
| | | "type": "resourceType" |
| | | }, |
| | | "_rev": { |
| | | "type": "simple", |
| | | "ldapAttribute": "etag", |
| | | "writability": "readOnly" |
| | | }, |
| | | "_meta": { |
| | | "type": "object", |
| | | "properties": { |
| | | "created": { |
| | | "type": "simple", |
| | | "ldapAttribute": "createTimestamp", |
| | | "writability": "readOnly" |
| | | }, |
| | | "lastModified": { |
| | | "type": "simple", |
| | | "ldapAttribute": "modifyTimestamp", |
| | | "writability": "readOnly" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | // A "user" resource includes property mapping for the inetOrgPerson LDAP object class and is identified by |
| | | // the "uid" LDAP attribute. Users have a single sub-type representing users with POSIX account information. |
| | | "frapi:opendj:rest2ldap:user:1.0": { |
| | | "superType": "frapi:opendj:rest2ldap:object:1.0", |
| | | "objectClasses": [ "person", "organizationalPerson", "inetOrgPerson" ], |
| | | "supportedActions": [ "passwordModify" ], |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "uid", |
| | | "isRequired": true, |
| | | "writability": "createOnly" |
| | | }, |
| | | "userName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "mail" |
| | | }, |
| | | "displayName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "isMultiValued": true, |
| | | "isRequired": true |
| | | }, |
| | | "name": { |
| | | "type": "object", |
| | | "properties": { |
| | | "givenName": { |
| | | "type": "simple" |
| | | }, |
| | | "familyName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "sn", |
| | | "isRequired": true |
| | | } |
| | | } |
| | | }, |
| | | "description": { |
| | | "type": "simple" |
| | | }, |
| | | "manager": { |
| | | "type": "reference", |
| | | "ldapAttribute": "manager", |
| | | "baseDn": "ou=people,dc=example,dc=com", |
| | | "primaryKey": "uid", |
| | | "mapper": { |
| | | "type": "object", |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "uid", |
| | | "isRequired": true |
| | | }, |
| | | "displayName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "writability": "readOnlyDiscardWrites" |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "groups": { |
| | | "type": "reference", |
| | | "ldapAttribute": "isMemberOf", |
| | | "baseDn": "ou=groups,dc=example,dc=com", |
| | | "isMultiValued": true, |
| | | "writability": "readOnly", |
| | | "primaryKey": "cn", |
| | | "mapper": { |
| | | "type": "object", |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn" |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "contactInformation": { |
| | | "type": "object", |
| | | "properties": { |
| | | "telephoneNumber": { |
| | | "type": "simple" |
| | | }, |
| | | "emailAddress": { |
| | | "type": "simple", |
| | | "ldapAttribute": "mail" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | // A user with POSIX account information. |
| | | "frapi:opendj:rest2ldap:posixUser:1.0": { |
| | | "superType": "frapi:opendj:rest2ldap:user:1.0", |
| | | "objectClasses": [ "posixAccount" ], |
| | | "properties": { |
| | | "uidNumber": { |
| | | "type": "simple", |
| | | "isRequired": true |
| | | }, |
| | | "gidNumber": { |
| | | "type": "simple", |
| | | "isRequired": true |
| | | }, |
| | | "homeDirectory": { |
| | | "type": "simple", |
| | | "isRequired": true |
| | | }, |
| | | "loginShell": { |
| | | "type": "simple" |
| | | }, |
| | | "gecos": { |
| | | "type": "simple" |
| | | } |
| | | } |
| | | }, |
| | | // A "group" resource includes property mapping for the inetOrgPerson LDAP object class and is identified by |
| | | // the "uid" LDAP attribute. Users have a single sub-type representing users with POSIX account information. |
| | | "frapi:opendj:rest2ldap:group:1.0": { |
| | | "superType": "frapi:opendj:rest2ldap:object:1.0", |
| | | "objectClasses": [ "groupOfUniqueNames" ], |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "isRequired": true, |
| | | "writability": "createOnly" |
| | | }, |
| | | "displayName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "isRequired": true, |
| | | "writability": "readOnly" |
| | | }, |
| | | "description": { |
| | | "type": "simple" |
| | | }, |
| | | "members": { |
| | | "type": "reference", |
| | | "ldapAttribute": "uniqueMember", |
| | | "baseDn": "dc=example,dc=com", |
| | | "primaryKey": "uid", |
| | | "isMultiValued": true, |
| | | "mapper": { |
| | | "type": "object", |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "uid", |
| | | "isRequired": true |
| | | }, |
| | | "displayName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "writability": "readOnlyDiscardWrites" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | { |
| | | // Options controlling how Rest2Ldap interacts with LDAP servers. |
| | | "useMvcc": true, |
| | | "mvccAttribute": "etag", |
| | | "readOnUpdatePolicy": "controls", |
| | | "useSubtreeDelete": true, |
| | | "usePermissiveModify": true |
| | | } |
| | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException; |
| | | import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE; |
| | | import static org.forgerock.util.promise.Promises.newExceptionPromise; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | |
| | | List<Object> defaultJsonValues = emptyList(); |
| | | final AttributeDescription ldapAttributeName; |
| | | private boolean isRequired; |
| | | private boolean isSingleValued; |
| | | private boolean isMultiValued; |
| | | private WritabilityPolicy writabilityPolicy = READ_WRITE; |
| | | |
| | | AbstractLdapPropertyMapper(final AttributeDescription ldapAttributeName) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the LDAP attribute is mandatory and must be provided |
| | | * during create requests. |
| | | * Indicates that the LDAP attribute is mandatory and must be provided during create requests. |
| | | * |
| | | * @param isRequired {@code true} if this property is required. |
| | | * @return This property mapper. |
| | | */ |
| | | public final T isRequired() { |
| | | this.isRequired = true; |
| | | public final T isRequired(final boolean isRequired) { |
| | | this.isRequired = isRequired; |
| | | return getThis(); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that multi-valued LDAP attribute should be represented as a |
| | | * single-valued JSON value, rather than an array of values. |
| | | * Indicates that the LDAP attribute is multi-valued and should be represented in JSON using an array of values. |
| | | * |
| | | * @param isMultiValued {@code true} if this property is multi-valued. |
| | | * @return This property mapper. |
| | | */ |
| | | public final T isSingleValued() { |
| | | this.isSingleValued = true; |
| | | public final T isMultiValued(final boolean isMultiValued) { |
| | | this.isMultiValued = isMultiValued; |
| | | return getThis(); |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether the LDAP attribute supports updates. |
| | | * The default is {@link WritabilityPolicy#READ_WRITE}. |
| | | * Indicates whether the LDAP attribute supports updates. The default is {@link WritabilityPolicy#READ_WRITE}. |
| | | * |
| | | * @param policy |
| | | * The writability policy. |
| | |
| | | } |
| | | |
| | | boolean attributeIsSingleValued() { |
| | | return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue(); |
| | | return !isMultiValued || ldapAttributeName.getAttributeType().isSingleValue(); |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Attribute>, ResourceException> create( |
| | | final Connection connection, final JsonPointer path, final JsonValue v) { |
| | | return getNewLdapAttributes(connection, path, v).then( |
| | | Promise<List<Attribute>, ResourceException> create(final Connection connection, |
| | | final Resource resource, final JsonPointer path, |
| | | final JsonValue v) { |
| | | return getNewLdapAttributes(connection, resource, path, v).then( |
| | | new Function<Attribute, List<Attribute>, ResourceException>() { |
| | | @Override |
| | | public List<Attribute> apply(Attribute newLDAPAttribute) throws ResourceException { |
| | |
| | | return Collections.emptyList(); |
| | | } else if (newLDAPAttribute.isEmpty()) { |
| | | if (isRequired) { |
| | | throw newBadRequestException(ERR_REMOVE_REQUIRED_FIELD.get("create", path)); |
| | | throw newBadRequestException(ERR_MISSING_REQUIRED_FIELD.get(path)); |
| | | } |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | return singletonList(newLDAPAttribute); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | void getLdapAttributes(final Connection connection, final JsonPointer path, |
| | | final JsonPointer subPath, final Set<String> ldapAttributes) { |
| | | void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(ldapAttributeName.toString()); |
| | | } |
| | | |
| | | abstract Promise<Attribute, ResourceException> getNewLdapAttributes(Connection connection, JsonPointer path, |
| | | List<Object> newValues); |
| | | abstract Promise<Attribute, ResourceException> getNewLdapAttributes(Connection connection, Resource resource, |
| | | JsonPointer path, List<Object> newValues); |
| | | |
| | | abstract T getThis(); |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> patch( |
| | | final Connection connection, final JsonPointer path, final PatchOperation operation) { |
| | | Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final PatchOperation operation) { |
| | | try { |
| | | final JsonPointer field = operation.getField(); |
| | | final JsonValue v = operation.getValue(); |
| | | |
| | | /* |
| | | * Reject any attempts to patch this field if it is read-only, even |
| | | * if it is configured to discard writes. |
| | | */ |
| | | // Reject any attempts to patch this field if it is read-only, even if it is configured to discard writes. |
| | | if (!writabilityPolicy.canWrite(ldapAttributeName)) { |
| | | throw newBadRequestException(ERR_MODIFY_READ_ONLY_FIELD.get("patch", path)); |
| | | } |
| | |
| | | * LDAP attribute is multi-valued, or the attribute already |
| | | * contains a value. |
| | | */ |
| | | modType = |
| | | attributeIsSingleValued() ? ModificationType.REPLACE : ModificationType.ADD; |
| | | modType = attributeIsSingleValued() ? ModificationType.REPLACE : ModificationType.ADD; |
| | | if (newValues.isEmpty()) { |
| | | throw newBadRequestException(ERR_PATCH_ADD_NO_VALUE_FOR_FIELD.get(path.child(field.get(0)))); |
| | | } |
| | |
| | | return Promises.<List<Modification>, ResourceException> newExceptionPromise( |
| | | newBadRequestException(ERR_REMOVE_REQUIRED_FIELD.get("update", path))); |
| | | } else { |
| | | return Promises.newResultPromise( |
| | | return newResultPromise( |
| | | singletonList(new Modification(modType, emptyAttribute(ldapAttributeName)))); |
| | | } |
| | | } else { |
| | | return getNewLdapAttributes(connection, path, newValues) |
| | | return getNewLdapAttributes(connection, resource, path, newValues) |
| | | .then(new Function<Attribute, List<Modification>, ResourceException>() { |
| | | @Override |
| | | public List<Modification> apply(final Attribute value) { |
| | |
| | | }); |
| | | } |
| | | } catch (final RuntimeException e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | return asResourceException(e).asPromise(); |
| | | } catch (final ResourceException e) { |
| | | return Promises.newExceptionPromise(e); |
| | | return newExceptionPromise(e); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> update(final Connection connection, final JsonPointer path, |
| | | final Entry e, final JsonValue v) { |
| | | return getNewLdapAttributes(connection, path, v).then( |
| | | Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final Entry e, final JsonValue v) { |
| | | return getNewLdapAttributes(connection, resource, path, v).then( |
| | | new Function<Attribute, List<Modification>, ResourceException>() { |
| | | @Override |
| | | public List<Modification> apply(final Attribute newLDAPAttribute) throws ResourceException { |
| | |
| | | } |
| | | |
| | | private Promise<Attribute, ResourceException> getNewLdapAttributes(final Connection connection, |
| | | final JsonPointer path, final JsonValue v) { |
| | | final Resource resource, 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. |
| | | return Promises.newResultPromise(emptyAttribute(ldapAttributeName)); |
| | | return newResultPromise(emptyAttribute(ldapAttributeName)); |
| | | } else { |
| | | return getNewLdapAttributes(connection, path, newValues); |
| | | return getNewLdapAttributes(connection, resource, path, newValues); |
| | | } |
| | | } catch (final Exception ex) { |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | } catch (final Exception e) { |
| | | return asResourceException(e).asPromise(); |
| | | } |
| | | } |
| | | |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import org.forgerock.json.resource.ActionRequest; |
| | | import org.forgerock.json.resource.ActionResponse; |
| | | import org.forgerock.json.resource.CreateRequest; |
| | | import org.forgerock.json.resource.DeleteRequest; |
| | | import org.forgerock.json.resource.PatchRequest; |
| | | import org.forgerock.json.resource.QueryRequest; |
| | | import org.forgerock.json.resource.QueryResourceHandler; |
| | | import org.forgerock.json.resource.QueryResponse; |
| | | import org.forgerock.json.resource.ReadRequest; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.json.resource.UpdateRequest; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * An abstract base class from which request handlers may be easily implemented. The default implementation of each |
| | | * method is to return the {@link ResourceException} passed in during construction. |
| | | */ |
| | | abstract class AbstractRequestHandler implements RequestHandler { |
| | | private final ResourceException defaultErrorResponse; |
| | | |
| | | AbstractRequestHandler(final ResourceException defaultErrorResponse) { |
| | | this.defaultErrorResponse = defaultErrorResponse; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> handleAction(final Context context, final ActionRequest request) { |
| | | return defaultErrorResponse.asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, |
| | | final CreateRequest request) { |
| | | return defaultErrorResponse.asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleDelete(final Context context, |
| | | final DeleteRequest request) { |
| | | return defaultErrorResponse.asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, final PatchRequest request) { |
| | | return defaultErrorResponse.asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, |
| | | final QueryResourceHandler handler) { |
| | | return defaultErrorResponse.asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleRead(final Context context, final ReadRequest request) { |
| | | return defaultErrorResponse.asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context, |
| | | final UpdateRequest request) { |
| | | return defaultErrorResponse.asPromise(); |
| | | } |
| | | } |
| 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 2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | /** |
| | | * Represents an {@link org.forgerock.json.resource.ActionRequest action} that may be performed against a resource. |
| | | * Resources will only accept actions which have been {@link Resource#supportedAction(Action) registered} as being |
| | | * supported. |
| | | */ |
| | | public enum Action { |
| | | // Notes: |
| | | // |
| | | // - actions are likely to become an extension point in future versions of Rest2Ldap, in which case this enum |
| | | // will need to be converted into a regular class or interface, |
| | | // |
| | | // - the actions are named so that they can be parsed easily from JSON. |
| | | |
| | | /** An action that allows users to change or reset their password. */ |
| | | PASSWORDMODIFY("passwordModify"); |
| | | |
| | | private final String actionId; |
| | | |
| | | Action(final String actionId) { |
| | | this.actionId = actionId; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return actionId; |
| | | } |
| | | } |
| | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toFilter; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | |
| | | 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 property mapper which maps a single JSON attribute to a fixed value. |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path, |
| | | final JsonValue v) { |
| | | Promise<List<Attribute>, ResourceException> create(final Connection connection, |
| | | final Resource resource, final JsonPointer path, |
| | | final JsonValue v) { |
| | | if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) { |
| | | return Promises.<List<Attribute>, ResourceException> newExceptionPromise( |
| | | newBadRequestException(ERR_CREATION_READ_ONLY_FIELD.get(path))); |
| | | return newBadRequestException(ERR_CREATION_READ_ONLY_FIELD.get(path)).asPromise(); |
| | | } else { |
| | | return Promises.newResultPromise(Collections.<Attribute> emptyList()); |
| | | return newResultPromise(Collections.<Attribute> emptyList()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void getLdapAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) { |
| | | // Nothing to do. |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, |
| | | final String operator, final Object valueAssertion) { |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource, |
| | | 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 property mapper is a candidate but it does not match. |
| | | filter = alwaysFalse(); |
| | | } |
| | | return Promises.newResultPromise(filter); |
| | | return newResultPromise(filter); |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path, |
| | | final PatchOperation operation) { |
| | | return Promises.<List<Modification>, ResourceException> newExceptionPromise( |
| | | newBadRequestException(ERR_PATCH_READ_ONLY_FIELD.get(path))); |
| | | Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final PatchOperation operation) { |
| | | return newBadRequestException(ERR_PATCH_READ_ONLY_FIELD.get(path)).asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) { |
| | | return Promises.newResultPromise(value.copy()); |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final Entry e) { |
| | | return newResultPromise(value.copy()); |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> update( |
| | | final Connection connection, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final Entry e, final JsonValue v) { |
| | | if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) { |
| | | return Promises.<List<Modification>, ResourceException> newExceptionPromise( |
| | | newBadRequestException(ERR_MODIFY_READ_ONLY_FIELD.get("update", path))); |
| | | return newBadRequestException(ERR_MODIFY_READ_ONLY_FIELD.get("update", path)).asPromise(); |
| | | } else { |
| | | return Promises.newResultPromise(Collections.<Modification> emptyList()); |
| | | return newResultPromise(Collections.<Modification>emptyList()); |
| | | } |
| | | } |
| | | |
| | |
| | | return alwaysFalse(); // Not supported. |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | * |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | */ |
| | | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | |
| | | /** |
| | | * A naming strategy is responsible for naming REST resources and LDAP entries. |
| | | * A naming strategy is responsible for naming JSON resources and LDAP entries. |
| | | */ |
| | | abstract class NamingStrategy { |
| | | /* |
| | | * This interface is an abstract class so that methods can be made package |
| | | * private until API is finalized. |
| | | */ |
| | | |
| | | NamingStrategy() { |
| | | // Nothing to do. |
| | | } |
| | | |
| | | interface NamingStrategy { |
| | | /** |
| | | * Returns a search request which can be used to obtain the specified REST |
| | | * resource. |
| | | * Returns a search request which can be used to obtain the specified JSON resource. |
| | | * |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param baseDN |
| | | * The search base DN. |
| | | * @param baseDn |
| | | * The search base DN. |
| | | * @param resourceId |
| | | * The resource ID. |
| | | * @return A search request which can be used to obtain the specified REST |
| | | * resource. |
| | | * The resource ID. |
| | | * @return A search request which can be used to obtain the specified JSON resource. |
| | | */ |
| | | abstract SearchRequest createSearchRequest(Connection connection, DN baseDN, String resourceId); |
| | | SearchRequest createSearchRequest(DN baseDn, String resourceId); |
| | | |
| | | /** |
| | | * Adds the name of any LDAP attribute required by this naming strategy to the |
| | | * provided set. |
| | | * Returns the name of the LDAP attribute from which this naming strategy computes the JSON resource ID. |
| | | * |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param ldapAttributes |
| | | * The set into which any required LDAP attribute name should be |
| | | * put. |
| | | * @return The name of the LDAP attribute from which this naming strategy computes the JSON resource ID. |
| | | */ |
| | | abstract void getLdapAttributes(Connection connection, Set<String> ldapAttributes); |
| | | String getResourceIdLdapAttribute(); |
| | | |
| | | /** |
| | | * 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. |
| | | * Decodes the JSON 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 connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param entry |
| | | * The LDAP entry from which the resource ID should be obtained. |
| | | * @return The resource ID. |
| | | * The LDAP entry from which the resource ID should be obtained. |
| | | * @return The resource ID or {@code null} if the resource ID will be obtained from the resource's "_id" field. |
| | | */ |
| | | abstract String getResourceId(Connection connection, Entry entry); |
| | | String decodeResourceId(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. |
| | | * Encodes the JSON 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 connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param baseDN |
| | | * The baseDN to use when constructing the entry's DN. |
| | | * @param baseDn |
| | | * The base DN to use when constructing the entry's DN. |
| | | * @param resourceId |
| | | * The resource ID. |
| | | * The resource ID. |
| | | * @param entry |
| | | * The LDAP entry whose DN and resource ID attributes are to be |
| | | * set. |
| | | * The LDAP entry whose DN and resource ID attributes are to be set. |
| | | * @throws ResourceException |
| | | * If the resource ID cannot be determined. |
| | | * If the resource ID cannot be determined. |
| | | */ |
| | | abstract void setResourceId(Connection connection, DN baseDN, String resourceId, Entry entry) |
| | | throws ResourceException; |
| | | |
| | | void encodeResourceId(DN baseDn, String resourceId, Entry entry) throws ResourceException; |
| | | } |
| | |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.simple; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | 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.newBadRequestException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase; |
| | | import static org.forgerock.util.Utils.joinAsString; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | import java.util.AbstractMap.SimpleImmutableEntry; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.TreeSet; |
| | | |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.Pair; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | |
| | | /** An property mapper which maps JSON objects to LDAP attributes. */ |
| | | public final class ObjectPropertyMapper extends PropertyMapper { |
| | | |
| | | private static final class Mapping { |
| | | private final PropertyMapper mapper; |
| | | private final String name; |
| | |
| | | |
| | | private final Map<String, Mapping> mappings = new LinkedHashMap<>(); |
| | | |
| | | private boolean includeAllUserAttributesByDefault = false; |
| | | private final Set<String> excludedDefaultUserAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); |
| | | |
| | | ObjectPropertyMapper() { |
| | | // Nothing to do. |
| | | } |
| | | |
| | | /** |
| | | * Creates a mapping for an attribute contained in the JSON object. |
| | | * Creates an explicit mapping for a property contained in the JSON object. When user attributes are |
| | | * {@link #includeAllUserAttributesByDefault included} by default, be careful to {@link |
| | | * #excludedDefaultUserAttributes exclude} any attributes which have explicit mappings defined using this method, |
| | | * otherwise they will be duplicated in the JSON representation. |
| | | * |
| | | * @param name |
| | | * The name of the JSON attribute to be mapped. |
| | | * The name of the JSON property to be mapped. |
| | | * @param mapper |
| | | * The property mapper responsible for mapping the JSON |
| | | * attribute to LDAP attribute(s). |
| | | * The property mapper responsible for mapping the JSON attribute to LDAP attribute(s). |
| | | * @return A reference to this property mapper. |
| | | */ |
| | | public ObjectPropertyMapper attribute(final String name, final PropertyMapper mapper) { |
| | | public ObjectPropertyMapper property(final String name, final PropertyMapper mapper) { |
| | | mappings.put(toLowerCase(name), new Mapping(name, mapper)); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping |
| | | * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent |
| | | * attributes with explicit mappings being mapped twice. |
| | | * |
| | | * @param include {@code true} if all LDAP user attributes be mapped by default. |
| | | * @return A reference to this property mapper. |
| | | */ |
| | | public ObjectPropertyMapper includeAllUserAttributesByDefault(final boolean include) { |
| | | this.includeAllUserAttributesByDefault = include; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when |
| | | * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be |
| | | * excluded in order to prevent duplication. |
| | | * |
| | | * @param attributeNames The list of attributes to be excluded. |
| | | * @return A reference to this property mapper. |
| | | */ |
| | | public ObjectPropertyMapper excludedDefaultUserAttributes(final String... attributeNames) { |
| | | return excludedDefaultUserAttributes(Arrays.asList(attributeNames)); |
| | | } |
| | | |
| | | /** |
| | | * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when |
| | | * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be |
| | | * excluded in order to prevent duplication. |
| | | * |
| | | * @param attributeNames The list of attributes to be excluded. |
| | | * @return A reference to this property mapper. |
| | | */ |
| | | public ObjectPropertyMapper excludedDefaultUserAttributes(final Collection<String> attributeNames) { |
| | | excludedDefaultUserAttributes.addAll(attributeNames); |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return "object(" + mappings.values() + ")"; |
| | | return "object(" + joinAsString(", ", mappings.values()) + ")"; |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path, |
| | | final JsonValue v) { |
| | | Promise<List<Attribute>, ResourceException> create(final Connection connection, |
| | | final Resource resource, final JsonPointer path, |
| | | final JsonValue v) { |
| | | try { |
| | | /* |
| | | * 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); |
| | | // 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 = validateJsonValue(path, v); |
| | | |
| | | // Accumulate the results of the subordinate mappings. |
| | | final List<Promise<List<Attribute>, ResourceException>> promises = new ArrayList<>(); |
| | |
| | | for (final Map.Entry<String, Object> me : v.asMap().entrySet()) { |
| | | final Mapping mapping = getMapping(me.getKey()); |
| | | final JsonValue subValue = new JsonValue(me.getValue()); |
| | | promises.add(mapping.mapper.create(connection, path.child(me.getKey()), subValue)); |
| | | promises.add(mapping.mapper.create(connection, resource, path.child(me.getKey()), |
| | | subValue)); |
| | | } |
| | | } |
| | | |
| | | // Invoke mappings for which there were no values provided. |
| | | for (final Mapping mapping : missingMappings.values()) { |
| | | promises.add(mapping.mapper.create(connection, path.child(mapping.name), null)); |
| | | promises.add(mapping.mapper.create(connection, resource, path.child(mapping.name), null)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | | .then(this.<Attribute> accumulateResults()); |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | return asResourceException(e).asPromise(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void getLdapAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) { |
| | | if (subPath.isEmpty()) { |
| | | // Request all subordinate mappings. |
| | | if (includeAllUserAttributesByDefault) { |
| | | ldapAttributes.add("*"); |
| | | // Continue because there may be explicit mappings for operational attributes. |
| | | } |
| | | for (final Mapping mapping : mappings.values()) { |
| | | mapping.mapper.getLdapAttributes(connection, path.child(mapping.name), subPath, ldapAttributes); |
| | | mapping.mapper.getLdapAttributes(path.child(mapping.name), subPath, ldapAttributes); |
| | | } |
| | | } else { |
| | | // Request single subordinate mapping. |
| | | final Mapping mapping = getMapping(subPath); |
| | | final Mapping mapping = getMappingOrNull(subPath); |
| | | if (mapping != null) { |
| | | mapping.mapper.getLdapAttributes( |
| | | connection, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes); |
| | | mapping.mapper.getLdapAttributes(path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, |
| | | final String operator, final Object valueAssertion) { |
| | | final Mapping mapping = getMapping(subPath); |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final JsonPointer subPath, |
| | | final FilterType type, final String operator, |
| | | final Object valueAssertion) { |
| | | final Mapping mapping = getMappingOrNull(subPath); |
| | | if (mapping != null) { |
| | | return mapping.mapper.getLdapFilter(connection, path.child(subPath.get(0)), |
| | | subPath.relativePointer(), type, operator, valueAssertion); |
| | | return mapping.mapper.getLdapFilter(connection, |
| | | resource, |
| | | 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. |
| | | */ |
| | | return Promises.newResultPromise(alwaysFalse()); |
| | | return newResultPromise(alwaysFalse()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path, |
| | | final PatchOperation operation) { |
| | | Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource, |
| | | 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. |
| | | */ |
| | | checkMapping(path, v); |
| | | validateJsonValue(path, v); |
| | | |
| | | // Accumulate the results of the subordinate mappings. |
| | | final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>(); |
| | |
| | | final JsonValue subValue = new JsonValue(me.getValue()); |
| | | final PatchOperation subOperation = |
| | | operation(operation.getOperation(), field /* empty */, subValue); |
| | | promises.add(mapping.mapper.patch(connection, path.child(me.getKey()), subOperation)); |
| | | promises.add(mapping.mapper.patch(connection, resource, path.child(me.getKey()), subOperation)); |
| | | } |
| | | } |
| | | |
| | |
| | | * appropriate mapper. |
| | | */ |
| | | final String fieldName = field.get(0); |
| | | final Mapping mapping = getMapping(fieldName); |
| | | final Mapping mapping = getMappingOrNull(fieldName); |
| | | if (mapping == null) { |
| | | throw newBadRequestException(ERR_UNRECOGNIZED_FIELD.get(path.child(fieldName))); |
| | | } |
| | | final PatchOperation subOperation = |
| | | operation(operation.getOperation(), field.relativePointer(), v); |
| | | return mapping.mapper.patch(connection, path.child(fieldName), subOperation); |
| | | return mapping.mapper.patch(connection, resource, path.child(fieldName), subOperation); |
| | | } |
| | | } catch (final Exception ex) { |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | } catch (final Exception e) { |
| | | return asResourceException(e).asPromise(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) { |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource, |
| | | 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 List<Promise<Map.Entry<String, JsonValue>, ResourceException>> promises = |
| | | final List<Promise<Pair<String, JsonValue>, ResourceException>> promises = |
| | | new ArrayList<>(mappings.size()); |
| | | |
| | | for (final Mapping mapping : mappings.values()) { |
| | | promises.add(mapping.mapper.read(connection, 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; |
| | | } |
| | | })); |
| | | promises.add(mapping.mapper.read(connection, resource, path.child(mapping.name), e) |
| | | .then(toProperty(mapping.name))); |
| | | } |
| | | |
| | | if (includeAllUserAttributesByDefault) { |
| | | // Map all user attributes using a default simple mapping. It would be nice if we could automatically |
| | | // detect which attributes have been mapped already using explicit mappings, but it would require us to |
| | | // track which attributes have been accessed in the entry. Instead, we'll rely on the user to exclude |
| | | // attributes which have explicit mappings. |
| | | for (final Attribute attribute : e.getAllAttributes()) { |
| | | // Don't include operational attributes. They must have explicit mappings. |
| | | if (attribute.getAttributeDescription().getAttributeType().isOperational()) { |
| | | continue; |
| | | } |
| | | // Filter out excluded attributes. |
| | | final String attributeName = attribute.getAttributeDescriptionAsString(); |
| | | if (!excludedDefaultUserAttributes.isEmpty() && excludedDefaultUserAttributes.contains(attributeName)) { |
| | | continue; |
| | | } |
| | | // This attribute needs to be mapped. |
| | | final SimplePropertyMapper mapper = simple(attribute.getAttributeDescription()); |
| | | promises.add(mapper.read(connection, resource, path.child(attributeName), e) |
| | | .then(toProperty(attributeName))); |
| | | } |
| | | } |
| | | |
| | | 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. |
| | | */ |
| | | 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); |
| | | } |
| | | } |
| | | }); |
| | | .then(new Function<List<Pair<String, JsonValue>>, JsonValue, ResourceException>() { |
| | | @Override |
| | | public JsonValue apply(final List<Pair<String, JsonValue>> value) { |
| | | if (value.isEmpty()) { |
| | | // 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 Pair<String, JsonValue> e : value) { |
| | | if (e != null) { |
| | | result.put(e.getFirst(), e.getSecond().getObject()); |
| | | } |
| | | } |
| | | return new JsonValue(result); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private Function<JsonValue, Pair<String, JsonValue>, ResourceException> toProperty(final String name) { |
| | | return new Function<JsonValue, Pair<String, JsonValue>, ResourceException>() { |
| | | @Override |
| | | public Pair<String, JsonValue> apply(final JsonValue value) { |
| | | return value != null ? Pair.of(name, value) : null; |
| | | } |
| | | }; |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> update( |
| | | final Connection connection, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource, |
| | | 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. |
| | | final Map<String, Mapping> missingMappings = checkMapping(path, v); |
| | | // 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 = validateJsonValue(path, v); |
| | | |
| | | // Accumulate the results of the subordinate mappings. |
| | | final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>(); |
| | |
| | | for (final Map.Entry<String, Object> me : v.asMap().entrySet()) { |
| | | final Mapping mapping = getMapping(me.getKey()); |
| | | final JsonValue subValue = new JsonValue(me.getValue()); |
| | | promises.add(mapping.mapper.update(connection, path.child(me.getKey()), e, subValue)); |
| | | promises.add(mapping.mapper.update(connection, resource, path.child(me.getKey()), e, subValue)); |
| | | } |
| | | } |
| | | |
| | | // Invoke mappings for which there were no values provided. |
| | | for (final Mapping mapping : missingMappings.values()) { |
| | | promises.add(mapping.mapper.update(connection, path.child(mapping.name), e, null)); |
| | | promises.add(mapping.mapper.update(connection, resource, path.child(mapping.name), e, null)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | | .then(this.<Modification> accumulateResults()); |
| | | } catch (final Exception ex) { |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | return asResourceException(ex).asPromise(); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** 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 { |
| | | private Map<String, Mapping> validateJsonValue(final JsonPointer path, final JsonValue v) throws ResourceException { |
| | | final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings); |
| | | if (v != null && !v.isNull()) { |
| | | if (v.isMap()) { |
| | | for (final String attribute : v.asMap().keySet()) { |
| | | if (missingMappings.remove(toLowerCase(attribute)) == null) { |
| | | if (missingMappings.remove(toLowerCase(attribute)) == null |
| | | && !isIncludedDefaultUserAttribute(attribute)) { |
| | | throw newBadRequestException(ERR_UNRECOGNIZED_FIELD.get(path.child(attribute))); |
| | | } |
| | | } |
| | |
| | | return missingMappings; |
| | | } |
| | | |
| | | private Mapping getMapping(final JsonPointer jsonAttribute) { |
| | | return jsonAttribute.isEmpty() ? null : getMapping(jsonAttribute.get(0)); |
| | | private Mapping getMappingOrNull(final JsonPointer jsonAttribute) { |
| | | return jsonAttribute.isEmpty() ? null : getMappingOrNull(jsonAttribute.get(0)); |
| | | } |
| | | |
| | | private Mapping getMappingOrNull(final String jsonAttribute) { |
| | | final Mapping mapping = mappings.get(toLowerCase(jsonAttribute)); |
| | | if (mapping != null) { |
| | | return mapping; |
| | | } |
| | | if (isIncludedDefaultUserAttribute(jsonAttribute)) { |
| | | return new Mapping(jsonAttribute, simple(jsonAttribute)); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Mapping getMapping(final String jsonAttribute) { |
| | | return mappings.get(toLowerCase(jsonAttribute)); |
| | | final Mapping mappingOrNull = getMappingOrNull(jsonAttribute); |
| | | if (mappingOrNull != null) { |
| | | return mappingOrNull; |
| | | } |
| | | throw new IllegalStateException("Unexpected null mapping for jsonAttribute: " + jsonAttribute); |
| | | } |
| | | |
| | | private boolean isIncludedDefaultUserAttribute(final String attributeName) { |
| | | return includeAllUserAttributesByDefault |
| | | && (excludedDefaultUserAttributes.isEmpty() || !excludedDefaultUserAttributes.contains(attributeName)); |
| | | } |
| | | } |
| | |
| | | * |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param resource The exact type of resource being created. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * property mapper. This may be used when constructing error |
| | |
| | | * in the resource. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract Promise<List<Attribute>, ResourceException> create(Connection connection, JsonPointer path, JsonValue v); |
| | | abstract Promise<List<Attribute>, ResourceException> create(Connection connection, Resource resource, |
| | | JsonPointer path, JsonValue v); |
| | | |
| | | /** |
| | | * Adds the names of the LDAP attributes required by this property mapper |
| | |
| | | * Implementations should only add the names of attributes found in the LDAP |
| | | * entry directly associated with the resource. |
| | | * |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * property mapper. This may be used when constructing error |
| | |
| | | * root if all attributes associated with this mapper have been |
| | | * targeted. |
| | | * @param ldapAttributes |
| | | * The set into which the required LDAP attribute names should be |
| | | * put. |
| | | * The set into which the required LDAP attribute names should be |
| | | */ |
| | | abstract void getLdapAttributes(Connection connection, JsonPointer path, JsonPointer subPath, |
| | | Set<String> ldapAttributes); |
| | | abstract void getLdapAttributes(JsonPointer path, JsonPointer subPath, Set<String> ldapAttributes); |
| | | |
| | | /** |
| | | * Transforms the provided REST comparison filter parameters to an LDAP |
| | |
| | | * |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param resource The type of resource being queried. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * property mapper. This may be used when constructing error |
| | |
| | | * {@link FilterType#PRESENT}. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract Promise<Filter, ResourceException> getLdapFilter(Connection connection, JsonPointer path, |
| | | JsonPointer subPath, FilterType type, String operator, |
| | | Object valueAssertion); |
| | | abstract Promise<Filter, ResourceException> getLdapFilter(Connection connection, Resource resource, |
| | | JsonPointer path, JsonPointer subPath, FilterType type, |
| | | String operator, Object valueAssertion); |
| | | |
| | | /** |
| | | * Maps a JSON patch operation to one or more LDAP modifications, returning |
| | |
| | | * |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param resource The exact type of resource being patched. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * property mapper. This may be used when constructing error |
| | |
| | | * with this mapper have been targeted. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract Promise<List<Modification>, ResourceException> patch( |
| | | Connection connection, JsonPointer path, PatchOperation operation); |
| | | abstract Promise<List<Modification>, ResourceException> patch(Connection connection, Resource resource, |
| | | JsonPointer path, PatchOperation operation); |
| | | |
| | | /** |
| | | * Maps one or more LDAP attributes to their JSON representation, returning |
| | |
| | | * |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param resource The exact type of resource being read. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * property mapper. This may be used when constructing error |
| | |
| | | * The LDAP entry to be converted to JSON. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract Promise<JsonValue, ResourceException> read(Connection connection, JsonPointer path, Entry e); |
| | | abstract Promise<JsonValue, ResourceException> read(Connection connection, Resource resource, |
| | | JsonPointer path, Entry e); |
| | | |
| | | /** |
| | | * Maps a JSON value to one or more LDAP modifications, returning a promise |
| | |
| | | * |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param resource The exact type of resource being updated. |
| | | * @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. |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract Promise<List<Modification>, ResourceException> update(Connection connection, JsonPointer path, Entry e, |
| | | JsonValue v); |
| | | abstract Promise<List<Modification>, ResourceException> update(Connection connection, Resource resource, |
| | | JsonPointer path, Entry e, JsonValue v); |
| | | |
| | | // TODO: methods for obtaining schema information (e.g. name, description, type information). |
| | | // TODO: methods for creating sort controls. |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_READ_ONLY_ENDPOINT; |
| | | |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.QueryRequest; |
| | | import org.forgerock.json.resource.QueryResourceHandler; |
| | | import org.forgerock.json.resource.QueryResponse; |
| | | import org.forgerock.json.resource.ReadRequest; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * Provides a read-only view of an underlying request handler. |
| | | */ |
| | | final class ReadOnlyRequestHandler extends AbstractRequestHandler { |
| | | private final RequestHandler delegate; |
| | | |
| | | ReadOnlyRequestHandler(final RequestHandler delegate) { |
| | | super(new BadRequestException(ERR_READ_ONLY_ENDPOINT.get().toString())); |
| | | this.delegate = delegate; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> handleQuery( |
| | | final Context context, final QueryRequest request, final QueryResourceHandler handler) { |
| | | return delegate.handleQuery(context, request, handler); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleRead( |
| | | final Context context, final ReadRequest request) { |
| | | return delegate.handleRead(context, request); |
| | | } |
| | | } |
| | |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.ldap.ResultCode.ADMIN_LIMIT_EXCEEDED; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | 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.newBadRequestException; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashSet; |
| | |
| | | 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.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, |
| | | final String operator, final Object valueAssertion) { |
| | | return mapper.getLdapFilter(connection, path, subPath, type, operator, valueAssertion) |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final JsonPointer subPath, |
| | | final FilterType type, final String operator, |
| | | final Object valueAssertion) { |
| | | return mapper.getLdapFilter(connection, resource, path, subPath, type, operator, valueAssertion) |
| | | .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() { |
| | | @Override |
| | | public Promise<Filter, ResourceException> apply(final Filter result) { |
| | |
| | | @Override |
| | | public Filter apply(Result result) throws ResourceException { |
| | | if (subFilters.size() >= SEARCH_MAX_CANDIDATES) { |
| | | throw asResourceException( |
| | | newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED)); |
| | | throw asResourceException(newLdapException(ADMIN_LIMIT_EXCEEDED)); |
| | | } else if (subFilters.size() == 1) { |
| | | return subFilters.get(0); |
| | | } else { |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<Attribute, ResourceException> getNewLdapAttributes(final Connection connection, final JsonPointer path, |
| | | final List<Object> newValues) { |
| | | Promise<Attribute, ResourceException> getNewLdapAttributes(final Connection connection, final Resource resource, |
| | | 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 PromiseImpl<Attribute, ResourceException> promise = PromiseImpl.create(); |
| | | |
| | | for (final Object value : newValues) { |
| | | mapper.create(connection, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() { |
| | | @Override |
| | | public void handleResult(List<Attribute> result) { |
| | | Attribute primaryKeyAttribute = null; |
| | | for (final Attribute attribute : result) { |
| | | if (attribute.getAttributeDescription().equals(primaryKey)) { |
| | | primaryKeyAttribute = attribute; |
| | | break; |
| | | } |
| | | } |
| | | mapper.create(connection, resource, path, new JsonValue(value)) |
| | | .thenOnResult(new ResultHandler<List<Attribute>>() { |
| | | @Override |
| | | public void handleResult(List<Attribute> result) { |
| | | Attribute primaryKeyAttribute = null; |
| | | for (final Attribute attribute : result) { |
| | | if (attribute.getAttributeDescription().equals(primaryKey)) { |
| | | primaryKeyAttribute = attribute; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) { |
| | | promise.handleException(newBadRequestException(ERR_REFERENCE_FIELD_NO_PRIMARY_KEY.get(path))); |
| | | } |
| | | if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) { |
| | | promise.handleException(newBadRequestException( |
| | | ERR_REFERENCE_FIELD_NO_PRIMARY_KEY.get(path))); |
| | | return; |
| | | } |
| | | |
| | | if (primaryKeyAttribute.size() > 1) { |
| | | promise.handleException( |
| | | newBadRequestException(ERR_REFERENCE_FIELD_MULTIPLE_PRIMARY_KEYS.get(path))); |
| | | } |
| | | if (primaryKeyAttribute.size() > 1) { |
| | | promise.handleException( |
| | | newBadRequestException(ERR_REFERENCE_FIELD_MULTIPLE_PRIMARY_KEYS.get(path))); |
| | | return; |
| | | } |
| | | |
| | | // 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); |
| | | connection.searchSingleEntryAsync(search) |
| | | .thenOnResult(new ResultHandler<SearchResultEntry>() { |
| | | @Override |
| | | public void handleResult(final SearchResultEntry result) { |
| | | synchronized (newLDAPAttribute) { |
| | | newLDAPAttribute.add(result.getName()); |
| | | } |
| | | completeIfNecessary(); |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>() { |
| | | @Override |
| | | public void handleException(final LdapException error) { |
| | | ResourceException re; |
| | | try { |
| | | throw error; |
| | | } catch (final EntryNotFoundException e) { |
| | | re = newBadRequestException(ERR_REFERENCE_FIELD_DOES_NOT_EXIST.get( |
| | | primaryKeyValue.toString(), path)); |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | re = newBadRequestException( |
| | | ERR_REFERENCE_FIELD_AMBIGUOUS.get(primaryKeyValue.toString(), path)); |
| | | } catch (final LdapException e) { |
| | | re = asResourceException(e); |
| | | } |
| | | exception.compareAndSet(null, re); |
| | | completeIfNecessary(); |
| | | } |
| | | }); |
| | | } |
| | | // 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); |
| | | connection.searchSingleEntryAsync(search) |
| | | .thenOnResult(new ResultHandler<SearchResultEntry>() { |
| | | @Override |
| | | public void handleResult(final SearchResultEntry result) { |
| | | synchronized (newLDAPAttribute) { |
| | | newLDAPAttribute.add(result.getName()); |
| | | } |
| | | completeIfNecessary(); |
| | | } |
| | | }) |
| | | .thenOnException(new ExceptionHandler<LdapException>() { |
| | | @Override |
| | | public void handleException(final LdapException error) { |
| | | ResourceException re; |
| | | try { |
| | | throw error; |
| | | } catch (final EntryNotFoundException e) { |
| | | re = newBadRequestException( |
| | | ERR_REFERENCE_FIELD_DOES_NOT_EXIST.get(primaryKeyValue, path)); |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | re = newBadRequestException( |
| | | ERR_REFERENCE_FIELD_AMBIGUOUS.get(primaryKeyValue, path)); |
| | | } catch (final LdapException e) { |
| | | re = asResourceException(e); |
| | | } |
| | | exception.compareAndSet(null, re); |
| | | completeIfNecessary(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private void completeIfNecessary() { |
| | | if (pendingSearches.decrementAndGet() == 0) { |
| | | if (exception.get() != null) { |
| | | promise.handleException(exception.get()); |
| | | } else { |
| | | promise.handleResult(newLDAPAttribute); |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | private void completeIfNecessary() { |
| | | if (pendingSearches.decrementAndGet() == 0) { |
| | | if (exception.get() != null) { |
| | | promise.handleException(exception.get()); |
| | | } else { |
| | | promise.handleResult(newLDAPAttribute); |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | return promise; |
| | | } |
| | |
| | | return this; |
| | | } |
| | | |
| | | @SuppressWarnings("fallthrough") |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) { |
| | | final Attribute attribute = e.getAttribute(ldapAttributeName); |
| | | if (attribute == null || attribute.isEmpty()) { |
| | | return Promises.newResultPromise(null); |
| | | } else if (attributeIsSingleValued()) { |
| | | try { |
| | | final DN dn = attribute.parse().usingSchema(schema).asDN(); |
| | | return readEntry(connection, path, dn); |
| | | } catch (final Exception ex) { |
| | | // The LDAP attribute could not be decoded. |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final Entry e) { |
| | | final Set<DN> dns = e.parseAttribute(ldapAttributeName).usingSchema(schema).asSetOfDN(); |
| | | switch (dns.size()) { |
| | | case 0: |
| | | return newResultPromise(null); |
| | | case 1: |
| | | if (attributeIsSingleValued()) { |
| | | try { |
| | | return readEntry(connection, resource, path, dns.iterator().next()); |
| | | } catch (final Exception ex) { |
| | | // The LDAP attribute could not be decoded. |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | | } |
| | | } |
| | | } else { |
| | | // Fall-though: unexpectedly got multiple values. It's probably best to just return them. |
| | | default: |
| | | try { |
| | | final Set<DN> dns = attribute.parse().usingSchema(schema).asSetOfDN(); |
| | | |
| | | final List<Promise<JsonValue, ResourceException>> promises = new ArrayList<>(dns.size()); |
| | | for (final DN dn : dns) { |
| | | promises.add(readEntry(connection, path, dn)); |
| | | promises.add(readEntry(connection, resource, path, dn)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | | .then(new Function<List<JsonValue>, JsonValue, ResourceException>() { |
| | | @Override |
| | |
| | | } |
| | | |
| | | private Promise<JsonValue, ResourceException> readEntry( |
| | | final Connection connection, final JsonPointer path, final DN dn) { |
| | | final Connection connection, final Resource resource, final JsonPointer path, final DN dn) { |
| | | final Set<String> requestedLDAPAttributes = new LinkedHashSet<>(); |
| | | mapper.getLdapAttributes(connection, path, new JsonPointer(), requestedLDAPAttributes); |
| | | mapper.getLdapAttributes(path, new JsonPointer(), requestedLDAPAttributes); |
| | | |
| | | final Filter searchFilter = filter != null ? filter : Filter.alwaysTrue(); |
| | | final String[] attributes = requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]); |
| | |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() { |
| | | @Override |
| | | public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) { |
| | | return mapper.read(connection, path, result); |
| | | return mapper.read(connection, resource, path, result); |
| | | } |
| | | }, new AsyncFunction<LdapException, JsonValue, ResourceException>() { |
| | | @Override |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static java.util.Arrays.asList; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ABSTRACT_TYPE_IN_CREATE; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MISSING_TYPE_PROPERTY_IN_CREATE; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_TYPE_IN_CREATE; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; |
| | | import static org.forgerock.util.Utils.joinAsString; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collection; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.LinkedAttribute; |
| | | |
| | | /** |
| | | * Defines the characteristics of a resource, including its properties, inheritance, and sub-resources. |
| | | */ |
| | | public final class Resource { |
| | | /** The resource ID. */ |
| | | private final String id; |
| | | /** {@code true} if only sub-types of this resource can be created. */ |
| | | private boolean isAbstract; |
| | | /** The ID of the super-type of this resource, may be {@code null}. */ |
| | | private String superTypeId; |
| | | /** The LDAP object classes associated with this resource. */ |
| | | private final Attribute objectClasses = new LinkedAttribute("objectClass"); |
| | | /** The possibly empty set of sub-resources. */ |
| | | private final Set<SubResource> subResources = new LinkedHashSet<>(); |
| | | /** The set of property mappers associated with this resource, excluding inherited properties. */ |
| | | private final Map<String, PropertyMapper> declaredProperties = new LinkedHashMap<>(); |
| | | /** The set of property mappers associated with this resource, including inherited properties. */ |
| | | private final Map<String, PropertyMapper> allProperties = new LinkedHashMap<>(); |
| | | /** |
| | | * A JSON pointer to the primitive JSON property that will be used to convey type information. May be {@code |
| | | * null} if the type property is defined in a super type or if this resource does not have any sub-types. |
| | | */ |
| | | private JsonPointer resourceTypeProperty; |
| | | /** Set to {@code true} once this Resource has been built. */ |
| | | private boolean isBuilt = false; |
| | | /** The resolved super-type. */ |
| | | private Resource superType; |
| | | /** The resolved sub-resources (only immediate children). */ |
| | | private final Set<Resource> subTypes = new LinkedHashSet<>(); |
| | | /** The property mapper which will map all properties for this resource including inherited properties. */ |
| | | private final ObjectPropertyMapper propertyMapper = new ObjectPropertyMapper(); |
| | | /** Routes requests to sub-resources. */ |
| | | private final Router subResourceRouter = new Router(); |
| | | private volatile Boolean hasSubTypesWithSubResources = null; |
| | | /** The set of actions supported by this resource and its sub-types. */ |
| | | private final Set<Action> supportedActions = new HashSet<>(); |
| | | |
| | | Resource(final String id) { |
| | | this.id = id; |
| | | } |
| | | |
| | | /** |
| | | * Returns the resource ID of this resource. |
| | | * |
| | | * @return The resource ID of this resource. |
| | | */ |
| | | @Override |
| | | public String toString() { |
| | | return id; |
| | | } |
| | | |
| | | /** |
| | | * Returns {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this |
| | | * resource. |
| | | * |
| | | * @param o |
| | | * The object to compare. |
| | | * @return {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this |
| | | * resource. |
| | | */ |
| | | @Override |
| | | public boolean equals(final Object o) { |
| | | return this == o || (o instanceof Resource && id.equals(((Resource) o).id)); |
| | | } |
| | | |
| | | @Override |
| | | public int hashCode() { |
| | | return id.hashCode(); |
| | | } |
| | | |
| | | /** |
| | | * Specifies the resource ID of the resource which is a super-type of this resource. This resource will inherit |
| | | * the properties and sub-resources of the super-type, and may optionally override them. |
| | | * |
| | | * @param resourceId |
| | | * The resource ID of the resource which is a super-type of this resource, or {@code null} if there is no |
| | | * super-type. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource superType(final String resourceId) { |
| | | this.superTypeId = resourceId; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies whether this resource is an abstract type and therefore cannot be created. Only non-abstract |
| | | * sub-types can be created. |
| | | * |
| | | * @param isAbstract |
| | | * {@code true} if this resource is abstract. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource isAbstract(final boolean isAbstract) { |
| | | this.isAbstract = isAbstract; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies a mapping for a property contained in this JSON resource. Properties are inherited and sub-types may |
| | | * override them. Properties are optional: a resource that does not have any properties cannot be created, read, |
| | | * or modified, and may only be used for accessing sub-resources. These resources usually represent API |
| | | * "endpoints". |
| | | * |
| | | * @param name |
| | | * The name of the JSON property to be mapped. |
| | | * @param mapper |
| | | * The property mapper responsible for mapping the JSON property to LDAP attribute(s). |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource property(final String name, final PropertyMapper mapper) { |
| | | declaredProperties.put(name, mapper); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping |
| | | * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent |
| | | * attributes with explicit mappings being mapped twice. |
| | | * |
| | | * @param include {@code true} if all LDAP user attributes be mapped by default. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource includeAllUserAttributesByDefault(final boolean include) { |
| | | propertyMapper.includeAllUserAttributesByDefault(include); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when |
| | | * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be |
| | | * excluded in order to prevent duplication. |
| | | * |
| | | * @param attributeNames The list of attributes to be excluded. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource excludedDefaultUserAttributes(final String... attributeNames) { |
| | | return excludedDefaultUserAttributes(Arrays.asList(attributeNames)); |
| | | } |
| | | |
| | | /** |
| | | * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when |
| | | * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be |
| | | * excluded in order to prevent duplication. |
| | | * |
| | | * @param attributeNames The list of attributes to be excluded. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource excludedDefaultUserAttributes(final Collection<String> attributeNames) { |
| | | propertyMapper.excludedDefaultUserAttributes(attributeNames); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies the name of the JSON property which contains the resource's type, whose value is the |
| | | * resource ID. The resource type property is inherited by sub-types and must be available to any resources |
| | | * referenced from {@link SubResource sub-resources}. |
| | | * |
| | | * @param resourceTypeProperty |
| | | * The name of the JSON property which contains the resource's type, or {@code null} if this resource does |
| | | * not have a resource type property or if it should be inherited from a super-type. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource resourceTypeProperty(final JsonPointer resourceTypeProperty) { |
| | | this.resourceTypeProperty = resourceTypeProperty; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies an LDAP object class which is to be associated with this resource. Multiple object classes may be |
| | | * specified. The object classes are used for determining the type of resource being accessed during all requests |
| | | * other than create. Object classes are inherited by sub-types and must be defined for any resources that are |
| | | * non-abstract and which can be created. |
| | | * |
| | | * @param objectClass |
| | | * An LDAP object class associated with this resource's LDAP representation. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource objectClass(final String objectClass) { |
| | | this.objectClasses.add(objectClass); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies LDAP object classes which are to be associated with this resource. Multiple object classes may be |
| | | * specified. The object classes are used for determining the type of resource being accessed during all requests |
| | | * other than create. Object classes are inherited by sub-types and must be defined for any resources that are |
| | | * non-abstract and which can be created. |
| | | * |
| | | * @param objectClasses |
| | | * The LDAP object classes associated with this resource's LDAP representation. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource objectClasses(final String... objectClasses) { |
| | | this.objectClasses.add((Object[]) objectClasses); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Registers an action which should be supported by this resource. By default, no actions are supported. |
| | | * |
| | | * @param action |
| | | * The action supported by this resource. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource supportedAction(final Action action) { |
| | | this.supportedActions.add(action); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Registers zero or more actions which should be supported by this resource. By default, no actions are supported. |
| | | * |
| | | * @param actions |
| | | * The actions supported by this resource. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource supportedActions(final Action... actions) { |
| | | this.supportedActions.addAll(Arrays.asList(actions)); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies a parent-child relationship with another resource. Sub-resources are inherited by sub-types and may |
| | | * be overridden. |
| | | * |
| | | * @param subResource |
| | | * The sub-resource definition. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource subResource(final SubResource subResource) { |
| | | this.subResources.add(subResource); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies a parent-child relationship with zero or more resources. Sub-resources are inherited by sub-types and |
| | | * may be overridden. |
| | | * |
| | | * @param subResources |
| | | * The sub-resource definitions. |
| | | * @return A reference to this object. |
| | | */ |
| | | public Resource subResources(final SubResource... subResources) { |
| | | this.subResources.addAll(asList(subResources)); |
| | | return this; |
| | | } |
| | | |
| | | boolean hasSupportedAction(final Action action) { |
| | | return supportedActions.contains(action); |
| | | } |
| | | |
| | | boolean hasSubTypes() { |
| | | return !subTypes.isEmpty(); |
| | | } |
| | | |
| | | boolean mayHaveSubResources() { |
| | | return !subResources.isEmpty() || hasSubTypesWithSubResources(); |
| | | } |
| | | |
| | | boolean hasSubTypesWithSubResources() { |
| | | if (hasSubTypesWithSubResources == null) { |
| | | for (final Resource subType : subTypes) { |
| | | if (!subType.subResources.isEmpty() || subType.hasSubTypesWithSubResources()) { |
| | | hasSubTypesWithSubResources = true; |
| | | return true; |
| | | } |
| | | } |
| | | hasSubTypesWithSubResources = false; |
| | | } |
| | | return hasSubTypesWithSubResources; |
| | | } |
| | | |
| | | Set<Resource> getSubTypes() { |
| | | return subTypes; |
| | | } |
| | | |
| | | Resource resolveSubTypeFromJson(final JsonValue content) throws ResourceException { |
| | | if (!hasSubTypes()) { |
| | | // The resource type is implied because this resource does not have sub-types. In particular, resources |
| | | // are not required to have type information if they don't have sub-types. |
| | | return this; |
| | | } |
| | | final JsonValue jsonType = content.get(resourceTypeProperty); |
| | | if (jsonType == null || !jsonType.isString()) { |
| | | throw newBadRequestException(ERR_MISSING_TYPE_PROPERTY_IN_CREATE.get(resourceTypeProperty)); |
| | | } |
| | | final String type = jsonType.asString(); |
| | | final Resource subType = resolveSubTypeFromString(type); |
| | | if (subType == null) { |
| | | throw newBadRequestException(ERR_UNRECOGNIZED_TYPE_IN_CREATE.get(type, getAllowedResourceTypes())); |
| | | } |
| | | if (subType.isAbstract) { |
| | | throw newBadRequestException(ERR_ABSTRACT_TYPE_IN_CREATE.get(type, getAllowedResourceTypes())); |
| | | } |
| | | return subType; |
| | | } |
| | | |
| | | private String getAllowedResourceTypes() { |
| | | final List<String> allowedTypes = new ArrayList<>(); |
| | | getAllowedResourceTypes(allowedTypes); |
| | | return joinAsString(", ", allowedTypes); |
| | | } |
| | | |
| | | private void getAllowedResourceTypes(final List<String> allowedTypes) { |
| | | if (!isAbstract) { |
| | | allowedTypes.add(id); |
| | | } |
| | | for (final Resource subType : subTypes) { |
| | | subType.getAllowedResourceTypes(allowedTypes); |
| | | } |
| | | } |
| | | |
| | | Resource resolveSubTypeFromString(final String type) { |
| | | if (id.equalsIgnoreCase(type)) { |
| | | return this; |
| | | } |
| | | for (final Resource subType : subTypes) { |
| | | final Resource resolvedSubType = subType.resolveSubTypeFromString(type); |
| | | if (resolvedSubType != null) { |
| | | return resolvedSubType; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | Resource resolveSubTypeFromObjectClasses(final Entry entry) { |
| | | if (!hasSubTypes()) { |
| | | // This resource does not have sub-types. |
| | | return this; |
| | | } |
| | | final Attribute objectClassesFromEntry = entry.getAttribute("objectClass"); |
| | | final Resource subType = resolveSubTypeFromObjectClasses(objectClassesFromEntry); |
| | | if (subType == null) { |
| | | // Best effort. |
| | | return this; |
| | | } |
| | | return subType; |
| | | } |
| | | |
| | | private Resource resolveSubTypeFromObjectClasses(final Attribute objectClassesFromEntry) { |
| | | if (!objectClassesFromEntry.containsAll(objectClasses)) { |
| | | return null; |
| | | } |
| | | // This resource is a potential match, but sub-types may be better. |
| | | for (final Resource subType : subTypes) { |
| | | final Resource resolvedSubType = subType.resolveSubTypeFromObjectClasses(objectClassesFromEntry); |
| | | if (resolvedSubType != null) { |
| | | return resolvedSubType; |
| | | } |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | Attribute getObjectClassAttribute() { |
| | | return objectClasses; |
| | | } |
| | | |
| | | RequestHandler getSubResourceRouter() { |
| | | return subResourceRouter; |
| | | } |
| | | |
| | | String getResourceId() { |
| | | return id; |
| | | } |
| | | |
| | | void build(final Rest2Ldap rest2Ldap) { |
| | | // Prevent re-entrant calls. |
| | | if (isBuilt) { |
| | | return; |
| | | } |
| | | isBuilt = true; |
| | | |
| | | if (superTypeId != null) { |
| | | superType = rest2Ldap.getResource(superTypeId); |
| | | if (superType == null) { |
| | | throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE.get(id, superTypeId)); |
| | | } |
| | | // Inherit content from super-type. |
| | | superType.build(rest2Ldap); |
| | | superType.subTypes.add(this); |
| | | if (resourceTypeProperty == null) { |
| | | resourceTypeProperty = superType.resourceTypeProperty; |
| | | } |
| | | objectClasses.addAll(superType.objectClasses); |
| | | subResourceRouter.addAllRoutes(superType.subResourceRouter); |
| | | allProperties.putAll(superType.allProperties); |
| | | } |
| | | allProperties.putAll(declaredProperties); |
| | | for (final Map.Entry<String, PropertyMapper> property : allProperties.entrySet()) { |
| | | propertyMapper.property(property.getKey(), property.getValue()); |
| | | } |
| | | for (final SubResource subResource : subResources) { |
| | | subResource.build(rest2Ldap, id); |
| | | subResource.addRoutes(subResourceRouter); |
| | | } |
| | | } |
| | | |
| | | PropertyMapper getPropertyMapper() { |
| | | return propertyMapper; |
| | | } |
| | | } |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static java.util.Collections.singletonList; |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysFalse; |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysTrue; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ILLEGAL_FILTER_ASSERTION_VALUE; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MODIFY_READ_ONLY_FIELD; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_PATCH_READ_ONLY_FIELD; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * A property mapper which maps a single JSON property containing the resource type to the resource's object classes. |
| | | */ |
| | | final class ResourceTypePropertyMapper extends PropertyMapper { |
| | | static final ResourceTypePropertyMapper INSTANCE = new ResourceTypePropertyMapper(); |
| | | |
| | | private ResourceTypePropertyMapper() { } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return "type()"; |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Attribute>, ResourceException> create(final Connection connection, |
| | | final Resource resource, final JsonPointer path, |
| | | final JsonValue v) { |
| | | return newResultPromise(singletonList(resource.getObjectClassAttribute())); |
| | | } |
| | | |
| | | @Override |
| | | void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add("objectClass"); |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final JsonPointer subPath, |
| | | final FilterType type, final String operator, |
| | | final Object valueAssertion) { |
| | | if (subPath.isEmpty()) { |
| | | switch (type) { |
| | | case PRESENT: |
| | | return newResultPromise(alwaysTrue()); |
| | | case EQUAL_TO: |
| | | if (valueAssertion instanceof String) { |
| | | final Resource subType = resource.resolveSubTypeFromString((String) valueAssertion); |
| | | if (subType == null) { |
| | | return newResultPromise(alwaysFalse()); |
| | | } |
| | | final List<Filter> subFilters = new ArrayList<>(); |
| | | for (final ByteString objectClass : subType.getObjectClassAttribute()) { |
| | | subFilters.add(Filter.equality("objectClass", objectClass)); |
| | | } |
| | | return newResultPromise(Filter.and(subFilters)); |
| | | } |
| | | return newBadRequestException(ERR_ILLEGAL_FILTER_ASSERTION_VALUE.get(valueAssertion, path)).asPromise(); |
| | | default: |
| | | return newResultPromise(alwaysFalse()); // Not supported. |
| | | } |
| | | } else { |
| | | // This property mapper does not support partial filtering. |
| | | return newResultPromise(alwaysFalse()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final PatchOperation operation) { |
| | | return newBadRequestException(ERR_PATCH_READ_ONLY_FIELD.get(path)).asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final Entry e) { |
| | | return newResultPromise(new JsonValue(resource.getResourceId())); |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final Entry e, final JsonValue v) { |
| | | if (!isNullOrEmpty(v) && !v.getObject().equals(resource.getResourceId())) { |
| | | return newBadRequestException(ERR_MODIFY_READ_ONLY_FIELD.get("update", path)).asPromise(); |
| | | } else { |
| | | return newResultPromise(Collections.<Modification>emptyList()); |
| | | } |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | * Copyright 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | import static java.util.Arrays.asList; |
| | | import static org.forgerock.json.resource.ResourceException.newResourceException; |
| | | import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool; |
| | | import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer; |
| | | import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer; |
| | | import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*; |
| | | import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_MONITORING_INTERVAL; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; |
| | | import static org.forgerock.opendj.ldap.schema.CoreSchema.getEntryUUIDAttributeType; |
| | | import static org.forgerock.opendj.ldap.ResultCode.ADMIN_LIMIT_EXCEEDED; |
| | | import static org.forgerock.opendj.ldap.ResultCode.ENTRY_ALREADY_EXISTS; |
| | | import static org.forgerock.opendj.ldap.ResultCode.SIZE_LIMIT_EXCEEDED; |
| | | import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newLocalizedIllegalArgumentException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | import static org.forgerock.util.time.Duration.*; |
| | | import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate; |
| | | |
| | | import java.security.GeneralSecurityException; |
| | | import java.util.ArrayList; |
| | | 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.Set; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import javax.net.ssl.TrustManager; |
| | | import javax.net.ssl.X509KeyManager; |
| | | |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.ForbiddenException; |
| | | import org.forgerock.json.resource.InternalServerErrorException; |
| | | import org.forgerock.json.resource.NotFoundException; |
| | | import org.forgerock.json.resource.PermanentException; |
| | | import org.forgerock.json.resource.PreconditionFailedException; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.RetryableException; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.json.resource.ServiceUnavailableException; |
| | | import org.forgerock.opendj.ldap.AssertionFailureException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | | 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.ConnectionException; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.ConstraintViolationException; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.DecodeOptions; |
| | | import org.forgerock.opendj.ldap.EntryNotFoundException; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LDAPConnectionFactory; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.LinkedAttribute; |
| | | import org.forgerock.opendj.ldap.MultipleEntriesFoundException; |
| | | import org.forgerock.opendj.ldap.RDN; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SSLContextBuilder; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.TimeoutResultException; |
| | | 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.schema.AttributeType; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.util.Option; |
| | | import org.forgerock.util.Options; |
| | | import org.forgerock.util.Reject; |
| | | import org.forgerock.util.time.Duration; |
| | | |
| | | /** Provides core factory methods and builders for constructing LDAP resource collections. */ |
| | | /** |
| | | * Provides methods for constructing Rest2Ldap protocol gateways. Applications construct a new Rest2Ldap |
| | | * instance by calling {@link #rest2Ldap} passing in a list of {@link Resource resources} which together define |
| | | * the data model being exposed by the gateway. Call {@link #newRequestHandlerFor(String)} in order to obtain |
| | | * a request handler for a specific resource. The methods in this class can be categorized as follows: |
| | | * <p/> |
| | | * Creating Rest2Ldap gateways: |
| | | * <ul> |
| | | * <li>{@link #rest2Ldap} - creates a gateway for a given set of resources</li> |
| | | * <li>{@link #newRequestHandlerFor} - obtains a request handler for the specified endpoint resource.</li> |
| | | * </ul> |
| | | * <p/> |
| | | * Defining resource types, e.g. users, groups, devices, etc: |
| | | * <ul> |
| | | * <li>{@link #resource} - creates a resource having a fluent API for defining additional characteristics |
| | | * such as the resource's inheritance, sub-resources, and properties</li> |
| | | * </ul> |
| | | * <p/> |
| | | * Defining a resource's sub-resources. A sub-resource is a resource which is subordinate to another resource. Or, to |
| | | * put it another way, sub-resources define parent child relationships where the life-cycle of a child resource is |
| | | * constrained by the life-cycle of the parent: deleting the parent implies that all children are deleted as well. An |
| | | * example of a sub-resource is a subscriber having one or more devices: |
| | | * <ul> |
| | | * <li>{@link #collectionOf} - creates a one-to-many relationship. Collections support creation, deletion, |
| | | * and querying of child resources</li> |
| | | * <li>{@link #singletonOf} - creates a one-to-one relationship. Singletons cannot be created or destroyed, |
| | | * although they may be modified if they have properties which are modifiable. Singletons are usually only used as |
| | | * top-level entry points into REST APIs. |
| | | * </li> |
| | | * </ul> |
| | | * <p/> |
| | | * Defining a resource's properties: |
| | | * <ul> |
| | | * <li>{@link #resourceType} - defines a property whose JSON value will be the name of the resource, e.g. "user"</li> |
| | | * <li>{@link #simple} - defines a property which maps a JSON value to a single LDAP attribute</li> |
| | | * <li>{@link #object} - defines a property which is a JSON object having zero or more nested properties</li> |
| | | * <li>{@link #reference} - defines a property whose JSON value is a reference to another resource. Use these for |
| | | * mapping LDAP attributes which contain the DN of another LDAP entry exposed by Rest2Ldap. For example, a user's |
| | | * "manager" attribute or the members of a group.</li> |
| | | * </ul> |
| | | */ |
| | | public final class Rest2Ldap { |
| | | /** Indicates whether LDAP client connections should use SSL or StartTLS. */ |
| | | private enum ConnectionSecurity { |
| | | NONE, SSL, STARTTLS |
| | | } |
| | | |
| | | /** |
| | | * Specifies the mechanism which should be used for trusting certificates |
| | | * presented by the LDAP server. |
| | | * Specifies the LDAP decoding options which should be used when decoding LDAP DNs, attribute types, and controls. |
| | | * By default Rest2Ldap will use a set of options of will always use the default schema. |
| | | */ |
| | | enum TrustManagerType { |
| | | TRUSTALL, JVM, FILE |
| | | } |
| | | |
| | | public static final Option<DecodeOptions> DECODE_OPTIONS = Option.withDefault(new DecodeOptions()); |
| | | /** |
| | | * Specifies the mechanism which manage which X509 certificate-based key pairs should be used to authenticate the |
| | | * local side of a secure socket. |
| | | * Specifies whether Rest2Ldap should support multi-version concurrency control (MVCC) through the use of an MVCC |
| | | * LDAP {@link #MVCC_ATTRIBUTE attribute} such as "etag". By default Rest2Ldap will use MVCC. |
| | | */ |
| | | enum KeyManagerType { |
| | | JVM, KEYSTORE, PKCS11 |
| | | } |
| | | |
| | | /** A builder for incrementally constructing LDAP resource collections. */ |
| | | public static final class Builder { |
| | | private final List<Attribute> additionalLDAPAttributes = new LinkedList<>(); |
| | | private DN baseDN; // TODO: support template variables. |
| | | private AttributeDescription etagAttribute; |
| | | private NamingStrategy namingStrategy; |
| | | private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS; |
| | | private PropertyMapper rootMapper; |
| | | private Schema schema = Schema.getDefaultSchema(); |
| | | private boolean usePermissiveModify; |
| | | private boolean useSubtreeDelete; |
| | | |
| | | private Builder() { |
| | | useEtagAttribute(); |
| | | useClientDNNaming("uid"); |
| | | } |
| | | |
| | | /** |
| | | * Specifies an additional LDAP attribute which should be included with |
| | | * new LDAP entries when they are created. Use this method to specify |
| | | * the LDAP objectClass attribute. |
| | | * |
| | | * @param attribute |
| | | * The additional LDAP attribute to be included with new LDAP |
| | | * entries. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder additionalLDAPAttribute(final Attribute attribute) { |
| | | additionalLDAPAttributes.add(attribute); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies an additional LDAP attribute which should be included with |
| | | * new LDAP entries when they are created. Use this method to specify |
| | | * the LDAP objectClass attribute. |
| | | * |
| | | * @param attribute |
| | | * The name of the additional LDAP attribute to be included |
| | | * with new LDAP entries. |
| | | * @param values |
| | | * The value(s) of the additional LDAP attribute. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder additionalLDAPAttribute(final String attribute, final Object... values) { |
| | | return additionalLDAPAttribute(new LinkedAttribute(ad(attribute), values)); |
| | | } |
| | | |
| | | /** |
| | | * Sets the base DN beneath which LDAP entries (resources) are to be found. |
| | | * |
| | | * @param dn |
| | | * The base DN. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder baseDN(final DN dn) { |
| | | Reject.ifNull(dn); |
| | | this.baseDN = dn; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the base DN beneath which LDAP entries (resources) are to be found. |
| | | * |
| | | * @param dn |
| | | * The base DN. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder baseDN(final String dn) { |
| | | return baseDN(DN.valueOf(dn, schema)); |
| | | } |
| | | |
| | | /** |
| | | * Creates a new LDAP resource collection configured using this builder. |
| | | * |
| | | * @return The new LDAP resource collection. |
| | | */ |
| | | public CollectionResourceProvider build() { |
| | | Reject.ifNull(baseDN); |
| | | if (rootMapper == null) { |
| | | throw new IllegalStateException(ERR_CONFIG_NO_MAPPINGS_PROVIDED.get().toString()); |
| | | } |
| | | return new SubResourceImpl(baseDN, rootMapper, namingStrategy, etagAttribute, |
| | | new Config(readOnUpdatePolicy, useSubtreeDelete, usePermissiveModify, schema), |
| | | additionalLDAPAttributes); |
| | | } |
| | | |
| | | /** |
| | | * Configures the JSON to LDAP mapping using the provided JSON |
| | | * configuration. The caller is still required to set the connection |
| | | * factory. See the sample configuration file for a detailed description |
| | | * of its content. |
| | | * |
| | | * @param configuration |
| | | * The JSON configuration. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public Builder configureMapping(final JsonValue configuration) { |
| | | baseDN(configuration.get("baseDN").required().asString()); |
| | | |
| | | final JsonValue readOnUpdatePolicy = configuration.get("readOnUpdatePolicy"); |
| | | if (!readOnUpdatePolicy.isNull()) { |
| | | readOnUpdatePolicy(readOnUpdatePolicy.asEnum(ReadOnUpdatePolicy.class)); |
| | | } |
| | | |
| | | for (final JsonValue v : configuration.get("additionalLDAPAttributes")) { |
| | | final String type = v.get("type").required().asString(); |
| | | final List<Object> values = v.get("values").required().asList(); |
| | | additionalLDAPAttribute(new LinkedAttribute(type, values)); |
| | | } |
| | | |
| | | final JsonValue namingStrategy = configuration.get("namingStrategy"); |
| | | if (!namingStrategy.isNull()) { |
| | | final String name = namingStrategy.get("strategy").required().asString(); |
| | | if (name.equalsIgnoreCase("clientDNNaming")) { |
| | | useClientDNNaming(namingStrategy.get("dnAttribute").required().asString()); |
| | | } else if (name.equalsIgnoreCase("clientNaming")) { |
| | | useClientNaming(namingStrategy.get("dnAttribute").required().asString(), |
| | | namingStrategy.get("idAttribute").required().asString()); |
| | | } else if (name.equalsIgnoreCase("serverNaming")) { |
| | | useServerNaming(namingStrategy.get("dnAttribute").required().asString(), |
| | | namingStrategy.get("idAttribute").required().asString()); |
| | | } else { |
| | | throw newLocalizedIllegalArgumentException(ERR_CONFIG_UNKNOWN_NAMING_CONFIGURATION.get( |
| | | namingStrategy.asString(), "clientDNNaming, clientNaming or serverNaming")); |
| | | } |
| | | } |
| | | |
| | | final JsonValue etagAttribute = configuration.get("etagAttribute"); |
| | | if (!etagAttribute.isNull()) { |
| | | useEtagAttribute(etagAttribute.asString()); |
| | | } |
| | | |
| | | /* |
| | | * Default to false, even though it is supported by OpenDJ, because |
| | | * it requires additional permissions. |
| | | */ |
| | | if (configuration.get("useSubtreeDelete").defaultTo(false).asBoolean()) { |
| | | useSubtreeDelete(); |
| | | } |
| | | |
| | | /* |
| | | * Default to true because it is supported by OpenDJ and does not |
| | | * require additional permissions. |
| | | */ |
| | | if (configuration.get("usePermissiveModify").defaultTo(true).asBoolean()) { |
| | | usePermissiveModify(); |
| | | } |
| | | |
| | | mapper(configureObjectMapper(configuration.get("attributes").required())); |
| | | |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the property mapper which should be used for mapping JSON |
| | | * resources to and from LDAP entries. |
| | | * |
| | | * @param mapper |
| | | * The property mapper. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder mapper(final PropertyMapper mapper) { |
| | | this.rootMapper = mapper; |
| | | 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. The default read on |
| | | * update policy is to use {@link ReadOnUpdatePolicy#CONTROLS controls}. |
| | | * |
| | | * @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 LDAP resource collection builder. |
| | | */ |
| | | public Builder readOnUpdatePolicy(final ReadOnUpdatePolicy policy) { |
| | | this.readOnUpdatePolicy = checkNotNull(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 LDAP resource collection builder. |
| | | */ |
| | | public Builder schema(final Schema schema) { |
| | | this.schema = checkNotNull(schema); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must be provided by the user, and |
| | | * will be used for naming the associated LDAP entry. More specifically, |
| | | * LDAP entry names will be derived by appending a single RDN to the |
| | | * {@link #baseDN(String) base DN} composed of the specified attribute |
| | | * type and LDAP value taken from the LDAP entry once attribute mapping |
| | | * has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the user provides the |
| | | * resource name when creating new resources, which means it must be |
| | | * included in the resource content when not specified explicitly in the |
| | | * create request. |
| | | * |
| | | * @param attribute |
| | | * The LDAP attribute which will be used for naming. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useClientDNNaming(final AttributeType attribute) { |
| | | this.namingStrategy = new DNNamingStrategy(attribute); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must be provided by the user, and |
| | | * will be used for naming the associated LDAP entry. More specifically, |
| | | * LDAP entry names will be derived by appending a single RDN to the |
| | | * {@link #baseDN(String) base DN} composed of the specified attribute |
| | | * type and LDAP value taken from the LDAP entry once attribute mapping |
| | | * has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the user provides the |
| | | * resource name when creating new resources, which means it must be |
| | | * included in the resource content when not specified explicitly in the |
| | | * create request. |
| | | * |
| | | * @param attribute |
| | | * The LDAP attribute which will be used for naming. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useClientDNNaming(final String attribute) { |
| | | return useClientDNNaming(at(attribute)); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must be provided by the user, but |
| | | * will not be used for naming the associated LDAP entry. Instead the |
| | | * JSON resource ID will be taken from the {@code idAttribute} in the |
| | | * LDAP entry, and the LDAP entry name will be derived by appending a |
| | | * single RDN to the {@link #baseDN(String) base DN} composed of the |
| | | * {@code dnAttribute} taken from the LDAP entry once attribute mapping |
| | | * has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the user provides the |
| | | * resource name when creating new resources, which means it must be |
| | | * included in the resource content when not specified explicitly in the |
| | | * create request. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @param idAttribute |
| | | * The attribute which will be used for JSON resource IDs. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useClientNaming(final AttributeType dnAttribute, |
| | | final AttributeDescription idAttribute) { |
| | | this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, false); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must be provided by the user, but |
| | | * will not be used for naming the associated LDAP entry. Instead the |
| | | * JSON resource ID will be taken from the {@code idAttribute} in the |
| | | * LDAP entry, and the LDAP entry name will be derived by appending a |
| | | * single RDN to the {@link #baseDN(String) base DN} composed of the |
| | | * {@code dnAttribute} taken from the LDAP entry once attribute mapping |
| | | * has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the user provides the |
| | | * resource name when creating new resources, which means it must be |
| | | * included in the resource content when not specified explicitly in the |
| | | * create request. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @param idAttribute |
| | | * The attribute which will be used for JSON resource IDs. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useClientNaming(final String dnAttribute, final String idAttribute) { |
| | | return useClientNaming(at(dnAttribute), ad(idAttribute)); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the "etag" LDAP attribute should be used for resource |
| | | * versioning. This is the default behavior. |
| | | * |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useEtagAttribute() { |
| | | return useEtagAttribute("etag"); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the provided LDAP attribute should be used for |
| | | * resource versioning. The "etag" attribute will be used by default. |
| | | * |
| | | * @param attribute |
| | | * The name of the attribute to use for versioning, or |
| | | * {@code null} if resource versioning will not supported. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useEtagAttribute(final AttributeDescription attribute) { |
| | | this.etagAttribute = attribute; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the provided LDAP attribute should be used for |
| | | * resource versioning. The "etag" attribute will be used by default. |
| | | * |
| | | * @param attribute |
| | | * The name of the attribute to use for versioning, or |
| | | * {@code null} if resource versioning will not supported. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useEtagAttribute(final String attribute) { |
| | | return useEtagAttribute(attribute != null ? ad(attribute) : null); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that all LDAP modify operations should be performed using |
| | | * the LDAP permissive modify control. The default behavior is to not |
| | | * use the permissive modify control. Use of the control is strongly |
| | | * recommended. |
| | | * |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder usePermissiveModify() { |
| | | this.usePermissiveModify = true; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID will be derived from the server |
| | | * provided "entryUUID" LDAP attribute. The LDAP entry name will be |
| | | * derived by appending a single RDN to the {@link #baseDN(String) base |
| | | * DN} composed of the {@code dnAttribute} taken from the LDAP entry |
| | | * once attribute mapping has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the server provides the |
| | | * resource name when creating new resources, which means it must not be |
| | | * specified in the create request, nor included in the resource |
| | | * content. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useServerEntryUUIDNaming(final AttributeType dnAttribute) { |
| | | return useServerNaming(dnAttribute, AttributeDescription |
| | | .create(getEntryUUIDAttributeType())); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID will be derived from the server |
| | | * provided "entryUUID" LDAP attribute. The LDAP entry name will be |
| | | * derived by appending a single RDN to the {@link #baseDN(String) base |
| | | * DN} composed of the {@code dnAttribute} taken from the LDAP entry |
| | | * once attribute mapping has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the server provides the |
| | | * resource name when creating new resources, which means it must not be |
| | | * specified in the create request, nor included in the resource |
| | | * content. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useServerEntryUUIDNaming(final String dnAttribute) { |
| | | return useServerEntryUUIDNaming(at(dnAttribute)); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must not be provided by the user, |
| | | * and will not be used for naming the associated LDAP entry. Instead |
| | | * the JSON resource ID will be taken from the {@code idAttribute} in |
| | | * the LDAP entry, and the LDAP entry name will be derived by appending |
| | | * a single RDN to the {@link #baseDN(String) base DN} composed of the |
| | | * {@code dnAttribute} taken from the LDAP entry once attribute mapping |
| | | * has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the server provides the |
| | | * resource name when creating new resources, which means it must not be |
| | | * specified in the create request, nor included in the resource |
| | | * content. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @param idAttribute |
| | | * The attribute which will be used for JSON resource IDs. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useServerNaming(final AttributeType dnAttribute, |
| | | final AttributeDescription idAttribute) { |
| | | this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, true); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must not be provided by the user, |
| | | * and will not be used for naming the associated LDAP entry. Instead |
| | | * the JSON resource ID will be taken from the {@code idAttribute} in |
| | | * the LDAP entry, and the LDAP entry name will be derived by appending |
| | | * a single RDN to the {@link #baseDN(String) base DN} composed of the |
| | | * {@code dnAttribute} taken from the LDAP entry once attribute mapping |
| | | * has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the server provides the |
| | | * resource name when creating new resources, which means it must not be |
| | | * specified in the create request, nor included in the resource |
| | | * content. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @param idAttribute |
| | | * The attribute which will be used for JSON resource IDs. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useServerNaming(final String dnAttribute, final String idAttribute) { |
| | | return useServerNaming(at(dnAttribute), ad(idAttribute)); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that all LDAP delete operations should be performed using |
| | | * the LDAP subtree delete control. The default behavior is to not use |
| | | * the subtree delete control. |
| | | * |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder useSubtreeDelete() { |
| | | this.useSubtreeDelete = true; |
| | | return this; |
| | | } |
| | | |
| | | private AttributeDescription ad(final String attribute) { |
| | | return AttributeDescription.valueOf(attribute, schema); |
| | | } |
| | | |
| | | private AttributeType at(final String attribute) { |
| | | return schema.getAttributeType(attribute); |
| | | } |
| | | |
| | | private PropertyMapper configureMapper(final JsonValue mapper) { |
| | | if (mapper.isDefined("constant")) { |
| | | return constant(mapper.get("constant").getObject()); |
| | | } else if (mapper.isDefined("simple")) { |
| | | final JsonValue config = mapper.get("simple"); |
| | | final SimplePropertyMapper s = |
| | | simple(ad(config.get("ldapAttribute").required().asString())); |
| | | if (config.isDefined("defaultJSONValue")) { |
| | | s.defaultJsonValue(config.get("defaultJSONValue").getObject()); |
| | | } |
| | | if (config.get("isBinary").defaultTo(false).asBoolean()) { |
| | | s.isBinary(); |
| | | } |
| | | if (config.get("isRequired").defaultTo(false).asBoolean()) { |
| | | s.isRequired(); |
| | | } |
| | | if (config.get("isSingleValued").defaultTo(false).asBoolean()) { |
| | | s.isSingleValued(); |
| | | } |
| | | s.writability(parseWritability(mapper, config)); |
| | | return s; |
| | | } else if (mapper.isDefined("reference")) { |
| | | final JsonValue config = mapper.get("reference"); |
| | | final AttributeDescription ldapAttribute = |
| | | ad(config.get("ldapAttribute").required().asString()); |
| | | final DN baseDN = DN.valueOf(config.get("baseDN").required().asString(), schema); |
| | | final AttributeDescription primaryKey = |
| | | ad(config.get("primaryKey").required().asString()); |
| | | final PropertyMapper m = configureMapper(config.get("mapper").required()); |
| | | final ReferencePropertyMapper r = reference(ldapAttribute, baseDN, primaryKey, m); |
| | | if (config.get("isRequired").defaultTo(false).asBoolean()) { |
| | | r.isRequired(); |
| | | } |
| | | if (config.get("isSingleValued").defaultTo(false).asBoolean()) { |
| | | r.isSingleValued(); |
| | | } |
| | | if (config.isDefined("searchFilter")) { |
| | | r.searchFilter(config.get("searchFilter").asString()); |
| | | } |
| | | r.writability(parseWritability(mapper, config)); |
| | | return r; |
| | | } else if (mapper.isDefined("object")) { |
| | | return configureObjectMapper(mapper.get("object")); |
| | | } else { |
| | | throw newJsonValueException(mapper, ERR_CONFIG_NO_MAPPING_IN_CONFIGURATION.get( |
| | | "constant, simple, reference or object")); |
| | | } |
| | | } |
| | | |
| | | private ObjectPropertyMapper configureObjectMapper(final JsonValue mapper) { |
| | | final ObjectPropertyMapper object = object(); |
| | | for (final String attribute : mapper.keys()) { |
| | | object.attribute(attribute, configureMapper(mapper.get(attribute))); |
| | | } |
| | | return object; |
| | | } |
| | | |
| | | private WritabilityPolicy parseWritability(final JsonValue mapper, final JsonValue config) { |
| | | if (config.isDefined("writability")) { |
| | | final String writability = config.get("writability").asString(); |
| | | if (writability.equalsIgnoreCase("readOnly")) { |
| | | return WritabilityPolicy.READ_ONLY; |
| | | } else if (writability.equalsIgnoreCase("readOnlyDiscardWrites")) { |
| | | return WritabilityPolicy.READ_ONLY_DISCARD_WRITES; |
| | | } else if (writability.equalsIgnoreCase("createOnly")) { |
| | | return WritabilityPolicy.CREATE_ONLY; |
| | | } else if (writability.equalsIgnoreCase("createOnlyDiscardWrites")) { |
| | | return WritabilityPolicy.CREATE_ONLY_DISCARD_WRITES; |
| | | } else if (writability.equalsIgnoreCase("readWrite")) { |
| | | return WritabilityPolicy.READ_WRITE; |
| | | } else { |
| | | throw newJsonValueException(mapper, ERR_CONFIG_UNKNOWN_WRITABILITY.get(writability, |
| | | "readOnly, readOnlyDiscardWrites, createOnly, createOnlyDiscardWrites, or readWrite")); |
| | | } |
| | | } else { |
| | | return WritabilityPolicy.READ_WRITE; |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static final class AttributeNamingStrategy extends NamingStrategy { |
| | | private final AttributeDescription dnAttribute; |
| | | private final AttributeDescription idAttribute; |
| | | private final boolean isServerProvided; |
| | | |
| | | private AttributeNamingStrategy(final AttributeType dnAttribute, |
| | | final AttributeDescription idAttribute, final boolean isServerProvided) { |
| | | this.dnAttribute = AttributeDescription.create(dnAttribute); |
| | | if (this.dnAttribute.equals(idAttribute)) { |
| | | throw newLocalizedIllegalArgumentException(ERR_CONFIG_NAMING_STRATEGY_DN_AND_ID_NOT_DIFFERENT.get()); |
| | | } |
| | | this.idAttribute = checkNotNull(idAttribute); |
| | | this.isServerProvided = isServerProvided; |
| | | } |
| | | |
| | | @Override |
| | | SearchRequest createSearchRequest(final Connection connection, final DN baseDN, final String resourceId) { |
| | | return newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter.equality(idAttribute |
| | | .toString(), resourceId)); |
| | | } |
| | | |
| | | @Override |
| | | void getLdapAttributes(final Connection connection, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(idAttribute.toString()); |
| | | } |
| | | |
| | | @Override |
| | | String getResourceId(final Connection connection, final Entry entry) { |
| | | return entry.parseAttribute(idAttribute).asString(); |
| | | } |
| | | |
| | | @Override |
| | | void setResourceId(final Connection connection, final DN baseDN, final String resourceId, |
| | | final Entry entry) throws ResourceException { |
| | | if (isServerProvided) { |
| | | if (resourceId != null) { |
| | | throw newBadRequestException(ERR_CLIENT_PROVIDER_RESOURCE_ID_MISSING.get()); |
| | | } |
| | | } else { |
| | | entry.addAttribute(new LinkedAttribute(idAttribute, ByteString.valueOfUtf8(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 DNNamingStrategy extends NamingStrategy { |
| | | private final AttributeDescription attribute; |
| | | |
| | | private DNNamingStrategy(final AttributeType attribute) { |
| | | this.attribute = AttributeDescription.create(attribute); |
| | | } |
| | | |
| | | @Override |
| | | SearchRequest createSearchRequest(final Connection connection, final DN baseDN, final String resourceId) { |
| | | return newSearchRequest(baseDN.child(rdn(resourceId)), SearchScope.BASE_OBJECT, Filter |
| | | .objectClassPresent()); |
| | | } |
| | | |
| | | @Override |
| | | void getLdapAttributes(final Connection connection, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(attribute.toString()); |
| | | } |
| | | |
| | | @Override |
| | | String getResourceId(final Connection connection, final Entry entry) { |
| | | return entry.parseAttribute(attribute).asString(); |
| | | } |
| | | |
| | | @Override |
| | | void setResourceId(final Connection connection, 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.valueOfUtf8(resourceId))); |
| | | } else if (entry.getAttribute(attribute) != null) { |
| | | entry.setName(baseDN.child(rdn(entry.parseAttribute(attribute).asString()))); |
| | | } else { |
| | | throw newBadRequestException(ERR_CLIENT_PROVIDER_RESOURCE_ID_MISSING.get()); |
| | | } |
| | | } |
| | | |
| | | private RDN rdn(final String resourceId) { |
| | | return new RDN(attribute.getAttributeType(), resourceId); |
| | | } |
| | | } |
| | | public static final Option<Boolean> USE_MVCC = Option.withDefault(true); |
| | | /** |
| | | * Specifies the name of the LDAP attribute which should be used for multi-version concurrency control (MVCC) if |
| | | * {@link #USE_MVCC enabled}. By default Rest2Ldap will use the "etag" operational attribute. |
| | | */ |
| | | public static final Option<String> MVCC_ATTRIBUTE = Option.withDefault("etag"); |
| | | /** |
| | | * Specifies the policy which should be used in order to read an entry before it is deleted, or after it is added or |
| | | * modified. By default Rest2Ldap will use the {@link ReadOnUpdatePolicy#CONTROLS controls} read on update policy. |
| | | */ |
| | | public static final Option<ReadOnUpdatePolicy> READ_ON_UPDATE_POLICY = Option.withDefault(CONTROLS); |
| | | /** |
| | | * Specifies whether Rest2Ldap should perform LDAP modify operations using the LDAP permissive modify |
| | | * control. By default Rest2Ldap will use the permissive modify control and use of the control is strongly |
| | | * recommended. |
| | | */ |
| | | public static final Option<Boolean> USE_PERMISSIVE_MODIFY = Option.withDefault(true); |
| | | /** |
| | | * Specifies whether Rest2Ldap should perform LDAP delete operations using the LDAP subtree delete control. By |
| | | * default Rest2Ldap will use the subtree delete control and use of the control is strongly recommended. |
| | | */ |
| | | public static final Option<Boolean> USE_SUBTREE_DELETE = Option.withDefault(true); |
| | | |
| | | /** |
| | | * Adapts a {@code Throwable} to a {@code ResourceException}. If the |
| | | * {@code Throwable} is an LDAP {@link LdapException} then an |
| | | * appropriate {@code ResourceException} is returned, otherwise an |
| | | * {@code InternalServerErrorException} is returned. |
| | | * Creates a new {@link Rest2Ldap} instance using the provided options and {@link Resource resources}. |
| | | * Applications should call {@link #newRequestHandlerFor(String)} to obtain a request handler for a specific |
| | | * resource. |
| | | * <p> |
| | | * The supported options are defined in this class. |
| | | * |
| | | * @param t |
| | | * The {@code Throwable} to be converted. |
| | | * @return The equivalent resource exception. |
| | | * @param options The configuration options for interactions with the backend LDAP server. The set of available |
| | | * options are provided in this class. |
| | | * @param resources The list of resources. |
| | | * @return A new Rest2Ldap instance from which REST request handlers can be obtained. |
| | | */ |
| | | public static ResourceException asResourceException(final Throwable t) { |
| | | int resourceResultCode; |
| | | try { |
| | | throw t; |
| | | } catch (final ResourceException e) { |
| | | return e; |
| | | } catch (final AssertionFailureException e) { |
| | | resourceResultCode = ResourceException.VERSION_MISMATCH; |
| | | } catch (final ConstraintViolationException e) { |
| | | final ResultCode rc = e.getResult().getResultCode(); |
| | | if (rc.equals(ResultCode.ENTRY_ALREADY_EXISTS)) { |
| | | resourceResultCode = ResourceException.VERSION_MISMATCH; // Consistent with MVCC. |
| | | } else { |
| | | // Schema violation, etc. |
| | | resourceResultCode = ResourceException.BAD_REQUEST; |
| | | } |
| | | } catch (final AuthenticationException e) { |
| | | resourceResultCode = 401; |
| | | } catch (final AuthorizationException e) { |
| | | resourceResultCode = ResourceException.FORBIDDEN; |
| | | } catch (final ConnectionException e) { |
| | | resourceResultCode = ResourceException.UNAVAILABLE; |
| | | } catch (final EntryNotFoundException e) { |
| | | resourceResultCode = ResourceException.NOT_FOUND; |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | resourceResultCode = ResourceException.INTERNAL_ERROR; |
| | | } catch (final TimeoutResultException e) { |
| | | resourceResultCode = 408; |
| | | } catch (final LdapException e) { |
| | | final ResultCode rc = e.getResult().getResultCode(); |
| | | if (rc.equals(ResultCode.ADMIN_LIMIT_EXCEEDED)) { |
| | | resourceResultCode = 413; // Request Entity Too Large |
| | | } else if (rc.equals(ResultCode.SIZE_LIMIT_EXCEEDED)) { |
| | | resourceResultCode = 413; // Request Entity Too Large |
| | | } else { |
| | | resourceResultCode = ResourceException.INTERNAL_ERROR; |
| | | } |
| | | } catch (final Throwable tmp) { |
| | | resourceResultCode = ResourceException.INTERNAL_ERROR; |
| | | } |
| | | return newResourceException(resourceResultCode, t.getMessage(), t); |
| | | public static Rest2Ldap rest2Ldap(final Options options, final Collection<Resource> resources) { |
| | | return new Rest2Ldap(options, resources); |
| | | } |
| | | |
| | | /** |
| | | * Returns a builder for incrementally constructing LDAP resource |
| | | * collections. |
| | | * Creates a new {@link Rest2Ldap} instance using the provided options and {@link Resource resources}. |
| | | * Applications should call {@link #newRequestHandlerFor(String)} to obtain a request handler for a specific |
| | | * resource. |
| | | * <p> |
| | | * The supported options are defined in this class. |
| | | * |
| | | * @return An LDAP resource collection builder. |
| | | * @param options The configuration options for interactions with the backend LDAP server. The set of available |
| | | * options are provided in this class. |
| | | * @param resources The list of resources. |
| | | * @return A new Rest2Ldap instance from which REST request handlers can be obtained. |
| | | */ |
| | | public static Builder builder() { |
| | | return new Builder(); |
| | | public static Rest2Ldap rest2Ldap(final Options options, final Resource... resources) { |
| | | return rest2Ldap(options, Arrays.asList(resources)); |
| | | } |
| | | |
| | | /** |
| | | * Creates a new connection factory using the named configuration in the |
| | | * provided JSON list of factory configurations. See the sample |
| | | * configuration file for a detailed description of its content. |
| | | * Creates a new {@link Resource resource} definition with the provided resource ID. |
| | | * |
| | | * @param configuration |
| | | * The JSON configuration. |
| | | * @param name |
| | | * The name of the connection factory configuration to be parsed. |
| | | * @param trustManager |
| | | * The trust manager to use for secure connection. Can be {@code null} |
| | | * to use the default JVM trust manager. |
| | | * @param keyManager |
| | | * The key manager to use for secure connection. Can be {@code null} |
| | | * to use the default JVM key manager. |
| | | * @param providerClassLoader |
| | | * The {@link ClassLoader} used to fetch the |
| | | * {@link org.forgerock.opendj.ldap.spi.TransportProvider}. |
| | | * This can be useful in OSGI environments. |
| | | * @return A new connection factory using the provided JSON configuration. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | * @param resourceId |
| | | * The resource ID. |
| | | * @return A new resource definition with the provided resource ID. |
| | | */ |
| | | public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, |
| | | final String name, |
| | | final TrustManager trustManager, |
| | | final X509KeyManager keyManager, |
| | | final ClassLoader providerClassLoader) { |
| | | final JsonValue normalizedConfiguration = |
| | | normalizeConnectionFactory(configuration, name, 0); |
| | | return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader); |
| | | public static Resource resource(final String resourceId) { |
| | | return new Resource(resourceId); |
| | | } |
| | | |
| | | /** |
| | | * Creates a new connection factory using the named configuration in the |
| | | * provided JSON list of factory configurations. See the sample |
| | | * configuration file for a detailed description of its content. |
| | | * Creates a new {@link SubResourceCollection collection} sub-resource definition whose members will be resources |
| | | * having the provided resource ID or its sub-types. |
| | | * |
| | | * @param configuration |
| | | * The JSON configuration. |
| | | * @param name |
| | | * The name of the connection factory configuration to be parsed. |
| | | * @param trustManager |
| | | * The trust manager to use for secure connection. Can be {@code null} |
| | | * to use the default JVM trust manager. |
| | | * @param keyManager |
| | | * The key manager to use for secure connection. Can be {@code null} |
| | | * to use the default JVM key manager. |
| | | * @return A new connection factory using the provided JSON configuration. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | * @param resourceId |
| | | * The type of resource contained in the sub-resource collection. |
| | | * @return A new sub-resource definition with the provided resource ID. |
| | | */ |
| | | public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, |
| | | final String name, final TrustManager trustManager, final X509KeyManager keyManager) { |
| | | return configureConnectionFactory(configuration, name, trustManager, keyManager, null); |
| | | public static SubResourceCollection collectionOf(final String resourceId) { |
| | | return new SubResourceCollection(resourceId); |
| | | } |
| | | |
| | | /** |
| | | * Returns an property mapper which maps a single JSON attribute to a JSON |
| | | * constant. |
| | | * Creates a new {@link SubResourceSingleton singleton} sub-resource definition which will reference a single |
| | | * resource having the specified resource ID. |
| | | * |
| | | * @param resourceId |
| | | * The type of resource referenced by the sub-resource singleton. |
| | | * @return A new sub-resource definition with the provided resource ID. |
| | | */ |
| | | public static SubResourceSingleton singletonOf(final String resourceId) { |
| | | return new SubResourceSingleton(resourceId); |
| | | } |
| | | |
| | | /** |
| | | * Returns a property mapper which maps a JSON property containing the resource type to its associated LDAP |
| | | * object classes. |
| | | * |
| | | * @return The property mapper. |
| | | */ |
| | | public static PropertyMapper resourceType() { |
| | | return ResourceTypePropertyMapper.INSTANCE; |
| | | } |
| | | |
| | | /** |
| | | * Returns a property mapper which maps a single JSON attribute to a JSON constant. |
| | | * |
| | | * @param value |
| | | * The constant JSON value (a Boolean, Number, String, Map, or |
| | | * List). |
| | | * The constant JSON value (a Boolean, Number, String, Map, or List). |
| | | * @return The property mapper. |
| | | */ |
| | | public static PropertyMapper constant(final Object value) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Returns an property mapper which maps JSON objects to LDAP attributes. |
| | | * Returns a property mapper which maps JSON objects to LDAP attributes. |
| | | * |
| | | * @return The property mapper. |
| | | */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * Returns an property mapper which provides a mapping from a JSON value to |
| | | * a single DN valued LDAP attribute. |
| | | * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute. |
| | | * |
| | | * @param attribute |
| | | * The DN valued LDAP attribute to be mapped. |
| | | * The DN valued LDAP attribute to be mapped. |
| | | * @param baseDN |
| | | * The search base DN for performing reverse lookups. |
| | | * The search base DN for performing reverse lookups. |
| | | * @param primaryKey |
| | | * The search primary key LDAP attribute to use for performing |
| | | * reverse lookups. |
| | | * The search primary key LDAP attribute to use for performing reverse lookups. |
| | | * @param mapper |
| | | * An property mapper which will be used to map LDAP attributes |
| | | * in the referenced entry. |
| | | * An property mapper which will be used to map LDAP attributes in the referenced entry. |
| | | * @return The property mapper. |
| | | */ |
| | | public static ReferencePropertyMapper reference(final AttributeDescription attribute, |
| | | final DN baseDN, final AttributeDescription primaryKey, |
| | | public static ReferencePropertyMapper reference(final AttributeDescription attribute, final DN baseDN, |
| | | final AttributeDescription primaryKey, |
| | | final PropertyMapper mapper) { |
| | | return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDN, primaryKey, mapper); |
| | | } |
| | | |
| | | /** |
| | | * Returns an property mapper which provides a mapping from a JSON value to |
| | | * a single DN valued LDAP attribute. |
| | | * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute. |
| | | * |
| | | * @param attribute |
| | | * The DN valued LDAP attribute to be mapped. |
| | | * The DN valued LDAP attribute to be mapped. |
| | | * @param baseDN |
| | | * The search base DN for performing reverse lookups. |
| | | * The search base DN for performing reverse lookups. |
| | | * @param primaryKey |
| | | * The search primary key LDAP attribute to use for performing |
| | | * reverse lookups. |
| | | * The search primary key LDAP attribute to use for performing reverse lookups. |
| | | * @param mapper |
| | | * An property mapper which will be used to map LDAP attributes |
| | | * in the referenced entry. |
| | | * An property mapper which will be used to map LDAP attributes in the referenced entry. |
| | | * @return The property mapper. |
| | | */ |
| | | public static ReferencePropertyMapper reference(final String attribute, final String baseDN, |
| | | final String primaryKey, final PropertyMapper mapper) { |
| | | return reference(AttributeDescription.valueOf(attribute), DN.valueOf(baseDN), |
| | | AttributeDescription.valueOf(primaryKey), mapper); |
| | | return reference(AttributeDescription.valueOf(attribute), |
| | | DN.valueOf(baseDN), |
| | | AttributeDescription.valueOf(primaryKey), |
| | | mapper); |
| | | } |
| | | |
| | | /** |
| | | * Returns an property mapper which provides a simple mapping from a JSON |
| | | * value to a single LDAP attribute. |
| | | * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. |
| | | * |
| | | * @param attribute |
| | | * The LDAP attribute to be mapped. |
| | | * The LDAP attribute to be mapped. |
| | | * @return The property mapper. |
| | | */ |
| | | public static SimplePropertyMapper simple(final AttributeDescription attribute) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Returns an property mapper which provides a simple mapping from a JSON |
| | | * value to a single LDAP attribute. |
| | | * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. |
| | | * |
| | | * @param attribute |
| | | * The LDAP attribute to be mapped. |
| | | * The LDAP attribute to be mapped. |
| | | * @return The property mapper. |
| | | */ |
| | | public static SimplePropertyMapper simple(final String attribute) { |
| | | return simple(AttributeDescription.valueOf(attribute)); |
| | | } |
| | | |
| | | private static ConnectionFactory configureConnectionFactory(final JsonValue configuration, |
| | | final TrustManager trustManager, |
| | | final X509KeyManager keyManager, |
| | | final ClassLoader providerClassLoader) { |
| | | final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong(); |
| | | final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS); |
| | | |
| | | final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong(); |
| | | final Duration heartBeatTimeout = duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS); |
| | | |
| | | final Options options = Options.defaultOptions() |
| | | .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader) |
| | | .set(HEARTBEAT_ENABLED, true) |
| | | .set(HEARTBEAT_INTERVAL, heartBeatInterval) |
| | | .set(HEARTBEAT_TIMEOUT, heartBeatTimeout) |
| | | .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval); |
| | | |
| | | // Parse pool parameters, |
| | | final int connectionPoolSize = |
| | | Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1); |
| | | |
| | | // Parse authentication parameters. |
| | | if (configuration.isDefined("authentication")) { |
| | | final JsonValue authn = configuration.get("authentication"); |
| | | if (authn.isDefined("simple")) { |
| | | final JsonValue simple = authn.get("simple"); |
| | | final BindRequest bindRequest = |
| | | Requests.newSimpleBindRequest(simple.get("bindDN").required().asString(), |
| | | simple.get("bindPassword").required().asString().toCharArray()); |
| | | options.set(AUTHN_BIND_REQUEST, bindRequest); |
| | | /** |
| | | * Adapts a {@code Throwable} to a {@code ResourceException}. If the {@code Throwable} is an LDAP |
| | | * {@link LdapException} then an appropriate {@code ResourceException} is returned, otherwise an {@code |
| | | * InternalServerErrorException} is returned. |
| | | * @param t |
| | | * The {@code Throwable} to be converted. |
| | | * @return The equivalent resource exception. |
| | | */ |
| | | public static ResourceException asResourceException(final Throwable t) { |
| | | try { |
| | | throw t; |
| | | } catch (final ResourceException e) { |
| | | return e; |
| | | } catch (final AssertionFailureException e) { |
| | | return new PreconditionFailedException(e); |
| | | } catch (final ConstraintViolationException e) { |
| | | final ResultCode rc = e.getResult().getResultCode(); |
| | | if (rc.equals(ENTRY_ALREADY_EXISTS)) { |
| | | return new PreconditionFailedException(e); // Consistent with MVCC. |
| | | } else { |
| | | throw newLocalizedIllegalArgumentException(ERR_CONFIG_INVALID_AUTHENTICATION.get()); |
| | | return new BadRequestException(e); // Schema violation, etc. |
| | | } |
| | | } |
| | | |
| | | // Parse SSL/StartTLS parameters. |
| | | final ConnectionSecurity connectionSecurity = |
| | | configuration.get("connectionSecurity").defaultTo(ConnectionSecurity.NONE).asEnum( |
| | | ConnectionSecurity.class); |
| | | if (connectionSecurity != ConnectionSecurity.NONE) { |
| | | try { |
| | | // Configure SSL. |
| | | final SSLContextBuilder builder = new SSLContextBuilder(); |
| | | builder.setTrustManager(trustManager); |
| | | final String sslCertAlias = configuration.get("sslCertAlias").asString(); |
| | | builder.setKeyManager(sslCertAlias != null |
| | | ? useSingleCertificate(sslCertAlias, keyManager) |
| | | : keyManager); |
| | | options.set(SSL_CONTEXT, builder.getSSLContext()); |
| | | options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS); |
| | | } catch (GeneralSecurityException e) { |
| | | // Rethrow as unchecked exception. |
| | | throw new IllegalArgumentException(e); |
| | | } |
| | | } |
| | | |
| | | // Parse primary data center. |
| | | final JsonValue primaryLDAPServers = configuration.get("primaryLDAPServers"); |
| | | if (!primaryLDAPServers.isList() || primaryLDAPServers.size() == 0) { |
| | | throw new IllegalArgumentException("No primaryLDAPServers"); |
| | | } |
| | | final ConnectionFactory primary = parseLDAPServers(primaryLDAPServers, connectionPoolSize, options); |
| | | |
| | | // Parse secondary data center(s). |
| | | final JsonValue secondaryLDAPServers = configuration.get("secondaryLDAPServers"); |
| | | ConnectionFactory secondary = null; |
| | | if (secondaryLDAPServers.isList()) { |
| | | if (secondaryLDAPServers.size() > 0) { |
| | | secondary = parseLDAPServers(secondaryLDAPServers, connectionPoolSize, options); |
| | | } |
| | | } else if (!secondaryLDAPServers.isNull()) { |
| | | throw newLocalizedIllegalArgumentException(ERR_CONFIG_INVALID_SECONDARY_LDAP_SERVER.get()); |
| | | } |
| | | |
| | | // Create fail-over. |
| | | if (secondary != null) { |
| | | return newFailoverLoadBalancer(asList(primary, secondary), options); |
| | | } else { |
| | | return primary; |
| | | } |
| | | } |
| | | |
| | | private static JsonValue normalizeConnectionFactory(final JsonValue configuration, |
| | | final String name, final int depth) { |
| | | // Protect against infinite recursion in the configuration. |
| | | if (depth > 100) { |
| | | throw newLocalizedIllegalArgumentException(ERR_CONFIG_SERVER_CIRCULAR_DEPENDENCIES.get(name)); |
| | | } |
| | | |
| | | final JsonValue current = configuration.get(name).required(); |
| | | if (current.isDefined("inheritFrom")) { |
| | | // Inherit missing fields from inherited configuration. |
| | | final JsonValue parent = |
| | | normalizeConnectionFactory(configuration, |
| | | current.get("inheritFrom").asString(), depth + 1); |
| | | final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap()); |
| | | normalized.putAll(current.asMap()); |
| | | normalized.remove("inheritFrom"); |
| | | return new JsonValue(normalized); |
| | | } else { |
| | | // No normalization required. |
| | | return current; |
| | | } |
| | | } |
| | | |
| | | private static ConnectionFactory parseLDAPServers(JsonValue config, int poolSize, Options options) { |
| | | final List<ConnectionFactory> servers = new ArrayList<>(config.size()); |
| | | for (final JsonValue server : config) { |
| | | final String host = server.get("hostname").required().asString(); |
| | | final int port = server.get("port").required().asInteger(); |
| | | final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options); |
| | | if (poolSize > 1) { |
| | | servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS)); |
| | | } catch (final AuthenticationException e) { |
| | | return new PermanentException(401, null, e); // Unauthorized |
| | | } catch (final AuthorizationException e) { |
| | | return new ForbiddenException(e); |
| | | } catch (final ConnectionException e) { |
| | | return new ServiceUnavailableException(e); |
| | | } catch (final EntryNotFoundException e) { |
| | | return new NotFoundException(e); |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | return new InternalServerErrorException(e); |
| | | } catch (final TimeoutResultException e) { |
| | | return new RetryableException(408, null, e); // Request Timeout |
| | | } catch (final LdapException e) { |
| | | final ResultCode rc = e.getResult().getResultCode(); |
| | | if (rc.equals(ADMIN_LIMIT_EXCEEDED) || rc.equals(SIZE_LIMIT_EXCEEDED)) { |
| | | return new PermanentException(413, null, e); // Payload Too Large (Request Entity Too Large) |
| | | } else { |
| | | servers.add(factory); |
| | | return new InternalServerErrorException(e); |
| | | } |
| | | } |
| | | if (servers.size() > 1) { |
| | | return newRoundRobinLoadBalancer(servers, options); |
| | | } else { |
| | | return servers.get(0); |
| | | } catch (final Throwable tmp) { |
| | | return new InternalServerErrorException(t); |
| | | } |
| | | } |
| | | |
| | | private Rest2Ldap() { |
| | | // Prevent instantiation. |
| | | private final Map<String, Resource> resources = new LinkedHashMap<>(); |
| | | private final Options options; |
| | | |
| | | private Rest2Ldap(final Options options, final Collection<Resource> resources) { |
| | | this.options = options; |
| | | for (final Resource resource : resources) { |
| | | this.resources.put(resource.getResourceId(), resource); |
| | | } |
| | | // Now build the model. |
| | | for (final Resource resource : resources) { |
| | | resource.build(this); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Returns a {@link RequestHandler} which will handle requests to the named resource and any of its sub-resources. |
| | | * |
| | | * @param resourceId |
| | | * The resource ID. |
| | | * @return A {@link RequestHandler} which will handle requests to the named resource. |
| | | */ |
| | | public RequestHandler newRequestHandlerFor(final String resourceId) { |
| | | Reject.ifTrue(!resources.containsKey(resourceId), "unrecognized resource '" + resourceId + "'"); |
| | | final SubResourceSingleton root = singletonOf(resourceId); |
| | | root.build(this, null); |
| | | return root.addRoutes(new Router()); |
| | | } |
| | | |
| | | Options getOptions() { |
| | | return options; |
| | | } |
| | | |
| | | Resource getResource(final String resourceId) { |
| | | return resources.get(resourceId); |
| | | } |
| | | } |
| | |
| | | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | import static org.forgerock.http.handler.HttpClientHandler.*; |
| | | import static org.forgerock.opendj.ldap.KeyManagers.*; |
| | | import static org.forgerock.opendj.ldap.TrustManagers.checkUsingTrustStore; |
| | | import static org.forgerock.opendj.ldap.TrustManagers.trustAll; |
| | | import static org.forgerock.http.util.Json.readJsonLenient; |
| | | import static org.forgerock.http.handler.HttpClientHandler.OPTION_KEY_MANAGERS; |
| | | import static org.forgerock.http.handler.HttpClientHandler.OPTION_TRUST_MANAGERS; |
| | | import static org.forgerock.json.JsonValueFunctions.duration; |
| | | import static org.forgerock.json.JsonValueFunctions.enumConstant; |
| | | import static org.forgerock.json.JsonValueFunctions.setOf; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.configureConnectionFactory; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newLocalizedIllegalArgumentException; |
| | | import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler; |
| | | import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LdapJsonConfigurator.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException; |
| | | import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.*; |
| | | import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.newSASLPlainStrategy; |
| | | import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.newSearchThenBindStrategy; |
| | | import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.newSimpleBindStrategy; |
| | | import static org.forgerock.opendj.rest2ldap.authz.Authorizations.*; |
| | | import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.*; |
| | | import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.*; |
| | | import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.newConditionalFilter; |
| | | import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.httpBasicExtractor; |
| | | import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.newCustomHeaderExtractor; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | import static org.forgerock.util.Utils.closeSilently; |
| | | import static org.forgerock.util.Utils.joinAsString; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.readPasswordFromFile; |
| | | |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.net.URI; |
| | | import java.net.URISyntaxException; |
| | | import java.net.URL; |
| | | import java.security.GeneralSecurityException; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.ScheduledExecutorService; |
| | | |
| | |
| | | import javax.net.ssl.TrustManager; |
| | | import javax.net.ssl.X509KeyManager; |
| | | |
| | | import org.forgerock.openig.oauth2.AccessTokenInfo; |
| | | import org.forgerock.openig.oauth2.AccessTokenException; |
| | | import org.forgerock.openig.oauth2.AccessTokenResolver; |
| | | import org.forgerock.openig.oauth2.resolver.CachingAccessTokenResolver; |
| | | import org.forgerock.openig.oauth2.resolver.OpenAmAccessTokenResolver; |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.HttpApplication; |
| | |
| | | import org.forgerock.http.io.Buffer; |
| | | import org.forgerock.http.protocol.Headers; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.json.resource.http.CrestHttp; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.rest2ldap.Rest2LDAP.KeyManagerType; |
| | | import org.forgerock.opendj.rest2ldap.Rest2LDAP.TrustManagerType; |
| | | import org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategy; |
| | | import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter; |
| | | import org.forgerock.openig.oauth2.AccessTokenException; |
| | | import org.forgerock.openig.oauth2.AccessTokenInfo; |
| | | import org.forgerock.openig.oauth2.AccessTokenResolver; |
| | | import org.forgerock.openig.oauth2.resolver.CachingAccessTokenResolver; |
| | | import org.forgerock.openig.oauth2.resolver.OpenAmAccessTokenResolver; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.Factory; |
| | | import org.forgerock.util.Function; |
| | |
| | | |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** URL to the JSON configuration file. */ |
| | | protected final URL configurationUrl; |
| | | /** The name of the JSON configuration directory in which config.json and rest2ldap/rest2ldap.json are located. */ |
| | | protected final File configDirectory; |
| | | |
| | | /** Schema used to perform DN validations. */ |
| | | protected final Schema schema; |
| | |
| | | } |
| | | |
| | | /** |
| | | * Default constructor called by the HTTP Framework which will use the default configuration file location. |
| | | * Default constructor called by the HTTP Framework which will use the default configuration directory. |
| | | */ |
| | | public Rest2LdapHttpApplication() { |
| | | this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json"); |
| | | try { |
| | | // The null check is required for unit test mocks because the resource does not exist. |
| | | final URL configUrl = getClass().getResource("/config.json"); |
| | | this.configDirectory = configUrl != null ? new File(configUrl.toURI()).getParentFile() : null; |
| | | } catch (final URISyntaxException e) { |
| | | throw new IllegalStateException(e); |
| | | } |
| | | this.schema = Schema.getDefaultSchema(); |
| | | } |
| | | |
| | | /** |
| | | * Creates a new Rest2LDAP HTTP application using the provided configuration URL. |
| | | * Creates a new Rest2LDAP HTTP application using the provided configuration directory. |
| | | * |
| | | * @param configurationURL |
| | | * The URL to the JSON configuration file |
| | | * @param configDirectory |
| | | * The name of the JSON configuration directory in which config.json and rest2ldap/rest2ldap.json are |
| | | * located. |
| | | * @param schema |
| | | * The {@link Schema} used to perform DN validations |
| | | * The {@link Schema} used to perform DN validations |
| | | */ |
| | | public Rest2LdapHttpApplication(final URL configurationURL, final Schema schema) { |
| | | this.configurationUrl = checkNotNull(configurationURL, "configurationURL cannot be null"); |
| | | public Rest2LdapHttpApplication(final File configDirectory, final Schema schema) { |
| | | this.configDirectory = checkNotNull(configDirectory, "configDirectory cannot be null"); |
| | | this.schema = checkNotNull(schema, "schema cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public final Handler start() throws HttpApplicationException { |
| | | try { |
| | | final JsonValue configuration = readJson(configurationUrl); |
| | | logger.info(INFO_REST2LDAP_STARTING.get(configDirectory)); |
| | | |
| | | executorService = Executors.newSingleThreadScheduledExecutor(); |
| | | configureSecurity(configuration.get("security")); |
| | | configureConnectionFactories(configuration.get("ldapConnectionFactories")); |
| | | return Handlers.chainOf( |
| | | CrestHttp.newHttpHandler(configureRest2Ldap(configuration)), |
| | | new ErrorLoggerFilter(), |
| | | buildAuthorizationFilter(configuration.get("authorization").required())); |
| | | |
| | | final JsonValue config = readJson(new File(configDirectory, "config.json")); |
| | | configureSecurity(config.get("security")); |
| | | configureConnectionFactories(config.get("ldapConnectionFactories")); |
| | | final Filter authorizationFilter = buildAuthorizationFilter(config.get("authorization").required()); |
| | | return Handlers.chainOf(newHttpHandler(configureRest2Ldap(configDirectory)), |
| | | new ErrorLoggerFilter(), |
| | | authorizationFilter); |
| | | } catch (final Exception e) { |
| | | final LocalizableMessage errorMsg = ERR_FAIL_PARSE_CONFIGURATION.get(e.getLocalizedMessage()); |
| | | logger.error(errorMsg, e); |
| | |
| | | } |
| | | } |
| | | |
| | | private static JsonValue readJson(final URL resource) throws IOException { |
| | | try (InputStream in = resource.openStream()) { |
| | | return new JsonValue(readJsonLenient(in)); |
| | | } |
| | | } |
| | | |
| | | private static RequestHandler configureRest2Ldap(final JsonValue configuration) { |
| | | final JsonValue mappings = configuration.get("mappings").required(); |
| | | final Router router = new Router(); |
| | | for (final String mappingUrl : mappings.keys()) { |
| | | final JsonValue mapping = mappings.get(mappingUrl); |
| | | router.addRoute(Router.uriTemplate(mappingUrl), Rest2Ldap.builder().configureMapping(mapping).build()); |
| | | } |
| | | return router; |
| | | private static RequestHandler configureRest2Ldap(final File configDirectory) throws IOException { |
| | | final File rest2LdapConfigDirectory = new File(configDirectory, "rest2ldap"); |
| | | final Options options = configureOptions(readJson(new File(rest2LdapConfigDirectory, "rest2ldap.json"))); |
| | | final File endpointsDirectory = new File(rest2LdapConfigDirectory, "endpoints"); |
| | | return configureEndpoints(endpointsDirectory, options); |
| | | } |
| | | |
| | | private void configureSecurity(final JsonValue configuration) { |
| | | try { |
| | | trustManager = configureTrustManager(configuration, TrustManagerType.JVM); |
| | | } catch (GeneralSecurityException | IOException e) { |
| | | throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER |
| | | .get(configuration.getPointer(), e.getLocalizedMessage()).toString(), e); |
| | | } |
| | | |
| | | try { |
| | | keyManager = configureKeyManager(configuration, KeyManagerType.JVM); |
| | | } catch (GeneralSecurityException | IOException e) { |
| | | throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER |
| | | .get(configuration.getPointer(), e.getLocalizedMessage()).toString(), e); |
| | | } |
| | | } |
| | | |
| | | private TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing) |
| | | throws GeneralSecurityException, IOException { |
| | | // Parse trust store configuration. |
| | | final TrustManagerType trustManagerType = |
| | | config.get("trustManager").defaultTo(defaultIfMissing).as(enumConstant(TrustManagerType.class)); |
| | | switch (trustManagerType) { |
| | | case TRUSTALL: |
| | | return trustAll(); |
| | | case JVM: |
| | | return null; |
| | | case FILE: |
| | | final String fileName = config.get("fileBasedTrustManagerFile").required().asString(); |
| | | final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString(); |
| | | final String password = passwordFile != null |
| | | ? readPasswordFromFile(passwordFile) |
| | | : config.get("fileBasedTrustManagerPassword").asString(); |
| | | final String type = config.get("fileBasedTrustManagerType").asString(); |
| | | return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type); |
| | | default: |
| | | throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType); |
| | | } |
| | | } |
| | | |
| | | private X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing) |
| | | throws GeneralSecurityException, IOException { |
| | | // Parse trust store configuration. |
| | | final KeyManagerType keyManagerType = config.get("keyManager").defaultTo(defaultIfMissing) |
| | | .as(enumConstant(KeyManagerType.class)); |
| | | switch (keyManagerType) { |
| | | case JVM: |
| | | return useJvmDefaultKeyStore(); |
| | | case KEYSTORE: |
| | | final String fileName = config.get("keyStoreFile").required().asString(); |
| | | final String passwordFile = config.get("keyStorePasswordFile").asString(); |
| | | final String password = passwordFile != null |
| | | ? readPasswordFromFile(passwordFile) |
| | | : config.get("keyStorePassword").asString(); |
| | | final String format = config.get("keyStoreFormat").asString(); |
| | | final String provider = config.get("keyStoreProvider").asString(); |
| | | return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, format, provider); |
| | | case PKCS11: |
| | | final String pkcs11PasswordFile = config.get("pkcs11PasswordFile").asString(); |
| | | return usePKCS11Token(pkcs11PasswordFile != null |
| | | ? readPasswordFromFile(pkcs11PasswordFile).toCharArray() |
| | | : null); |
| | | default: |
| | | throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType); |
| | | } |
| | | trustManager = configureTrustManager(configuration); |
| | | keyManager = configureKeyManager(configuration); |
| | | } |
| | | |
| | | private void configureConnectionFactories(final JsonValue config) { |
| | |
| | | case SASL_PLAIN: |
| | | return buildSaslBindStrategy(config); |
| | | default: |
| | | throw newLocalizedIllegalArgumentException(ERR_CONFIG_UNSUPPORTED_BIND_STRATEGY.get( |
| | | strategy, BindStrategy.listValues())); |
| | | throw new LocalizedIllegalArgumentException( |
| | | ERR_CONFIG_UNSUPPORTED_BIND_STRATEGY.get(strategy, BindStrategy.listValues())); |
| | | } |
| | | } |
| | | |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static java.util.Arrays.asList; |
| | | import static java.util.Collections.emptyList; |
| | | import static org.forgerock.http.routing.RoutingMode.STARTS_WITH; |
| | | import static org.forgerock.http.routing.Version.version; |
| | | import static org.forgerock.http.util.Json.readJsonLenient; |
| | | import static org.forgerock.json.JsonValueFunctions.enumConstant; |
| | | import static org.forgerock.json.JsonValueFunctions.pointer; |
| | | import static org.forgerock.json.JsonValueFunctions.setOf; |
| | | import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher; |
| | | import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_MONITORING_INTERVAL; |
| | | import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool; |
| | | import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer; |
| | | import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer; |
| | | import static org.forgerock.opendj.ldap.KeyManagers.useJvmDefaultKeyStore; |
| | | import static org.forgerock.opendj.ldap.KeyManagers.useKeyStoreFile; |
| | | import static org.forgerock.opendj.ldap.KeyManagers.usePKCS11Token; |
| | | import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate; |
| | | import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*; |
| | | import static org.forgerock.opendj.ldap.TrustManagers.checkUsingTrustStore; |
| | | import static org.forgerock.opendj.ldap.TrustManagers.trustAll; |
| | | import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException; |
| | | import static org.forgerock.util.time.Duration.duration; |
| | | |
| | | import java.io.BufferedReader; |
| | | import java.io.File; |
| | | import java.io.FileFilter; |
| | | import java.io.FileInputStream; |
| | | import java.io.FileReader; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.security.GeneralSecurityException; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import javax.net.ssl.TrustManager; |
| | | import javax.net.ssl.X509KeyManager; |
| | | |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.LDAPConnectionFactory; |
| | | import org.forgerock.opendj.ldap.SSLContextBuilder; |
| | | import org.forgerock.opendj.ldap.requests.BindRequest; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.util.Options; |
| | | import org.forgerock.util.time.Duration; |
| | | |
| | | /** Provides core factory methods and builders for constructing Rest2Ldap endpoints from JSON configuration. */ |
| | | public final class Rest2LdapJsonConfigurator { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** |
| | | * Parses Rest2Ldap configuration options. The JSON configuration must have the following format: |
| | | * <p> |
| | | * <pre> |
| | | * { |
| | | * "readOnUpdatePolicy": "controls", |
| | | * "useSubtreeDelete": true, |
| | | * "usePermissiveModify": true, |
| | | * "useMvcc": true |
| | | * "mvccAttribute": "etag" |
| | | * } |
| | | * </pre> |
| | | * <p> |
| | | * See the sample configuration file for a detailed description of its content. |
| | | * |
| | | * @param config |
| | | * The JSON configuration. |
| | | * @return The parsed Rest2Ldap configuration options. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public static Options configureOptions(final JsonValue config) { |
| | | final Options options = Options.defaultOptions(); |
| | | |
| | | options.set(READ_ON_UPDATE_POLICY, |
| | | config.get("readOnUpdatePolicy").defaultTo(CONTROLS).as(enumConstant(ReadOnUpdatePolicy.class))); |
| | | |
| | | // Default to false, even though it is supported by OpenDJ, because it requires additional permissions. |
| | | options.set(USE_SUBTREE_DELETE, config.get("useSubtreeDelete").defaultTo(false).asBoolean()); |
| | | |
| | | // Default to true because it is supported by OpenDJ and does not require additional permissions. |
| | | options.set(USE_PERMISSIVE_MODIFY, config.get("usePermissiveModify").defaultTo(false).asBoolean()); |
| | | |
| | | options.set(USE_MVCC, config.get("useMvcc").defaultTo(true).asBoolean()); |
| | | options.set(MVCC_ATTRIBUTE, config.get("mvccAttribute").defaultTo("etag").asString()); |
| | | |
| | | return options; |
| | | } |
| | | |
| | | /** |
| | | * Parses a list of Rest2Ldap resource definitions. The JSON configuration must have the following format: |
| | | * <p> |
| | | * <pre> |
| | | * "top": { |
| | | * "isAbstract": true, |
| | | * "properties": { |
| | | * "_rev": { |
| | | * "type": "simple" |
| | | * "ldapAttribute": "etag", |
| | | * "writability": "readOnly" |
| | | * }, |
| | | * ... |
| | | * }, |
| | | * ... |
| | | * }, |
| | | * ... |
| | | * </pre> |
| | | * <p> |
| | | * See the sample configuration file for a detailed description of its content. |
| | | * |
| | | * @param config |
| | | * The JSON configuration. |
| | | * @return The parsed list of Rest2Ldap resource definitions. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public static List<Resource> configureResources(final JsonValue config) { |
| | | final JsonValue resourcesConfig = config.required().expect(Map.class); |
| | | final List<Resource> resources = new LinkedList<>(); |
| | | for (final String resourceId : resourcesConfig.keys()) { |
| | | resources.add(configureResource(resourceId, resourcesConfig.get(resourceId))); |
| | | } |
| | | return resources; |
| | | } |
| | | |
| | | /** |
| | | * Creates a new CREST {@link Router} using the provided endpoints configuration directory and Rest2Ldap options. |
| | | * The Rest2Ldap configuration typically has the following structure on disk: |
| | | * <ul> |
| | | * <li> config.json - contains the configuration for the LDAP connection factories and authorization |
| | | * <li> rest2ldap/rest2ldap.json - defines Rest2Ldap configuration options |
| | | * <li> rest2ldap/endpoints/{api} - a directory containing the endpoint's resource definitions for endpoint {api} |
| | | * <li> rest2ldap/endpoints/{api}/{resource-id}.json - the resource definitions for a specific version of API {api}. |
| | | * The name of the file, {resource-id}, determines which resource type definition in the mapping file will be |
| | | * used as the root resource. |
| | | * </ul> |
| | | * |
| | | * @param endpointsDirectory The directory representing the Rest2Ldap "endpoints" directory. |
| | | * @param options The Rest2Ldap configuration options. |
| | | * @return A new CREST {@link Router} configured using the provided options and endpoints. |
| | | * @throws IOException If the endpoints configuration cannot be read. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public static Router configureEndpoints(final File endpointsDirectory, final Options options) throws IOException { |
| | | final Router pathRouter = new Router(); |
| | | |
| | | final File[] endpoints = endpointsDirectory.listFiles(new FileFilter() { |
| | | @Override |
| | | public boolean accept(final File pathname) { |
| | | return pathname.isDirectory() && pathname.canRead(); |
| | | } |
| | | }); |
| | | |
| | | if (endpoints == null) { |
| | | throw new LocalizedIllegalArgumentException(ERR_INVALID_ENDPOINTS_DIRECTORY.get(endpointsDirectory)); |
| | | } |
| | | |
| | | for (final File endpoint : endpoints) { |
| | | final Router router = configureEndpoint(endpoint, options); |
| | | pathRouter.addRoute(requestUriMatcher(STARTS_WITH, endpoint.getName()), router); |
| | | } |
| | | return pathRouter; |
| | | } |
| | | |
| | | /** |
| | | * Creates a new CREST {@link Router} representing a single endpoint whose configuration is defined in the |
| | | * provided {@code endpointDirectory} parameter. The directory should contain a separate file for each supported |
| | | * version of the REST endpoint. The name of the file, excluding the suffix, identifies the resource definition |
| | | * which acts as the entry point into the endpoint. |
| | | * |
| | | * @param endpointDirectory The directory containing the endpoint's resource definitions, e.g. |
| | | * rest2ldap/routes/api would contain definitions for the "api" endpoint. |
| | | * @param options The Rest2Ldap configuration options. |
| | | * @return A new CREST {@link Router} configured using the provided options and endpoint mappings. |
| | | * @throws IOException If the endpoint configuration cannot be read. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public static Router configureEndpoint(final File endpointDirectory, final Options options) throws IOException { |
| | | final Router versionRouter = new Router(); |
| | | |
| | | final File[] endpointVersions = endpointDirectory.listFiles(new FileFilter() { |
| | | @Override |
| | | public boolean accept(final File pathname) { |
| | | return pathname.isFile() && pathname.canRead() && pathname.getName().endsWith(".json"); |
| | | } |
| | | }); |
| | | |
| | | if (endpointVersions == null) { |
| | | throw new LocalizedIllegalArgumentException(ERR_INVALID_ENDPOINT_DIRECTORY.get(endpointDirectory)); |
| | | } |
| | | |
| | | for (final File endpointVersion : endpointVersions) { |
| | | final JsonValue mappingConfig = readJson(endpointVersion); |
| | | final String version = mappingConfig.get("version").defaultTo("*").asString(); |
| | | final List<Resource> resourceTypes = configureResources(mappingConfig.get("resourceTypes")); |
| | | final Rest2Ldap rest2Ldap = rest2Ldap(options, resourceTypes); |
| | | |
| | | final String endpointVersionFileName = endpointVersion.getName(); |
| | | final int endIndex = endpointVersionFileName.lastIndexOf('.'); |
| | | final String rootResourceType = endpointVersionFileName.substring(0, endIndex); |
| | | final RequestHandler handler = rest2Ldap.newRequestHandlerFor(rootResourceType); |
| | | |
| | | if (version.equals("*")) { |
| | | versionRouter.setDefaultRoute(handler); |
| | | } else { |
| | | versionRouter.addRoute(version(version), handler); |
| | | } |
| | | |
| | | logger.debug(INFO_REST2LDAP_CREATING_ENDPOINT.get(endpointDirectory.getName(), version)); |
| | | } |
| | | |
| | | return versionRouter; |
| | | } |
| | | |
| | | static JsonValue readJson(final File resource) throws IOException { |
| | | try (InputStream in = new FileInputStream(resource)) { |
| | | return new JsonValue(readJsonLenient(in)); |
| | | } |
| | | } |
| | | |
| | | private static Resource configureResource(final String resourceId, final JsonValue config) { |
| | | final Resource resource = resource(resourceId) |
| | | .isAbstract(config.get("isAbstract").defaultTo(false).asBoolean()) |
| | | .superType(config.get("superType").asString()) |
| | | .objectClasses(config.get("objectClasses") |
| | | .defaultTo(emptyList()).asList(String.class).toArray(new String[0])) |
| | | .supportedActions(config.get("supportedActions") |
| | | .defaultTo(emptyList()) |
| | | .as(setOf(enumConstant(Action.class))).toArray(new Action[0])) |
| | | .resourceTypeProperty(config.get("resourceTypeProperty").as(pointer())) |
| | | .includeAllUserAttributesByDefault(config.get("includeAllUserAttributesByDefault") |
| | | .defaultTo(false).asBoolean()) |
| | | .excludedDefaultUserAttributes(config.get("excludedDefaultUserAttributes") |
| | | .defaultTo(Collections.emptyList()).asList(String.class)); |
| | | |
| | | final JsonValue properties = config.get("properties").expect(Map.class); |
| | | for (final String property : properties.keys()) { |
| | | resource.property(property, configurePropertyMapper(properties.get(property), property)); |
| | | } |
| | | |
| | | final JsonValue subResources = config.get("subResources").expect(Map.class); |
| | | for (final String urlTemplate : subResources.keys()) { |
| | | resource.subResource(configureSubResource(urlTemplate, subResources.get(urlTemplate))); |
| | | } |
| | | |
| | | return resource; |
| | | } |
| | | |
| | | private enum NamingStrategyType { CLIENTDNNAMING, CLIENTNAMING, SERVERNAMING } |
| | | private enum SubResourceType { COLLECTION, SINGLETON } |
| | | |
| | | private static SubResource configureSubResource(final String urlTemplate, final JsonValue config) { |
| | | final String dnTemplate = config.get("dnTemplate").defaultTo("").asString(); |
| | | final Boolean isReadOnly = config.get("isReadOnly").defaultTo(false).asBoolean(); |
| | | final String resourceId = config.get("resource").required().asString(); |
| | | |
| | | if (config.get("type").required().as(enumConstant(SubResourceType.class)) == SubResourceType.COLLECTION) { |
| | | final String[] glueObjectClasses = config.get("glueObjectClasses") |
| | | .defaultTo(emptyList()) |
| | | .asList(String.class) |
| | | .toArray(new String[0]); |
| | | |
| | | final SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate) |
| | | .dnTemplate(dnTemplate) |
| | | .isReadOnly(isReadOnly) |
| | | .glueObjectClasses(glueObjectClasses); |
| | | |
| | | final JsonValue namingStrategy = config.get("namingStrategy").required(); |
| | | switch (namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class))) { |
| | | case CLIENTDNNAMING: |
| | | collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString()); |
| | | break; |
| | | case CLIENTNAMING: |
| | | collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(), |
| | | namingStrategy.get("idAttribute").required().asString()); |
| | | break; |
| | | case SERVERNAMING: |
| | | collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(), |
| | | namingStrategy.get("idAttribute").required().asString()); |
| | | break; |
| | | } |
| | | |
| | | return collection; |
| | | } else { |
| | | return singletonOf(resourceId).urlTemplate(urlTemplate).dnTemplate(dnTemplate).isReadOnly(isReadOnly); |
| | | } |
| | | } |
| | | |
| | | private static PropertyMapper configurePropertyMapper(final JsonValue mapper, final String defaultLdapAttribute) { |
| | | switch (mapper.get("type").required().asString()) { |
| | | case "resourceType": |
| | | return resourceType(); |
| | | case "constant": |
| | | return constant(mapper.get("value").getObject()); |
| | | case "simple": |
| | | return simple(mapper.get("ldapAttribute").defaultTo(defaultLdapAttribute).required().asString()) |
| | | .defaultJsonValue(mapper.get("defaultJsonValue").getObject()) |
| | | .isBinary(mapper.get("isBinary").defaultTo(false).asBoolean()) |
| | | .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean()) |
| | | .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean()) |
| | | .writability(parseWritability(mapper)); |
| | | case "reference": |
| | | final String ldapAttribute = mapper.get("ldapAttribute") |
| | | .defaultTo(defaultLdapAttribute).required().asString(); |
| | | final String baseDN = mapper.get("baseDn").required().asString(); |
| | | final String primaryKey = mapper.get("primaryKey").required().asString(); |
| | | final PropertyMapper m = configurePropertyMapper(mapper.get("mapper").required(), primaryKey); |
| | | return reference(ldapAttribute, baseDN, primaryKey, m) |
| | | .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean()) |
| | | .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean()) |
| | | .searchFilter(mapper.get("searchFilter").defaultTo("(objectClass=*)").asString()) |
| | | .writability(parseWritability(mapper)); |
| | | case "object": |
| | | final JsonValue properties = mapper.get("properties"); |
| | | final ObjectPropertyMapper object = object(); |
| | | for (final String attribute : properties.keys()) { |
| | | object.property(attribute, configurePropertyMapper(properties.get(attribute), attribute)); |
| | | } |
| | | return object; |
| | | default: |
| | | throw newJsonValueException(mapper, ERR_CONFIG_NO_MAPPING_IN_CONFIGURATION.get( |
| | | "constant, simple, reference, object")); |
| | | } |
| | | } |
| | | |
| | | private static WritabilityPolicy parseWritability(final JsonValue mapper) { |
| | | if (mapper.isDefined("writability")) { |
| | | final String writability = mapper.get("writability").asString(); |
| | | if (writability.equalsIgnoreCase("readOnly")) { |
| | | return WritabilityPolicy.READ_ONLY; |
| | | } else if (writability.equalsIgnoreCase("readOnlyDiscardWrites")) { |
| | | return WritabilityPolicy.READ_ONLY_DISCARD_WRITES; |
| | | } else if (writability.equalsIgnoreCase("createOnly")) { |
| | | return WritabilityPolicy.CREATE_ONLY; |
| | | } else if (writability.equalsIgnoreCase("createOnlyDiscardWrites")) { |
| | | return WritabilityPolicy.CREATE_ONLY_DISCARD_WRITES; |
| | | } else if (writability.equalsIgnoreCase("readWrite")) { |
| | | return WritabilityPolicy.READ_WRITE; |
| | | } else { |
| | | throw newJsonValueException(mapper, ERR_CONFIG_UNKNOWN_WRITABILITY.get(writability, |
| | | "readOnly, readOnlyDiscardWrites, createOnly, createOnlyDiscardWrites, readWrite")); |
| | | } |
| | | } else { |
| | | return WritabilityPolicy.READ_WRITE; |
| | | } |
| | | } |
| | | |
| | | /** Indicates whether LDAP client connections should use SSL or StartTLS. */ |
| | | private enum ConnectionSecurity { NONE, SSL, STARTTLS } |
| | | |
| | | /** Specifies the mechanism which will be used for trusting certificates presented by the LDAP server. */ |
| | | private enum TrustManagerType { TRUSTALL, JVM, FILE } |
| | | |
| | | /** Specifies the type of key-store to use when performing SSL client authentication. */ |
| | | private enum KeyManagerType { JVM, KEYSTORE, PKCS11 } |
| | | |
| | | /** |
| | | * Configures a {@link X509KeyManager} using the provided JSON configuration. |
| | | * |
| | | * @param configuration |
| | | * The JSON object containing the key manager configuration. |
| | | * @return The configured key manager. |
| | | */ |
| | | public static X509KeyManager configureKeyManager(final JsonValue configuration) { |
| | | try { |
| | | return configureKeyManager(configuration, KeyManagerType.JVM); |
| | | } catch (GeneralSecurityException | IOException e) { |
| | | throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER.get( |
| | | configuration.getPointer(), e.getLocalizedMessage()).toString(), e); |
| | | } |
| | | } |
| | | |
| | | private static X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing) |
| | | throws GeneralSecurityException, IOException { |
| | | final KeyManagerType keyManagerType = config.get("keyManager") |
| | | .defaultTo(defaultIfMissing) |
| | | .as(enumConstant(KeyManagerType.class)); |
| | | switch (keyManagerType) { |
| | | case JVM: |
| | | return useJvmDefaultKeyStore(); |
| | | case KEYSTORE: |
| | | final String fileName = config.get("keyStoreFile").required().asString(); |
| | | final String passwordFile = config.get("keyStorePasswordFile").asString(); |
| | | final String password = passwordFile != null |
| | | ? readPasswordFromFile(passwordFile) |
| | | : config.get("keyStorePassword").asString(); |
| | | final String format = config.get("keyStoreFormat").asString(); |
| | | final String provider = config.get("keyStoreProvider").asString(); |
| | | return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, format, provider); |
| | | case PKCS11: |
| | | final String pkcs11PasswordFile = config.get("pkcs11PasswordFile").asString(); |
| | | return usePKCS11Token(pkcs11PasswordFile != null |
| | | ? readPasswordFromFile(pkcs11PasswordFile).toCharArray() |
| | | : null); |
| | | default: |
| | | throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType); |
| | | } |
| | | } |
| | | |
| | | private static String readPasswordFromFile(String fileName) throws IOException { |
| | | try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) { |
| | | return reader.readLine(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Configures a {@link TrustManager} using the provided JSON configuration. |
| | | * |
| | | * @param configuration |
| | | * The JSON object containing the trust manager configuration. |
| | | * @return The configured trust manager. |
| | | */ |
| | | public static TrustManager configureTrustManager(final JsonValue configuration) { |
| | | try { |
| | | return configureTrustManager(configuration, TrustManagerType.JVM); |
| | | } catch (GeneralSecurityException | IOException e) { |
| | | throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER.get( |
| | | configuration.getPointer(), e.getLocalizedMessage()).toString(), e); |
| | | } |
| | | } |
| | | |
| | | private static TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing) |
| | | throws GeneralSecurityException, IOException { |
| | | final TrustManagerType trustManagerType = config.get("trustManager") |
| | | .defaultTo(defaultIfMissing) |
| | | .as(enumConstant(TrustManagerType.class)); |
| | | switch (trustManagerType) { |
| | | case TRUSTALL: |
| | | return trustAll(); |
| | | case JVM: |
| | | return null; |
| | | case FILE: |
| | | final String fileName = config.get("fileBasedTrustManagerFile").required().asString(); |
| | | final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString(); |
| | | final String password = passwordFile != null |
| | | ? readPasswordFromFile(passwordFile) : config.get("fileBasedTrustManagerPassword").asString(); |
| | | final String type = config.get("fileBasedTrustManagerType").asString(); |
| | | return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type); |
| | | default: |
| | | throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Creates a new connection factory using the named configuration in the provided JSON list of factory |
| | | * configurations. See the sample configuration file for a detailed description of its content. |
| | | * |
| | | * @param configuration |
| | | * The JSON configuration. |
| | | * @param name |
| | | * The name of the connection factory configuration to be parsed. |
| | | * @param trustManager |
| | | * The trust manager to use for secure connection. Can be {@code null} |
| | | * to use the default JVM trust manager. |
| | | * @param keyManager |
| | | * The key manager to use for secure connection. Can be {@code null} |
| | | * to use the default JVM key manager. |
| | | * @param providerClassLoader |
| | | * The {@link ClassLoader} used to fetch the {@link org.forgerock.opendj.ldap.spi.TransportProvider}. This |
| | | * can be useful in OSGI environments. |
| | | * @return A new connection factory using the provided JSON configuration. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, |
| | | final String name, |
| | | final TrustManager trustManager, |
| | | final X509KeyManager keyManager, |
| | | final ClassLoader providerClassLoader) { |
| | | final JsonValue normalizedConfiguration = normalizeConnectionFactory(configuration, name, 0); |
| | | return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader); |
| | | } |
| | | |
| | | /** |
| | | * Creates a new connection factory using the named configuration in the provided JSON list of factory |
| | | * configurations. See the sample configuration file for a detailed description of its content. |
| | | * |
| | | * @param configuration |
| | | * The JSON configuration. |
| | | * @param name |
| | | * The name of the connection factory configuration to be parsed. |
| | | * @param trustManager |
| | | * The trust manager to use for secure connection. Can be {@code null} |
| | | * to use the default JVM trust manager. |
| | | * @param keyManager |
| | | * The key manager to use for secure connection. Can be {@code null} |
| | | * to use the default JVM key manager. |
| | | * @return A new connection factory using the provided JSON configuration. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, |
| | | final String name, |
| | | final TrustManager trustManager, |
| | | final X509KeyManager keyManager) { |
| | | return configureConnectionFactory(configuration, name, trustManager, keyManager, null); |
| | | } |
| | | |
| | | private static ConnectionFactory configureConnectionFactory(final JsonValue configuration, |
| | | final TrustManager trustManager, |
| | | final X509KeyManager keyManager, |
| | | final ClassLoader providerClassLoader) { |
| | | final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong(); |
| | | final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS); |
| | | |
| | | final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong(); |
| | | final Duration heartBeatTimeout = duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS); |
| | | |
| | | final Options options = Options.defaultOptions() |
| | | .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader) |
| | | .set(HEARTBEAT_ENABLED, true) |
| | | .set(HEARTBEAT_INTERVAL, heartBeatInterval) |
| | | .set(HEARTBEAT_TIMEOUT, heartBeatTimeout) |
| | | .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval); |
| | | |
| | | // Parse pool parameters, |
| | | final int connectionPoolSize = |
| | | Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1); |
| | | |
| | | // Parse authentication parameters. |
| | | if (configuration.isDefined("authentication")) { |
| | | final JsonValue authn = configuration.get("authentication"); |
| | | if (authn.isDefined("simple")) { |
| | | final JsonValue simple = authn.get("simple"); |
| | | final BindRequest bindRequest = |
| | | Requests.newSimpleBindRequest(simple.get("bindDN").required().asString(), |
| | | simple.get("bindPassword").required().asString().toCharArray()); |
| | | options.set(AUTHN_BIND_REQUEST, bindRequest); |
| | | } else { |
| | | throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_AUTHENTICATION.get()); |
| | | } |
| | | } |
| | | |
| | | // Parse SSL/StartTLS parameters. |
| | | final ConnectionSecurity connectionSecurity = configuration.get("connectionSecurity") |
| | | .defaultTo(ConnectionSecurity.NONE) |
| | | .as(enumConstant(ConnectionSecurity.class)); |
| | | if (connectionSecurity != ConnectionSecurity.NONE) { |
| | | try { |
| | | // Configure SSL. |
| | | final SSLContextBuilder builder = new SSLContextBuilder(); |
| | | builder.setTrustManager(trustManager); |
| | | final String sslCertAlias = configuration.get("sslCertAlias").asString(); |
| | | builder.setKeyManager(sslCertAlias != null |
| | | ? useSingleCertificate(sslCertAlias, keyManager) |
| | | : keyManager); |
| | | options.set(SSL_CONTEXT, builder.getSSLContext()); |
| | | options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS); |
| | | } catch (GeneralSecurityException e) { |
| | | // Rethrow as unchecked exception. |
| | | throw new IllegalArgumentException(e); |
| | | } |
| | | } |
| | | |
| | | // Parse primary data center. |
| | | final JsonValue primaryLdapServers = configuration.get("primaryLDAPServers"); |
| | | if (!primaryLdapServers.isList() || primaryLdapServers.size() == 0) { |
| | | throw new IllegalArgumentException("No primaryLDAPServers"); |
| | | } |
| | | final ConnectionFactory primary = parseLdapServers(primaryLdapServers, connectionPoolSize, options); |
| | | |
| | | // Parse secondary data center(s). |
| | | final JsonValue secondaryLdapServers = configuration.get("secondaryLDAPServers"); |
| | | ConnectionFactory secondary = null; |
| | | if (secondaryLdapServers.isList()) { |
| | | if (secondaryLdapServers.size() > 0) { |
| | | secondary = parseLdapServers(secondaryLdapServers, connectionPoolSize, options); |
| | | } |
| | | } else if (!secondaryLdapServers.isNull()) { |
| | | throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_SECONDARY_LDAP_SERVER.get()); |
| | | } |
| | | |
| | | // Create fail-over. |
| | | if (secondary != null) { |
| | | return newFailoverLoadBalancer(asList(primary, secondary), options); |
| | | } else { |
| | | return primary; |
| | | } |
| | | } |
| | | |
| | | private static JsonValue normalizeConnectionFactory(final JsonValue configuration, |
| | | final String name, final int depth) { |
| | | // Protect against infinite recursion in the configuration. |
| | | if (depth > 100) { |
| | | throw new LocalizedIllegalArgumentException(ERR_CONFIG_SERVER_CIRCULAR_DEPENDENCIES.get(name)); |
| | | } |
| | | |
| | | final JsonValue current = configuration.get(name).required(); |
| | | if (current.isDefined("inheritFrom")) { |
| | | // Inherit missing fields from inherited configuration. |
| | | final JsonValue parent = |
| | | normalizeConnectionFactory(configuration, |
| | | current.get("inheritFrom").asString(), depth + 1); |
| | | final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap()); |
| | | normalized.putAll(current.asMap()); |
| | | normalized.remove("inheritFrom"); |
| | | return new JsonValue(normalized); |
| | | } else { |
| | | // No normalization required. |
| | | return current; |
| | | } |
| | | } |
| | | |
| | | private static ConnectionFactory parseLdapServers(JsonValue config, int poolSize, Options options) { |
| | | final List<ConnectionFactory> servers = new ArrayList<>(config.size()); |
| | | for (final JsonValue server : config) { |
| | | final String host = server.get("hostname").required().asString(); |
| | | final int port = server.get("port").required().asInteger(); |
| | | final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options); |
| | | if (poolSize > 1) { |
| | | servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS)); |
| | | } else { |
| | | servers.add(factory); |
| | | } |
| | | } |
| | | if (servers.size() > 1) { |
| | | return newRoundRobinLoadBalancer(servers, options); |
| | | } else { |
| | | return servers.get(0); |
| | | } |
| | | } |
| | | |
| | | private Rest2LdapJsonConfigurator() { |
| | | // Prevent instantiation. |
| | | } |
| | | } |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.services.context.AbstractContext; |
| | | import org.forgerock.services.context.Context; |
| | | |
| | | /** |
| | | * A {@link Context} which communicates the current Rest2Ldap routing state to downstream handlers. |
| | | */ |
| | | final class RoutingContext extends AbstractContext { |
| | | private final DN dn; |
| | | private final Resource resource; |
| | | |
| | | RoutingContext(final Context parent, final DN dn, final Resource resource) { |
| | | super(parent, "routing context"); |
| | | this.dn = dn; |
| | | this.resource = resource; |
| | | } |
| | | |
| | | DN getDn() { |
| | | return dn; |
| | | } |
| | | |
| | | Resource getType() { |
| | | return resource; |
| | | } |
| | | } |
| | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | |
| | | 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.Rest2Ldap.asResourceException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.*; |
| | | import static org.forgerock.util.promise.Promises.newExceptionPromise; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | /** |
| | | * An property mapper which provides a simple mapping from a JSON value to a |
| | | * single LDAP attribute. |
| | | */ |
| | | /** An property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. */ |
| | | public final class SimplePropertyMapper extends AbstractLdapPropertyMapper<SimplePropertyMapper> { |
| | | private Function<ByteString, ?, NeverThrowsException> decoder; |
| | | private Function<Object, ByteString, NeverThrowsException> encoder; |
| | |
| | | } |
| | | |
| | | /** |
| | | * Sets the default JSON value which should be substituted when the LDAP |
| | | * attribute is not found in the LDAP entry. |
| | | * Sets the default JSON value which should be substituted when the LDAP attribute is not found in the LDAP entry. |
| | | * |
| | | * @param defaultValue |
| | | * The default JSON value. |
| | |
| | | } |
| | | |
| | | /** |
| | | * Sets the default JSON values which should be substituted when the LDAP attribute is not found in the LDAP entry. |
| | | * |
| | | * @param defaultValues |
| | | * The default JSON values. |
| | | * @return This property mapper. |
| | | */ |
| | | public SimplePropertyMapper defaultJsonValues(final Collection<?> defaultValues) { |
| | | this.defaultJsonValues = defaultValues != null ? new ArrayList<>(defaultValues) : emptyList(); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the encoder which will be used for converting JSON values to LDAP |
| | | * attribute values. |
| | | * |
| | |
| | | |
| | | /** |
| | | * Indicates that JSON values are base 64 encodings of binary data. Calling |
| | | * this method is equivalent to the following: |
| | | * this method with the value {@code true} is equivalent to the following: |
| | | * |
| | | * <pre> |
| | | * mapper.decoder(...); // function that converts binary data to base 64 |
| | | * mapper.encoder(...); // function that converts base 64 to binary data |
| | | * </pre> |
| | | * |
| | | * Passing in a value of {@code false} resets the encoding and decoding |
| | | * functions to the default. |
| | | * |
| | | * @param isBinary {@code true} if this property is binary. |
| | | * @return This property mapper. |
| | | */ |
| | | public SimplePropertyMapper isBinary() { |
| | | decoder = byteStringToBase64(); |
| | | encoder = base64ToByteString(); |
| | | public SimplePropertyMapper isBinary(final boolean isBinary) { |
| | | if (isBinary) { |
| | | decoder = byteStringToBase64(); |
| | | encoder = base64ToByteString(); |
| | | } else { |
| | | decoder = null; |
| | | encoder = null; |
| | | } |
| | | return this; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, |
| | | final String operator, final Object valueAssertion) { |
| | | Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource, |
| | | 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; |
| | | final ByteString va = valueAssertion != null ? encoder().apply(valueAssertion) : null; |
| | | return newResultPromise(toFilter(type, ldapAttributeName.toString(), va)); |
| | | } catch (final Exception e) { |
| | | // Invalid assertion value - bad request. |
| | | return newExceptionPromise((ResourceException) newBadRequestException( |
| | | ERR_ILLEGAL_FILTER_ASSERTION_VALUE.get(String.valueOf(valueAssertion), path), e)); |
| | | return newBadRequestException( |
| | | ERR_ILLEGAL_FILTER_ASSERTION_VALUE.get(String.valueOf(valueAssertion), path), e).asPromise(); |
| | | } |
| | | } else { |
| | | // This property mapper does not support partial filtering. |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<Attribute, ResourceException> getNewLdapAttributes( |
| | | final Connection connection, final JsonPointer path, final List<Object> newValues) { |
| | | Promise<Attribute, ResourceException> getNewLdapAttributes(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final List<Object> newValues) { |
| | | try { |
| | | return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder())); |
| | | } catch (final Exception ex) { |
| | | return newExceptionPromise((ResourceException) newBadRequestException( |
| | | ERR_ENCODING_VALUES_FOR_FIELD.get(path, ex.getMessage()))); |
| | | return newBadRequestException(ERR_ENCODING_VALUES_FOR_FIELD.get(path, ex.getMessage())).asPromise(); |
| | | } |
| | | } |
| | | |
| | |
| | | return this; |
| | | } |
| | | |
| | | @SuppressWarnings("fallthrough") |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) { |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource, |
| | | final JsonPointer path, final Entry e) { |
| | | try { |
| | | final Object value; |
| | | if (attributeIsSingleValued()) { |
| | | value = e.parseAttribute(ldapAttributeName) |
| | | .as(decoder(), defaultJsonValues.isEmpty() ? null : defaultJsonValues.get(0)); |
| | | } else { |
| | | final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJsonValues); |
| | | value = s.isEmpty() ? null : new ArrayList<>(s); |
| | | final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJsonValues); |
| | | switch (s.size()) { |
| | | case 0: |
| | | return newResultPromise(null); |
| | | case 1: |
| | | if (attributeIsSingleValued()) { |
| | | return newResultPromise(new JsonValue(s.iterator().next())); |
| | | } |
| | | // Fall-though: unexpectedly got multiple values. It's probably best to just return them. |
| | | default: |
| | | return newResultPromise(new JsonValue(new ArrayList<>(s))); |
| | | } |
| | | return newResultPromise(value != null ? new JsonValue(value) : null); |
| | | } catch (final Exception ex) { |
| | | // The LDAP attribute could not be decoded. |
| | | return newExceptionPromise(asResourceException(ex)); |
| | | return asResourceException(ex).asPromise(); |
| | | } |
| | | } |
| | | |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.DECODE_OPTIONS; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | import org.forgerock.http.routing.UriRouterContext; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.NotFoundException; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.util.Function; |
| | | |
| | | /** |
| | | * Defines a parent-child relationship between a parent resource and one or more child resource(s). Removal of the |
| | | * parent resource implies that the children (the sub-resources) are also removed. There are two types of |
| | | * sub-resource: |
| | | * <ul> |
| | | * <li>{@link SubResourceSingleton} represents a one-to-one relationship supporting read, update, patch, and action |
| | | * requests</li> |
| | | * <li>{@link SubResourceCollection} represents a one-to-many relationship supporting all requests.</li> |
| | | * </ul> |
| | | */ |
| | | public abstract class SubResource { |
| | | private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}"); |
| | | |
| | | private final String resourceId; |
| | | private final List<String> dnTemplateVariables = new ArrayList<>(); |
| | | private String dnTemplateFormatString; |
| | | |
| | | String urlTemplate = ""; |
| | | String dnTemplate = ""; |
| | | boolean isReadOnly = false; |
| | | Rest2Ldap rest2Ldap; |
| | | Resource resource; |
| | | |
| | | SubResource(final String resourceId) { |
| | | this.resourceId = resourceId; |
| | | } |
| | | |
| | | @Override |
| | | public final boolean equals(final Object o) { |
| | | return this == o || (o instanceof SubResource && urlTemplate.equals(((SubResource) o).urlTemplate)); |
| | | } |
| | | |
| | | @Override |
| | | public final int hashCode() { |
| | | return urlTemplate.hashCode(); |
| | | } |
| | | |
| | | @Override |
| | | public final String toString() { |
| | | return urlTemplate; |
| | | } |
| | | |
| | | final Resource getResource() { |
| | | return resource; |
| | | } |
| | | |
| | | final void build(final Rest2Ldap rest2Ldap, final String parent) { |
| | | this.rest2Ldap = rest2Ldap; |
| | | this.resource = rest2Ldap.getResource(resourceId); |
| | | if (resource == null) { |
| | | throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE.get(parent, resourceId)); |
| | | } |
| | | this.dnTemplateFormatString = formatTemplate(dnTemplate, dnTemplateVariables); |
| | | } |
| | | |
| | | // Parse the template keys and replace them with %s for formatting. |
| | | private String formatTemplate(final String template, final List<String> templateVariables) { |
| | | final Matcher matcher = TEMPLATE_KEY_RE.matcher(template); |
| | | final StringBuffer buffer = new StringBuffer(template.length()); |
| | | while (matcher.find()) { |
| | | matcher.appendReplacement(buffer, "%s"); |
| | | templateVariables.add(matcher.group(1)); |
| | | } |
| | | matcher.appendTail(buffer); |
| | | return buffer.toString(); |
| | | } |
| | | |
| | | abstract Router addRoutes(Router router); |
| | | |
| | | /** A 404 indicates that this instance is not also a collection, so return a more helpful message. */ |
| | | static <T> Function<ResourceException, T, ResourceException> convert404To400(final LocalizableMessage msg) { |
| | | return new Function<ResourceException, T, ResourceException>() { |
| | | @Override |
| | | public T apply(final ResourceException e) throws ResourceException { |
| | | if (e instanceof NotFoundException) { |
| | | throw new BadRequestException(msg.toString()); |
| | | } |
| | | throw e; |
| | | } |
| | | }; |
| | | } |
| | | |
| | | final RequestHandler readOnly(final RequestHandler handler) { |
| | | return isReadOnly ? new ReadOnlyRequestHandler(handler) : handler; |
| | | } |
| | | |
| | | final DN dnFrom(final Context context) { |
| | | final DN baseDn = context.containsContext(RoutingContext.class) |
| | | ? context.asContext(RoutingContext.class).getDn() : DN.rootDN(); |
| | | |
| | | final Schema schema = rest2Ldap.getOptions().get(DECODE_OPTIONS).getSchemaResolver().resolveSchema(dnTemplate); |
| | | if (dnTemplateVariables.isEmpty()) { |
| | | final DN relativeDn = DN.valueOf(dnTemplate, schema); |
| | | return baseDn.child(relativeDn); |
| | | } else { |
| | | final UriRouterContext uriRouterContext = context.asContext(UriRouterContext.class); |
| | | final Map<String, String> uriTemplateVariables = uriRouterContext.getUriTemplateVariables(); |
| | | final String[] values = new String[dnTemplateVariables.size()]; |
| | | for (int i = 0; i < values.length; i++) { |
| | | final String key = dnTemplateVariables.get(i); |
| | | values[i] = uriTemplateVariables.get(key); |
| | | } |
| | | final DN relativeDn = DN.format(dnTemplateFormatString, schema, (Object[]) values); |
| | | return baseDn.child(relativeDn); |
| | | } |
| | | } |
| | | |
| | | final RequestHandler subResourceRouterFrom(final RoutingContext context) { |
| | | return context.getType().getSubResourceRouter(); |
| | | } |
| | | } |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.http.routing.RoutingMode.EQUALS; |
| | | import static org.forgerock.http.routing.RoutingMode.STARTS_WITH; |
| | | import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher; |
| | | import static org.forgerock.opendj.ldap.Filter.objectClassPresent; |
| | | import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT; |
| | | import static org.forgerock.opendj.ldap.SearchScope.SINGLE_LEVEL; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | import org.forgerock.http.routing.UriRouterContext; |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.json.resource.ActionRequest; |
| | | import org.forgerock.json.resource.ActionResponse; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.CreateRequest; |
| | | import org.forgerock.json.resource.DeleteRequest; |
| | | import org.forgerock.json.resource.NotSupportedException; |
| | | import org.forgerock.json.resource.PatchRequest; |
| | | import org.forgerock.json.resource.QueryRequest; |
| | | import org.forgerock.json.resource.QueryResourceHandler; |
| | | import org.forgerock.json.resource.QueryResponse; |
| | | import org.forgerock.json.resource.ReadRequest; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.json.resource.Router; |
| | | 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.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.LinkedAttribute; |
| | | import org.forgerock.opendj.ldap.RDN; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * Defines a one-to-many relationship between a parent resource and its children. Removal of the parent resource |
| | | * implies that the children (the sub-resources) are also removed. Collections support all request types. |
| | | */ |
| | | public final class SubResourceCollection extends SubResource { |
| | | /** The LDAP object classes associated with the glue entries forming the DN template. */ |
| | | private final Attribute glueObjectClasses = new LinkedAttribute("objectClass"); |
| | | |
| | | private NamingStrategy namingStrategy; |
| | | |
| | | SubResourceCollection(final String resourceId) { |
| | | super(resourceId); |
| | | useClientDnNaming("uid"); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must be provided by the user, and will be used for naming the associated LDAP |
| | | * entry. More specifically, LDAP entry names will be derived by appending a single RDN to the collection's base DN |
| | | * composed of the specified attribute type and LDAP value taken from the LDAP entry once attribute mapping has been |
| | | * performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the user provides the resource name when creating new resources, which |
| | | * means it must be included in the resource content when not specified explicitly in the create request. |
| | | * |
| | | * @param dnAttribute |
| | | * The LDAP attribute which will be used for naming. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection useClientDnNaming(final String dnAttribute) { |
| | | this.namingStrategy = new DnNamingStrategy(dnAttribute); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must be provided by the user, but will not be used for naming the |
| | | * associated LDAP entry. Instead the JSON resource ID will be taken from the {@code idAttribute} in the LDAP |
| | | * entry, and the LDAP entry name will be derived by appending a single RDN to the collection's base DN composed |
| | | * of the {@code dnAttribute} taken from the LDAP entry once attribute mapping has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the user provides the resource name when creating new resources, which |
| | | * means it must be included in the resource content when not specified explicitly in the create request. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @param idAttribute |
| | | * The attribute which will be used for JSON resource IDs. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection useClientNaming(final String dnAttribute, final String idAttribute) { |
| | | this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, false); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID will be derived from the server provided "entryUUID" LDAP attribute. The |
| | | * LDAP entry name will be derived by appending a single RDN to the collection's base DN composed of the {@code |
| | | * dnAttribute} taken from the LDAP entry once attribute mapping has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the server provides the resource name when creating new resources, |
| | | * which means it must not be specified in the create request, nor included in the resource content. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection useServerEntryUuidNaming(final String dnAttribute) { |
| | | return useServerNaming(dnAttribute, "entryUUID"); |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the JSON resource ID must not be provided by the user, and will not be used for naming the |
| | | * associated LDAP entry. Instead the JSON resource ID will be taken from the {@code idAttribute} in the LDAP |
| | | * entry, and the LDAP entry name will be derived by appending a single RDN to the collection's base DN composed |
| | | * of the {@code dnAttribute} taken from the LDAP entry once attribute mapping has been performed. |
| | | * <p> |
| | | * Note that this naming policy requires that the server provides the resource name when creating new resources, |
| | | * which means it must not be specified in the create request, nor included in the resource content. |
| | | * |
| | | * @param dnAttribute |
| | | * The attribute which will be used for naming LDAP entries. |
| | | * @param idAttribute |
| | | * The attribute which will be used for JSON resource IDs. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection useServerNaming(final String dnAttribute, final String idAttribute) { |
| | | this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, true); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the relative URL template beneath which the sub-resources will be located. The template may be empty |
| | | * indicating that the sub-resources will be located directly beneath the parent resource. Any URL template |
| | | * variables will be substituted into the {@link #dnTemplate(String) DN template}. |
| | | * |
| | | * @param urlTemplate |
| | | * The relative URL template. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection urlTemplate(final String urlTemplate) { |
| | | this.urlTemplate = urlTemplate; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the relative DN template beneath which the sub-resource LDAP entries will be located. The template may be |
| | | * empty indicating that the LDAP entries will be located directly beneath the parent LDAP entry. Any DN template |
| | | * variables will be substituted using values extracted from the {@link #urlTemplate(String) URL template}. |
| | | * |
| | | * @param dnTemplate |
| | | * The relative DN template. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection dnTemplate(final String dnTemplate) { |
| | | this.dnTemplate = dnTemplate; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies an LDAP object class which is to be associated with any intermediate "glue" entries forming the DN |
| | | * template. Multiple object classes may be specified. |
| | | * |
| | | * @param objectClass |
| | | * An LDAP object class which is to be associated with any intermediate "glue" entries forming the DN |
| | | * template. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection glueObjectClass(final String objectClass) { |
| | | this.glueObjectClasses.add(objectClass); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Specifies one or more LDAP object classes which is to be associated with any intermediate "glue" entries |
| | | * forming the DN template. Multiple object classes may be specified. |
| | | * |
| | | * @param objectClasses |
| | | * The LDAP object classes which is to be associated with any intermediate "glue" entries forming the DN |
| | | * template. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection glueObjectClasses(final String... objectClasses) { |
| | | this.glueObjectClasses.add((Object[]) objectClasses); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether this sub-resource collection only supports read and query operations. |
| | | * |
| | | * @param readOnly |
| | | * {@code true} if this sub-resource collection is read-only. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceCollection isReadOnly(final boolean readOnly) { |
| | | isReadOnly = readOnly; |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | Router addRoutes(final Router router) { |
| | | router.addRoute(requestUriMatcher(EQUALS, urlTemplate), readOnly(new CollectionHandler())); |
| | | router.addRoute(requestUriMatcher(EQUALS, urlTemplate + "/{id}"), readOnly(new InstanceHandler())); |
| | | router.addRoute(requestUriMatcher(STARTS_WITH, urlTemplate + "/{id}"), readOnly(new SubResourceHandler())); |
| | | return router; |
| | | } |
| | | |
| | | private Promise<RoutingContext, ResourceException> route(final Context context) { |
| | | final Connection conn = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | final SearchRequest searchRequest = namingStrategy.createSearchRequest(dnFrom(context), idFrom(context)); |
| | | if (searchRequest.getScope().equals(BASE_OBJECT) && !resource.hasSubTypesWithSubResources()) { |
| | | // There's no point in doing a search because we already know the DN and sub-resources. |
| | | return newResultPromise(new RoutingContext(context, searchRequest.getName(), resource)); |
| | | } |
| | | searchRequest.addAttribute("objectClass"); |
| | | return conn.searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, RoutingContext, ResourceException>() { |
| | | @Override |
| | | public Promise<RoutingContext, ResourceException> apply(SearchResultEntry entry) |
| | | throws ResourceException { |
| | | final Resource subType = resource.resolveSubTypeFromObjectClasses(entry); |
| | | return newResultPromise(new RoutingContext(context, entry.getName(), subType)); |
| | | } |
| | | }, new AsyncFunction<LdapException, RoutingContext, ResourceException>() { |
| | | @Override |
| | | public Promise<RoutingContext, ResourceException> apply(LdapException e) |
| | | throws ResourceException { |
| | | return asResourceException(e).asPromise(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private SubResourceImpl collection(final Context context) { |
| | | return new SubResourceImpl(rest2Ldap, |
| | | dnFrom(context), |
| | | dnTemplate.isEmpty() ? null : glueObjectClasses, |
| | | namingStrategy, |
| | | resource); |
| | | } |
| | | |
| | | private String idFrom(final Context context) { |
| | | return context.asContext(UriRouterContext.class).getUriTemplateVariables().get("id"); |
| | | } |
| | | |
| | | private static final class AttributeNamingStrategy implements NamingStrategy { |
| | | private final AttributeDescription dnAttribute; |
| | | private final AttributeDescription idAttribute; |
| | | private final boolean isServerProvided; |
| | | |
| | | private AttributeNamingStrategy(final String dnAttribute, final String idAttribute, |
| | | final boolean isServerProvided) { |
| | | this.dnAttribute = AttributeDescription.valueOf(dnAttribute); |
| | | this.idAttribute = AttributeDescription.valueOf(idAttribute); |
| | | if (this.dnAttribute.equals(this.idAttribute)) { |
| | | throw new LocalizedIllegalArgumentException(ERR_CONFIG_NAMING_STRATEGY_DN_AND_ID_NOT_DIFFERENT.get()); |
| | | } |
| | | this.isServerProvided = isServerProvided; |
| | | } |
| | | |
| | | @Override |
| | | public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) { |
| | | return newSearchRequest(baseDn, SINGLE_LEVEL, Filter.equality(idAttribute.toString(), resourceId)); |
| | | } |
| | | |
| | | @Override |
| | | public String getResourceIdLdapAttribute() { |
| | | return idAttribute.toString(); |
| | | } |
| | | |
| | | @Override |
| | | public String decodeResourceId(final Entry entry) { |
| | | return entry.parseAttribute(idAttribute).asString(); |
| | | } |
| | | |
| | | @Override |
| | | public void encodeResourceId(final DN baseDn, final String resourceId, final Entry entry) |
| | | throws ResourceException { |
| | | if (isServerProvided) { |
| | | if (resourceId != null) { |
| | | throw newBadRequestException(ERR_SERVER_PROVIDED_RESOURCE_ID_UNEXPECTED.get()); |
| | | } |
| | | } else { |
| | | entry.addAttribute(new LinkedAttribute(idAttribute, ByteString.valueOfUtf8(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 DnNamingStrategy implements NamingStrategy { |
| | | private final AttributeDescription attribute; |
| | | |
| | | private DnNamingStrategy(final String attribute) { |
| | | this.attribute = AttributeDescription.valueOf(attribute); |
| | | } |
| | | |
| | | @Override |
| | | public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) { |
| | | return newSearchRequest(baseDn.child(rdn(resourceId)), BASE_OBJECT, objectClassPresent()); |
| | | } |
| | | |
| | | @Override |
| | | public String getResourceIdLdapAttribute() { |
| | | return attribute.toString(); |
| | | } |
| | | |
| | | @Override |
| | | public String decodeResourceId(final Entry entry) { |
| | | return entry.parseAttribute(attribute).asString(); |
| | | } |
| | | |
| | | @Override |
| | | public void encodeResourceId(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.valueOfUtf8(resourceId))); |
| | | } else if (entry.getAttribute(attribute) != null) { |
| | | entry.setName(baseDn.child(rdn(entry.parseAttribute(attribute).asString()))); |
| | | } else { |
| | | throw newBadRequestException(ERR_CLIENT_PROVIDED_RESOURCE_ID_MISSING.get()); |
| | | } |
| | | } |
| | | |
| | | private RDN rdn(final String resourceId) { |
| | | return new RDN(attribute.getAttributeType(), resourceId); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Responsible for routing collection requests (CQ) to this collection. More specifically, given the |
| | | * URL template /collection/{id} then this handler processes requests against /collection. |
| | | */ |
| | | private final class CollectionHandler extends AbstractRequestHandler { |
| | | private CollectionHandler() { |
| | | super(new BadRequestException(ERR_UNSUPPORTED_REQUEST_AGAINST_COLLECTION.get().toString())); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> handleAction(final Context context, |
| | | final ActionRequest request) { |
| | | return new NotSupportedException(ERR_COLLECTION_ACTIONS_NOT_SUPPORTED.get().toString()).asPromise(); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, |
| | | final CreateRequest request) { |
| | | return collection(context).create(context, request); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, |
| | | final QueryResourceHandler handler) { |
| | | return collection(context).query(context, request, handler); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Responsible for processing instance requests (RUDPA) against this collection and collection requests (CQ) to |
| | | * any collections sharing the same base URL as an instance within this collection. More specifically, given the |
| | | * URL template /collection/{parent}/{child} then this handler processes requests against {parent} since it is |
| | | * both an instance within /collection and also a collection of {child}. |
| | | */ |
| | | private final class InstanceHandler implements RequestHandler { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> handleAction(final Context context, |
| | | final ActionRequest request) { |
| | | return collection(context).action(context, idFrom(context), request); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, |
| | | final CreateRequest request) { |
| | | return route(context) |
| | | .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleCreate(context, request); |
| | | } |
| | | }).thenCatch(this.<ResourceResponse>convert404To400()); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleDelete(final Context context, |
| | | final DeleteRequest request) { |
| | | return collection(context).delete(context, idFrom(context), request); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, |
| | | final PatchRequest request) { |
| | | return collection(context).patch(context, idFrom(context), request); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, |
| | | final QueryResourceHandler handler) { |
| | | return route(context) |
| | | .thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleQuery(context, request, handler); |
| | | } |
| | | }).thenCatch(this.<QueryResponse>convert404To400()); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleRead(final Context context, |
| | | final ReadRequest request) { |
| | | return collection(context).read(context, idFrom(context), request); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context, |
| | | final UpdateRequest request) { |
| | | return collection(context).update(context, idFrom(context), request); |
| | | } |
| | | |
| | | private <T> Function<ResourceException, T, ResourceException> convert404To400() { |
| | | return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_INSTANCE.get()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Responsible for routing requests to sub-resources of instances within this collection. More specifically, given |
| | | * the URL template /collection/{id} then this handler processes all requests beneath /collection/{id}. |
| | | */ |
| | | private final class SubResourceHandler implements RequestHandler { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> handleAction(final Context context, |
| | | final ActionRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ActionResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleAction(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, |
| | | final CreateRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleCreate(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleDelete(final Context context, |
| | | final DeleteRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleDelete(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, |
| | | final PatchRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handlePatch(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, |
| | | final QueryResourceHandler handler) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleQuery(context, request, handler); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleRead(final Context context, |
| | | final ReadRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleRead(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context, |
| | | final UpdateRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleUpdate(context, request); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | } |
| | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.i18n.LocalizableMessage.raw; |
| | | import static org.forgerock.json.resource.Responses.newResourceResponse; |
| | | import static org.forgerock.opendj.ldap.ResultCode.Enum.NOT_ALLOWED_ON_NONLEAF; |
| | | import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT; |
| | | import static org.forgerock.opendj.ldap.responses.Responses.newResult; |
| | | import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise; |
| | | import static org.forgerock.opendj.rest2ldap.FilterType.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*; |
| | | import static java.util.Arrays.asList; |
| | | import static org.forgerock.json.resource.ResourceException.FORBIDDEN; |
| | | import static org.forgerock.json.resource.ResourceException.newResourceException; |
| | | import static org.forgerock.json.resource.Responses.newActionResponse; |
| | | import static org.forgerock.json.resource.Responses.newQueryResponse; |
| | | import static org.forgerock.json.resource.Responses.newResourceResponse; |
| | | import static org.forgerock.opendj.ldap.ByteString.valueOfBytes; |
| | | 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.ldap.SearchScope.SINGLE_LEVEL; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.*; |
| | | import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toFilter; |
| | | import static org.forgerock.util.Utils.asEnum; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | import static org.forgerock.util.promise.Promises.when; |
| | | |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.ArrayList; |
| | |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | |
| | | import org.forgerock.json.JsonValueException; |
| | | import org.forgerock.json.resource.ActionRequest; |
| | | import org.forgerock.json.resource.ActionResponse; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.CreateRequest; |
| | | import org.forgerock.json.resource.DeleteRequest; |
| | | import org.forgerock.json.resource.NotSupportedException; |
| | |
| | | import org.forgerock.json.resource.ReadRequest; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | 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.DecodeException; |
| | | import org.forgerock.opendj.ldap.DecodeOptions; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.EntryNotFoundException; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.LdapPromise; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.controls.AssertionRequestControl; |
| | |
| | | import org.forgerock.opendj.ldap.requests.AddRequest; |
| | | import org.forgerock.opendj.ldap.requests.ModifyRequest; |
| | | import org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | |
| | | 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 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 SubResourceImpl implements CollectionResourceProvider { |
| | | |
| | | /** Implements the core CREST operations supported by singleton and collection sub-resources. */ |
| | | final class SubResourceImpl { |
| | | private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | /** Dummy exception used for signalling search success. */ |
| | | private static final ResourceException SUCCESS = new UncategorizedException(0, null, null); |
| | | |
| | | /** Empty decode options required for decoding response controls. */ |
| | | private static final DecodeOptions DECODE_OPTIONS = new DecodeOptions(); |
| | | private static final JsonPointer ROOT = new JsonPointer(); |
| | | |
| | | private final List<Attribute> additionalLDAPAttributes; |
| | | private final PropertyMapper propertyMapper; |
| | | private final DN baseDn; // TODO: support template variables. |
| | | private final Config config; |
| | | private final DN baseDn; |
| | | private final AttributeDescription etagAttribute; |
| | | private final NamingStrategy namingStrategy; |
| | | private final DecodeOptions decodeOptions; |
| | | private final ReadOnUpdatePolicy readOnUpdatePolicy; |
| | | private final boolean useSubtreeDelete; |
| | | private final boolean usePermissiveModify; |
| | | private final Resource resource; |
| | | private final Attribute glueObjectClasses; |
| | | |
| | | SubResourceImpl(final DN baseDn, final PropertyMapper mapper, |
| | | final NamingStrategy namingStrategy, final AttributeDescription etagAttribute, |
| | | final Config config, final List<Attribute> additionalLDAPAttributes) { |
| | | SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses, |
| | | final NamingStrategy namingStrategy, final Resource resource) { |
| | | this.readOnUpdatePolicy = rest2Ldap.getOptions().get(READ_ON_UPDATE_POLICY); |
| | | this.useSubtreeDelete = rest2Ldap.getOptions().get(USE_SUBTREE_DELETE); |
| | | this.usePermissiveModify = rest2Ldap.getOptions().get(USE_PERMISSIVE_MODIFY); |
| | | this.etagAttribute = rest2Ldap.getOptions().get(USE_MVCC) |
| | | ? AttributeDescription.valueOf(rest2Ldap.getOptions().get(MVCC_ATTRIBUTE)) : null; |
| | | this.decodeOptions = rest2Ldap.getOptions().get(DECODE_OPTIONS); |
| | | this.baseDn = baseDn; |
| | | this.propertyMapper = mapper; |
| | | this.config = config; |
| | | this.glueObjectClasses = glueObjectClasses; |
| | | this.namingStrategy = namingStrategy; |
| | | this.etagAttribute = etagAttribute; |
| | | this.additionalLDAPAttributes = additionalLDAPAttributes; |
| | | this.resource = resource; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> actionCollection( |
| | | final Context context, final ActionRequest request) { |
| | | return Promises.<ActionResponse, ResourceException> newExceptionPromise( |
| | | newNotSupportedException(ERR_NOT_YET_IMPLEMENTED.get())); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> actionInstance( |
| | | Promise<ActionResponse, ResourceException> action( |
| | | final Context context, final String resourceId, final ActionRequest request) { |
| | | String actionId = request.getAction(); |
| | | if (actionId.equals("passwordModify")) { |
| | | return passwordModify(context, resourceId, request); |
| | | try { |
| | | final Action action = asEnum(request.getAction(), Action.class); |
| | | if (resource.hasSupportedAction(action)) { |
| | | switch (action) { |
| | | case PASSWORDMODIFY: |
| | | return passwordModify(context, resourceId, request); |
| | | } |
| | | } |
| | | } catch (final IllegalArgumentException ignored) { |
| | | // fall-through |
| | | } |
| | | return Promises.<ActionResponse, ResourceException> newExceptionPromise( |
| | | newNotSupportedException(ERR_ACTION_NOT_SUPPORTED.get(actionId))); |
| | | return newNotSupportedException(ERR_ACTION_NOT_SUPPORTED.get(request.getAction())).asPromise(); |
| | | |
| | | } |
| | | |
| | | private Promise<ActionResponse, ResourceException> passwordModify( |
| | | final Context context, final String resourceId, final ActionRequest request) { |
| | | if (!context.containsContext(ClientContext.class) |
| | | || !context.asContext(ClientContext.class).isSecure()) { |
| | | return Promises.newExceptionPromise(ResourceException.newResourceException( |
| | | ResourceException.FORBIDDEN, ERR_PASSWORD_MODIFY_SECURE_CONNECTION.get().toString())); |
| | | return newResourceException(FORBIDDEN, ERR_PASSWORD_MODIFY_SECURE_CONNECTION.get().toString()).asPromise(); |
| | | } |
| | | if (!context.containsContext(SecurityContext.class) |
| | | || context.asContext(SecurityContext.class).getAuthenticationId() == null) { |
| | | return Promises.newExceptionPromise(ResourceException.newResourceException( |
| | | ResourceException.FORBIDDEN, ERR_PASSWORD_MODIFY_USER_AUTHENTICATED.get().toString())); |
| | | return newResourceException(FORBIDDEN, ERR_PASSWORD_MODIFY_USER_AUTHENTICATED.get().toString()).asPromise(); |
| | | } |
| | | |
| | | final JsonValue jsonContent = request.getContent(); |
| | |
| | | final LocalizableMessage msg = ERR_PASSWORD_MODIFY_REQUEST_IS_INVALID.get(); |
| | | final ResourceException ex = newBadRequestException(msg, e); |
| | | logger.error(msg, e); |
| | | return Promises.newExceptionPromise(ex); |
| | | return ex.asPromise(); |
| | | } |
| | | |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | List<JsonPointer> attrs = Collections.emptyList(); |
| | | return connection.searchSingleEntryAsync(searchRequest(connection, resourceId, attrs)) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, ActionResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> apply( |
| | | final SearchResultEntry entry) { |
| | | PasswordModifyExtendedRequest pwdModifyRequest = |
| | | Requests.newPasswordModifyExtendedRequest(); |
| | | pwdModifyRequest.setUserIdentity("dn: " + entry.getName()); |
| | | pwdModifyRequest.setOldPassword(asBytes(oldPassword)); |
| | | pwdModifyRequest.setNewPassword(asBytes(newPassword)); |
| | | return connection.extendedRequestAsync(pwdModifyRequest) |
| | | .thenAsync(new AsyncFunction<PasswordModifyExtendedResult, |
| | | ActionResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> apply( |
| | | PasswordModifyExtendedResult value) throws ResourceException { |
| | | JsonValue result = new JsonValue(new LinkedHashMap<>()); |
| | | byte[] generatedPwd = value.getGeneratedPassword(); |
| | | if (generatedPwd != null) { |
| | | result = result.put("generatedPassword", |
| | | ByteString.valueOfBytes(generatedPwd).toString()); |
| | | } |
| | | return Responses.newActionResponse(result).asPromise(); |
| | | } |
| | | }, Exceptions.<ActionResponse>toResourceException()); |
| | | } |
| | | }, Exceptions.<ActionResponse>toResourceException()); |
| | | final Connection connection = connectionFrom(context); |
| | | return resolveResourceDnAndType(context, connection, resourceId, null) |
| | | .thenAsync(new AsyncFunction<RoutingContext, PasswordModifyExtendedResult, ResourceException>() { |
| | | @Override |
| | | public Promise<PasswordModifyExtendedResult, ResourceException> apply(RoutingContext dnAndType) { |
| | | final PasswordModifyExtendedRequest pwdModifyRequest = newPasswordModifyExtendedRequest() |
| | | .setUserIdentity("dn: " + dnAndType.getDn()) |
| | | .setOldPassword(asBytes(oldPassword)) |
| | | .setNewPassword(asBytes(newPassword)); |
| | | return connection.extendedRequestAsync(pwdModifyRequest) |
| | | .thenCatchAsync(adaptLdapException(PasswordModifyExtendedResult.class)); |
| | | } |
| | | }).thenAsync(new AsyncFunction<PasswordModifyExtendedResult, ActionResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> apply(PasswordModifyExtendedResult r) { |
| | | final JsonValue result = new JsonValue(new LinkedHashMap<>()); |
| | | final byte[] generatedPwd = r.getGeneratedPassword(); |
| | | if (generatedPwd != null) { |
| | | result.put("generatedPassword", valueOfBytes(generatedPwd).toString()); |
| | | } |
| | | return newActionResponse(result).asPromise(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private byte[] asBytes(final String s) { |
| | | return s != null ? s.getBytes(StandardCharsets.UTF_8) : null; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> createInstance(final Context context, |
| | | final CreateRequest request) { |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | // Calculate entry content. |
| | | return propertyMapper |
| | | .create(connection, new JsonPointer(), request.getContent()) |
| | | .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | 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 : attributes) { |
| | | addRequest.addAttribute(attribute); |
| | | } |
| | | try { |
| | | namingStrategy.setResourceId(connection, getBaseDn(), |
| | | request.getNewResourceId(), |
| | | addRequest); |
| | | } catch (final ResourceException e) { |
| | | logger.error(raw(e.getLocalizedMessage()), e); |
| | | return Promises.newExceptionPromise(e); |
| | | } |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | addRequest.addControl(PostReadRequestControl.newControl( |
| | | false, getLdapAttributes(connection, request.getFields()))); |
| | | } |
| | | return connection.applyChangeAsync(addRequest) |
| | | .thenAsync( |
| | | postUpdateResultAsyncFunction(connection), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> deleteInstance( |
| | | final Context context, final String resourceId, final DeleteRequest request) { |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | return doUpdateFunction(connection, resourceId, request.getRevision()) |
| | | .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(DN dn) throws ResourceException { |
| | | try { |
| | | final ChangeRecord deleteRequest = newDeleteRequest(dn); |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | final String[] attributes = getLdapAttributes(connection, request.getFields()); |
| | | deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (config.useSubtreeDelete()) { |
| | | deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(deleteRequest, request.getRevision()); |
| | | return connection.applyChangeAsync(deleteRequest) |
| | | .thenAsync( |
| | | postUpdateResultAsyncFunction(connection), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> patchInstance( |
| | | final Context context, final String resourceId, final PatchRequest request) { |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | if (request.getPatchOperations().isEmpty()) { |
| | | return emptyPatchInstance(connection, resourceId, request); |
| | | Promise<ResourceResponse, ResourceException> create(final Context context, final CreateRequest request) { |
| | | // First determine the type of resource being created. |
| | | final Resource subType; |
| | | try { |
| | | subType = resource.resolveSubTypeFromJson(request.getContent()); |
| | | } catch (final ResourceException e) { |
| | | return e.asPromise(); |
| | | } |
| | | return doUpdateFunction(connection, resourceId, request.getRevision()) |
| | | .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() { |
| | | |
| | | // Now build the LDAP representation and add it. |
| | | final Connection connection = connectionFrom(context); |
| | | return subType.getPropertyMapper() |
| | | .create(connection, subType, ROOT, request.getContent()) |
| | | .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final List<Attribute> attributes) { |
| | | // Perform add operation. |
| | | final AddRequest addRequest = newAddRequest(DN.rootDN()); |
| | | addRequest.addAttribute(subType.getObjectClassAttribute()); |
| | | for (final Attribute attribute : attributes) { |
| | | addRequest.addAttribute(attribute); |
| | | } |
| | | try { |
| | | namingStrategy.encodeResourceId(baseDn, request.getNewResourceId(), addRequest); |
| | | } catch (final ResourceException e) { |
| | | logger.error(raw(e.getLocalizedMessage()), e); |
| | | return e.asPromise(); |
| | | } |
| | | if (readOnUpdatePolicy == CONTROLS) { |
| | | final Set<String> ldapAttributes = |
| | | getLdapAttributesForKnownType(request.getFields(), subType); |
| | | addRequest.addControl(PostReadRequestControl.newControl(false, ldapAttributes)); |
| | | } |
| | | return connection.addAsync(addRequest) |
| | | .thenCatchAsync(lazilyAddGlueEntry(connection, addRequest)) |
| | | .thenAsync(encodeUpdateResourceResponse(connection, subType), |
| | | adaptLdapException(ResourceResponse.class)); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * A resource and sub-resource may be separated by a "glue" entry in LDAP. This method detects when a glue entry |
| | | * is missing, creates it, and then retries the original add operation. As a concrete example, consider the |
| | | * backend configuration entry "ds-cfg-backend-id=userRoot,cn=backends,cn=config". Since its indexes are located |
| | | * beneath "cn=Indexes,ds-cfg-backend-id=userRoot,cn=backends,cn=config" we need to add "cn=Indexes" before |
| | | * adding an index entry. |
| | | */ |
| | | private AsyncFunction<LdapException, Result, LdapException> lazilyAddGlueEntry(final Connection connection, |
| | | final AddRequest addRequest) { |
| | | return new AsyncFunction<LdapException, Result, LdapException>() { |
| | | @Override |
| | | public Promise<Result, LdapException> apply(final LdapException e) throws LdapException { |
| | | if (glueObjectClasses != null && e instanceof EntryNotFoundException) { |
| | | // The parent glue entry may be missing - lazily create it. |
| | | final AddRequest glueAddRequest = newAddRequest(baseDn); |
| | | glueAddRequest.addAttribute(glueObjectClasses); |
| | | glueAddRequest.addAttribute(baseDn.rdn().getFirstAVA().toAttribute()); |
| | | return connection.addAsync(glueAddRequest) |
| | | .thenAsync(new AsyncFunction<Result, Result, LdapException>() { |
| | | @Override |
| | | public Promise<Result, LdapException> apply(final Result value) { |
| | | return connection.addAsync(addRequest); |
| | | } |
| | | }); |
| | | } |
| | | // Something else happened, so rethrow. |
| | | throw e; |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private Connection connectionFrom(final Context context) { |
| | | return context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | } |
| | | |
| | | Promise<ResourceResponse, ResourceException> delete( |
| | | final Context context, final String resourceId, final DeleteRequest request) { |
| | | final Connection connection = connectionFrom(context); |
| | | return resolveResourceDnAndType(context, connection, resourceId, request.getRevision()) |
| | | .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | 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()) { |
| | | promises.add(propertyMapper.patch(connection, new JsonPointer(), operation)); |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext dnAndType) |
| | | throws ResourceException { |
| | | final ChangeRecord deleteRequest = newDeleteRequest(dnAndType.getDn()); |
| | | if (readOnUpdatePolicy == CONTROLS) { |
| | | final Set<String> attributes = |
| | | getLdapAttributesForKnownType(request.getFields(), dnAndType.getType()); |
| | | deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | |
| | | return Promises.when(promises).thenAsync( |
| | | new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | final List<List<Modification>> result) { |
| | | // The patch operations have been converted successfully. |
| | | try { |
| | | final ModifyRequest modifyRequest = newModifyRequest(dn); |
| | | |
| | | // Add the modifications. |
| | | for (final List<Modification> modifications : result) { |
| | | if (modifications != null) { |
| | | modifyRequest.getModifications().addAll(modifications); |
| | | } |
| | | } |
| | | |
| | | final List<String> attributes = |
| | | asList(getLdapAttributes(connection, request.getFields())); |
| | | if (modifyRequest.getModifications().isEmpty()) { |
| | | // This patch is a no-op so just read the entry and check its version. |
| | | return |
| | | connection |
| | | .readEntryAsync(dn, attributes) |
| | | .thenAsync(postEmptyPatchAsyncFunction(connection, request), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | } else { |
| | | // Add controls and perform the modify request. |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | modifyRequest.addControl( |
| | | PostReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (config.usePermissiveModify()) { |
| | | modifyRequest.addControl( |
| | | PermissiveModifyRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | return connection |
| | | .applyChangeAsync(modifyRequest) |
| | | .thenAsync( |
| | | postUpdateResultAsyncFunction(connection), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | } |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }); |
| | | if (resource.mayHaveSubResources() && useSubtreeDelete) { |
| | | // Non-critical so that we can detect failure and retry without the control. Some backends, |
| | | // such as cn=config, do not support the subtree delete control. |
| | | deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(false)); |
| | | } |
| | | addAssertionControl(deleteRequest, request.getRevision()); |
| | | return connection.applyChangeAsync(deleteRequest) |
| | | .thenCatchAsync(deleteSubtreeWithoutUsingSubtreeDeleteControl(connection, |
| | | deleteRequest)) |
| | | .thenAsync(encodeUpdateResourceResponse(connection, dnAndType.getType()), |
| | | adaptLdapException(ResourceResponse.class)); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** Just read the entry and check its version. */ |
| | | private Promise<ResourceResponse, ResourceException> emptyPatchInstance(final Connection connection, |
| | | final String resourceId, final PatchRequest request) { |
| | | final SearchRequest searchRequest = searchRequest(connection, resourceId, request.getFields()); |
| | | return connection |
| | | .searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(postEmptyPatchAsyncFunction(connection, request), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | /** |
| | | * Detects whether a delete request failed because the targeted entry has children and the subtree delete control |
| | | * could not be applied (e.g. due to ACIs or lack of support in the backend). On failure, fall-back to a search |
| | | * and then a recursive bottom up delete of all subordinate entries, before finally retrying the original delete |
| | | * request. |
| | | */ |
| | | private AsyncFunction<LdapException, Result, LdapException> deleteSubtreeWithoutUsingSubtreeDeleteControl( |
| | | final Connection connection, final ChangeRecord deleteRequest) { |
| | | return new AsyncFunction<LdapException, Result, LdapException>() { |
| | | @Override |
| | | public Promise<Result, LdapException> apply(final LdapException e) throws LdapException { |
| | | if (e.getResult().getResultCode().asEnum() != NOT_ALLOWED_ON_NONLEAF |
| | | || !resource.mayHaveSubResources()) { |
| | | throw e; |
| | | } |
| | | |
| | | // Perform a subtree search and then delete entries one by one. |
| | | final SearchRequest subordinates = newSearchRequest(deleteRequest.getName(), |
| | | SearchScope.SUBORDINATES, |
| | | Filter.objectClassPresent(), |
| | | "1.1"); |
| | | |
| | | // This list does not need synchronization because search result notification is synchronized. |
| | | final List<DN> subordinateEntries = new ArrayList<>(); |
| | | return connection.searchAsync(subordinates, new SearchResultHandler() { |
| | | @Override |
| | | public boolean handleEntry(final SearchResultEntry entry) { |
| | | subordinateEntries.add(entry.getName()); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean handleReference(final SearchResultReference reference) { |
| | | return false; |
| | | } |
| | | }).thenAsync(new AsyncFunction<Result, Result, LdapException>() { |
| | | @Override |
| | | public Promise<Result, LdapException> apply(final Result result) { |
| | | // Sort the entries in hierarchical order and then delete them in reverse, thus |
| | | // always deleting children before parents. |
| | | Collections.sort(subordinateEntries); |
| | | LdapPromise<Result> promise = newSuccessfulLdapPromise(newResult(ResultCode.SUCCESS)); |
| | | for (int i = subordinateEntries.size() - 1; i >= 0; i--) { |
| | | final ChangeRecord subordinateDelete = newDeleteRequest(subordinateEntries.get(i)); |
| | | promise = promise.thenAsync(new AsyncFunction<Result, Result, LdapException>() { |
| | | @Override |
| | | public Promise<Result, LdapException> apply(final Result result) { |
| | | return connection.applyChangeAsync(subordinateDelete); |
| | | } |
| | | }); |
| | | } |
| | | // And finally retry the original delete request. |
| | | return promise.thenAsync(new AsyncFunction<Result, Result, LdapException>() { |
| | | @Override |
| | | public Promise<Result, LdapException> apply(final Result result) { |
| | | return connection.applyChangeAsync(deleteRequest); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction( |
| | | final Connection connection, final PatchRequest request) { |
| | | return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | Promise<ResourceResponse, ResourceException> patch( |
| | | final Context context, final String resourceId, final PatchRequest request) { |
| | | final Connection connection = connectionFrom(context); |
| | | final AtomicReference<RoutingContext> dnAndTypeHolder = new AtomicReference<>(); |
| | | return resolveResourceDnAndType(context, connection, resourceId, request.getRevision()) |
| | | .thenAsync(new AsyncFunction<RoutingContext, List<List<Modification>>, ResourceException>() { |
| | | @Override |
| | | public Promise<List<List<Modification>>, ResourceException> apply(final RoutingContext dnAndType) |
| | | throws ResourceException { |
| | | dnAndTypeHolder.set(dnAndType); |
| | | |
| | | // Convert the patch operations to LDAP modifications. |
| | | final List<Promise<List<Modification>, ResourceException>> promises = |
| | | new ArrayList<>(request.getPatchOperations().size()); |
| | | final Resource subType = dnAndType.getType(); |
| | | final PropertyMapper propertyMapper = subType.getPropertyMapper(); |
| | | for (final PatchOperation operation : request.getPatchOperations()) { |
| | | promises.add(propertyMapper.patch(connection, subType, ROOT, operation)); |
| | | } |
| | | return when(promises); |
| | | } |
| | | }).thenAsync(new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final List<List<Modification>> result) |
| | | throws ResourceException { |
| | | // The patch operations have been converted successfully. |
| | | final RoutingContext dnAndType = dnAndTypeHolder.get(); |
| | | final ModifyRequest modifyRequest = newModifyRequest(dnAndType.getDn()); |
| | | |
| | | // Add the modifications. |
| | | for (final List<Modification> modifications : result) { |
| | | if (modifications != null) { |
| | | modifyRequest.getModifications().addAll(modifications); |
| | | } |
| | | } |
| | | |
| | | final Resource subType = dnAndType.getType(); |
| | | final Set<String> attributes = getLdapAttributesForKnownType(request.getFields(), subType); |
| | | if (modifyRequest.getModifications().isEmpty()) { |
| | | // This patch is a no-op so just read the entry and check its version. |
| | | return connection.readEntryAsync(dnAndType.getDn(), attributes) |
| | | .thenAsync(encodeEmptyPatchResourceResponse(connection, subType, request), |
| | | adaptLdapException(ResourceResponse.class)); |
| | | } else { |
| | | // Add controls and perform the modify request. |
| | | if (readOnUpdatePolicy == CONTROLS) { |
| | | modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (usePermissiveModify) { |
| | | modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | return connection.applyChangeAsync(modifyRequest) |
| | | .thenAsync(encodeUpdateResourceResponse(connection, subType), |
| | | adaptLdapException(ResourceResponse.class)); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private AsyncFunction<Entry, ResourceResponse, ResourceException> encodeEmptyPatchResourceResponse( |
| | | final Connection connection, final Resource resource, final PatchRequest request) { |
| | | return new AsyncFunction<Entry, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) |
| | | throws ResourceException { |
| | | public Promise<ResourceResponse, ResourceException> apply(Entry entry) throws ResourceException { |
| | | try { |
| | | // Fail if there is a version mismatch. |
| | | ensureMvccVersionMatches(entry, request.getRevision()); |
| | | return adaptEntry(connection, entry); |
| | | return encodeResourceResponse(connection, resource, entry); |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | return asResourceException(e).asPromise(); |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> queryCollection( |
| | | Promise<QueryResponse, ResourceException> query( |
| | | final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) { |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | // Calculate the filter (this may require the connection). |
| | | final Connection connection = connectionFrom(context); |
| | | return getLdapFilter(connection, request.getQueryFilter()) |
| | | .thenAsync(runQuery(request, resourceHandler, connection)); |
| | | } |
| | | |
| | | private Promise<Filter, ResourceException> getLdapFilter(final Connection connection, |
| | | final QueryFilter<JsonPointer> queryFilter) { |
| | | // FIXME: supporting assertions against sub-type properties. |
| | | private Promise<Filter, ResourceException> getLdapFilter( |
| | | final Connection connection, final QueryFilter<JsonPointer> queryFilter) { |
| | | if (queryFilter == null) { |
| | | return new BadRequestException(ERR_QUERY_BY_ID_OR_EXPRESSION_NOT_SUPPORTED.get().toString()).asPromise(); |
| | | } |
| | | final PropertyMapper propertyMapper = resource.getPropertyMapper(); |
| | | 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) { |
| | | 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>() { |
| | | return when(promises).then(new Function<List<Filter>, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(final List<Filter> value) { |
| | | // Check for unmapped filter components and optimize. |
| | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitBooleanLiteralFilter( |
| | | final Void unused, final boolean value) { |
| | | return Promises.newResultPromise(toFilter(value)); |
| | | return newResultPromise(toFilter(value)); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitContainsFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return propertyMapper.getLdapFilter( |
| | | connection, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion); |
| | | connection, resource, ROOT, field, CONTAINS, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitEqualsFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return propertyMapper.getLdapFilter( |
| | | connection, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion); |
| | | connection, resource, ROOT, field, EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused, |
| | | final JsonPointer field, final String operator, final Object valueAssertion) { |
| | | final JsonPointer field, |
| | | final String operator, |
| | | final Object valueAssertion) { |
| | | return propertyMapper.getLdapFilter( |
| | | connection, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion); |
| | | connection, resource, ROOT, field, EXTENDED, operator, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitGreaterThanFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return propertyMapper.getLdapFilter( |
| | | connection, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion); |
| | | connection, resource, ROOT, field, GREATER_THAN, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return propertyMapper.getLdapFilter(connection, new JsonPointer(), field, |
| | | FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion); |
| | | return propertyMapper.getLdapFilter( |
| | | connection, resource, ROOT, field, GREATER_THAN_OR_EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitLessThanFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return propertyMapper.getLdapFilter( |
| | | connection, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion); |
| | | connection, resource, ROOT, field, LESS_THAN, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return propertyMapper.getLdapFilter(connection, new JsonPointer(), field, |
| | | FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion); |
| | | return propertyMapper.getLdapFilter( |
| | | connection, resource, ROOT, field, LESS_THAN_OR_EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitOrFilter(final Void unused, |
| | | final List<QueryFilter<JsonPointer>> subFilters) { |
| | | 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>() { |
| | | return when(promises).then(new Function<List<Filter>, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(final List<Filter> value) { |
| | | // Check for unmapped filter components and optimize. |
| | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitPresentFilter( |
| | | final Void unused, final JsonPointer field) { |
| | | return propertyMapper.getLdapFilter( |
| | | connection, new JsonPointer(), field, FilterType.PRESENT, null, null); |
| | | return propertyMapper.getLdapFilter(connection, resource, ROOT, field, PRESENT, null, null); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitStartsWithFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return propertyMapper.getLdapFilter( |
| | | connection, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion); |
| | | connection, resource, ROOT, field, STARTS_WITH, null, valueAssertion); |
| | | } |
| | | |
| | | }; |
| | | // Note that the returned LDAP filter may be null if it could not be mapped by any property mappers. |
| | | return queryFilter.accept(visitor, null); |
| | | } |
| | | |
| | | private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request, |
| | | final QueryResourceHandler resourceHandler, final Connection connection) { |
| | | private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery( |
| | | final QueryRequest request, final QueryResourceHandler resourceHandler, 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 ResourceException pendingResult; |
| | |
| | | public Promise<QueryResponse, ResourceException> apply(final Filter ldapFilter) { |
| | | if (ldapFilter == null || ldapFilter == alwaysFalse()) { |
| | | // Avoid performing a search if the filter could not be mapped or if it will never match. |
| | | return Promises.newResultPromise(Responses.newQueryResponse()); |
| | | return newQueryResponse().asPromise(); |
| | | } |
| | | final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create(); |
| | | // Perform the search. |
| | | final String[] attributes = getLdapAttributes(connection, request.getFields()); |
| | | final String[] attributes = getLdapAttributesForUnknownType(request.getFields()).toArray(new String[0]); |
| | | final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent() |
| | | : ldapFilter; |
| | | final SearchRequest searchRequest = newSearchRequest( |
| | | getBaseDn(), SearchScope.SINGLE_LEVEL, searchFilter, attributes); |
| | | : ldapFilter; |
| | | final SearchRequest searchRequest = newSearchRequest(baseDn, 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) { |
| | |
| | | * The best solution is probably to process the primary search results in batches using |
| | | * the paged results control. |
| | | */ |
| | | final String id = namingStrategy.getResourceId(connection, entry); |
| | | final String id = namingStrategy.decodeResourceId(entry); |
| | | final String revision = getRevisionFromEntry(entry); |
| | | propertyMapper.read(connection, new JsonPointer(), entry) |
| | | final Resource subType = resource.resolveSubTypeFromObjectClasses(entry); |
| | | final PropertyMapper propertyMapper = subType.getPropertyMapper(); |
| | | propertyMapper.read(connection, subType, ROOT, entry) |
| | | .thenOnResult(new ResultHandler<JsonValue>() { |
| | | @Override |
| | | public void handleResult(final JsonValue result) { |
| | | synchronized (sequenceLock) { |
| | | pendingResourceCount--; |
| | | if (!resultSent) { |
| | | resourceHandler.handleResource( |
| | | Responses.newResourceResponse(id, revision, result)); |
| | | } |
| | | completeIfNecessary(promise); |
| | | } |
| | | } |
| | | }).thenOnException(new ExceptionHandler<ResourceException>() { |
| | | @Override |
| | | public void handleException(ResourceException exception) { |
| | | synchronized (sequenceLock) { |
| | | pendingResourceCount--; |
| | | completeIfNecessary(exception, promise); |
| | | } |
| | | } |
| | | }); |
| | | @Override |
| | | public void handleResult(final JsonValue result) { |
| | | synchronized (sequenceLock) { |
| | | pendingResourceCount--; |
| | | if (!resultSent) { |
| | | resourceHandler.handleResource( |
| | | newResourceResponse(id, revision, result)); |
| | | } |
| | | completeIfNecessary(promise); |
| | | } |
| | | } |
| | | }) |
| | | .thenOnException(new ExceptionHandler<ResourceException>() { |
| | | @Override |
| | | public void handleException(ResourceException exception) { |
| | | synchronized (sequenceLock) { |
| | | pendingResourceCount--; |
| | | completeIfNecessary(exception, promise); |
| | | } |
| | | } |
| | | }); |
| | | return true; |
| | | } |
| | | |
| | | @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; |
| | | } |
| | | |
| | |
| | | if (request.getPageSize() > 0) { |
| | | try { |
| | | final SimplePagedResultsControl control = |
| | | result.getControl(SimplePagedResultsControl.DECODER, DECODE_OPTIONS); |
| | | result.getControl(SimplePagedResultsControl.DECODER, decodeOptions); |
| | | if (control != null && !control.getCookie().isEmpty()) { |
| | | cookie = control.getCookie().toBase64String(); |
| | | } |
| | |
| | | } |
| | | }).thenOnException(new ExceptionHandler<LdapException>() { |
| | | @Override |
| | | public void handleException(LdapException exception) { |
| | | public void handleException(final LdapException e) { |
| | | synchronized (sequenceLock) { |
| | | completeIfNecessary(asResourceException(exception), promise); |
| | | if (glueObjectClasses != null && e instanceof EntryNotFoundException) { |
| | | // Glue entry does not exist, so treat this as an empty result set. |
| | | completeIfNecessary(SUCCESS, promise); |
| | | } else { |
| | | completeIfNecessary(asResourceException(e), promise); |
| | | } |
| | | } |
| | | } |
| | | }); |
| | |
| | | private void completeIfNecessary(final PromiseImpl<QueryResponse, ResourceException> handler) { |
| | | if (pendingResourceCount == 0 && pendingResult != null && !resultSent) { |
| | | if (pendingResult == SUCCESS) { |
| | | handler.handleResult(Responses.newQueryResponse(cookie)); |
| | | handler.handleResult(newQueryResponse(cookie)); |
| | | } else { |
| | | handler.handleException(pendingResult); |
| | | } |
| | |
| | | }; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> readInstance( |
| | | Promise<ResourceResponse, ResourceException> read( |
| | | final Context context, final String resourceId, final ReadRequest request) { |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | // Do the search. |
| | | SearchRequest searchRequest = searchRequest(connection, resourceId, request.getFields()); |
| | | return connection |
| | | .searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) |
| | | throws ResourceException { |
| | | return adaptEntry(connection, entry); |
| | | } |
| | | }, Exceptions.<ResourceResponse>toResourceException()); |
| | | final Connection connection = connectionFrom(context); |
| | | return connection.searchSingleEntryAsync(searchRequestForUnknownType(resourceId, request.getFields())) |
| | | .thenCatchAsync(adaptLdapException(SearchResultEntry.class)) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) { |
| | | final Resource subType = resource.resolveSubTypeFromObjectClasses(entry); |
| | | return encodeResourceResponse(connection, subType, entry); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> updateInstance( |
| | | Promise<ResourceResponse, ResourceException> update( |
| | | final Context context, final String resourceId, final UpdateRequest request) { |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | List<JsonPointer> attrs = Collections.emptyList(); |
| | | SearchRequest searchRequest = searchRequest(connection, resourceId, attrs); |
| | | final Connection connection = connectionFrom(context); |
| | | final AtomicReference<Entry> entryHolder = new AtomicReference<>(); |
| | | final AtomicReference<Resource> subTypeHolder = new AtomicReference<>(); |
| | | return connection |
| | | .searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | .searchSingleEntryAsync(searchRequestForUnknownType(resourceId, Collections.<JsonPointer>emptyList())) |
| | | .thenCatchAsync(adaptLdapException(SearchResultEntry.class)) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, List<Modification>, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | final SearchResultEntry entry) { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMvccVersionMatches(entry, request.getRevision()); |
| | | public Promise<List<Modification>, ResourceException> apply(final SearchResultEntry entry) |
| | | throws ResourceException { |
| | | entryHolder.set(entry); |
| | | |
| | | // Create the modify request. |
| | | final ModifyRequest modifyRequest = newModifyRequest(entry.getName()); |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | final String[] attributes = |
| | | getLdapAttributes(connection, request.getFields()); |
| | | modifyRequest.addControl( |
| | | PostReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (config.usePermissiveModify()) { |
| | | modifyRequest.addControl( |
| | | PermissiveModifyRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMvccVersionMatches(entry, request.getRevision()); |
| | | |
| | | // Determine the set of changes that need to be performed. |
| | | return propertyMapper.update( |
| | | connection, new JsonPointer(), entry, request.getContent()) |
| | | .thenAsync(new AsyncFunction< |
| | | List<Modification>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | 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(connection, entry); |
| | | } |
| | | // Perform the modify operation. |
| | | modifyRequest.getModifications().addAll(modifications); |
| | | return connection |
| | | .applyChangeAsync(modifyRequest) |
| | | .thenAsync( |
| | | postUpdateResultAsyncFunction(connection), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | } |
| | | }); |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | // Determine the type of resource and set of changes that need to be performed. |
| | | final Resource subType = resource.resolveSubTypeFromObjectClasses(entry); |
| | | subTypeHolder.set(subType); |
| | | final PropertyMapper propertyMapper = subType.getPropertyMapper(); |
| | | return propertyMapper.update(connection, subType , ROOT, entry, request.getContent()); |
| | | } |
| | | }, Exceptions.<ResourceResponse>toResourceException()); |
| | | }).thenAsync(new AsyncFunction<List<Modification>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(List<Modification> modifications) |
| | | throws ResourceException { |
| | | final Resource subType = subTypeHolder.get(); |
| | | if (modifications.isEmpty()) { |
| | | // No changes to be performed so just return the entry that we read. |
| | | return encodeResourceResponse(connection, subType, entryHolder.get()); |
| | | } |
| | | // Perform the modify operation. |
| | | final ModifyRequest modifyRequest = newModifyRequest(entryHolder.get().getName()); |
| | | if (readOnUpdatePolicy == CONTROLS) { |
| | | final Set<String> attributes = getLdapAttributesForKnownType(request.getFields(), subType); |
| | | modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (usePermissiveModify) { |
| | | modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | modifyRequest.getModifications().addAll(modifications); |
| | | return connection.applyChangeAsync(modifyRequest) |
| | | .thenAsync(encodeUpdateResourceResponse(connection, subType), |
| | | adaptLdapException(ResourceResponse.class)); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private Promise<ResourceResponse, ResourceException> adaptEntry(final Connection connection, final Entry entry) { |
| | | final String actualResourceId = namingStrategy.getResourceId(connection, entry); |
| | | final String revision = getRevisionFromEntry(entry); |
| | | return propertyMapper.read(connection, new JsonPointer(), entry) |
| | | private Promise<ResourceResponse, ResourceException> encodeResourceResponse( |
| | | final Connection connection, final Resource resource, final Entry entry) { |
| | | final PropertyMapper propertyMapper = resource.getPropertyMapper(); |
| | | return propertyMapper.read(connection, resource, ROOT, entry) |
| | | .then(new Function<JsonValue, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public ResourceResponse apply(final JsonValue value) { |
| | | return newResourceResponse( |
| | | actualResourceId, revision, new JsonValue(value)); |
| | | } |
| | | @Override |
| | | public ResourceResponse apply(final JsonValue value) { |
| | | final String revision = getRevisionFromEntry(entry); |
| | | final String actualResourceId = namingStrategy.decodeResourceId(entry); |
| | | return newResourceResponse(actualResourceId, revision, new JsonValue(value)); |
| | | } |
| | | }); |
| | | } |
| | | |
| | |
| | | throws ResourceException { |
| | | if (expectedRevision != null) { |
| | | ensureMvccSupported(); |
| | | request.addControl(AssertionRequestControl.newControl(true, Filter.equality( |
| | | etagAttribute.toString(), expectedRevision))); |
| | | final Filter filter = Filter.equality(etagAttribute.toString(), expectedRevision); |
| | | request.addControl(AssertionRequestControl.newControl(true, filter)); |
| | | } |
| | | } |
| | | |
| | | private Promise<DN, ResourceException> doUpdateFunction(final Connection connection, final String resourceId, |
| | | final String revision) { |
| | | final String ldapAttribute = (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1"; |
| | | final SearchRequest searchRequest = namingStrategy.createSearchRequest(connection, getBaseDn(), resourceId) |
| | | .addAttribute(ldapAttribute); |
| | | if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) { |
| | | // There's no point in doing a search because we already know the DN. |
| | | return Promises.newResultPromise(searchRequest.getName()); |
| | | private Promise<RoutingContext, ResourceException> resolveResourceDnAndType( |
| | | final Context context, final Connection connection, final String resourceId, final String revision) { |
| | | final SearchRequest searchRequest = namingStrategy.createSearchRequest(baseDn, resourceId); |
| | | if (searchRequest.getScope().equals(BASE_OBJECT) && !resource.hasSubTypes()) { |
| | | // There's no point in doing a search because we already know the DN and sub-resources. |
| | | return newResultPromise(new RoutingContext(context, searchRequest.getName(), resource)); |
| | | } |
| | | return connection |
| | | .searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() { |
| | | @Override |
| | | public Promise<DN, ResourceException> apply(SearchResultEntry entry) throws ResourceException { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMvccVersionMatches(entry, revision); |
| | | // Perform update operation. |
| | | return Promises.newResultPromise(entry.getName()); |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }, new AsyncFunction<LdapException, DN, ResourceException>() { |
| | | @Override |
| | | public Promise<DN, ResourceException> apply(LdapException ldapException) throws ResourceException { |
| | | return Promises.newExceptionPromise(asResourceException(ldapException)); |
| | | } |
| | | }); |
| | | if (etagAttribute != null && revision != null) { |
| | | searchRequest.addAttribute(etagAttribute.toString()); |
| | | } |
| | | // The resource type will be resolved from the LDAP entry's objectClass. |
| | | searchRequest.addAttribute("objectClass"); |
| | | return connection.searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, RoutingContext, ResourceException>() { |
| | | @Override |
| | | public Promise<RoutingContext, ResourceException> apply(final SearchResultEntry entry) |
| | | throws ResourceException { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMvccVersionMatches(entry, revision); |
| | | final Resource subType = resource.resolveSubTypeFromObjectClasses(entry); |
| | | return newResultPromise(new RoutingContext(context, entry.getName(), subType)); |
| | | } |
| | | }, adaptLdapException(RoutingContext.class)); |
| | | } |
| | | |
| | | private void ensureMvccSupported() throws NotSupportedException { |
| | |
| | | } |
| | | } |
| | | |
| | | private DN getBaseDn() { |
| | | return baseDn; |
| | | private Set<String> getLdapAttributesForUnknownType(final Collection<JsonPointer> fields) { |
| | | final Set<String> ldapAttributes = getLdapAttributesForKnownType(fields, resource); |
| | | getLdapAttributesForUnknownType(fields, resource, ldapAttributes); |
| | | return ldapAttributes; |
| | | } |
| | | |
| | | /** |
| | | * Determines the set of LDAP attributes to request in an LDAP read (search, |
| | | * post-read), based on the provided list of JSON pointers. |
| | | * |
| | | * @param connection |
| | | * 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 Connection connection, final Collection<JsonPointer> requestedAttributes) { |
| | | // Get all the LDAP attributes required by the property mappers. |
| | | final Set<String> requestedLDAPAttributes; |
| | | if (requestedAttributes.isEmpty()) { |
| | | private void getLdapAttributesForUnknownType(final Collection<JsonPointer> fields, final Resource resource, |
| | | final Set<String> ldapAttributes) { |
| | | for (final Resource subType : resource.getSubTypes()) { |
| | | addLdapAttributesForFields(fields, subType, ldapAttributes); |
| | | getLdapAttributesForUnknownType(fields, subType, ldapAttributes); |
| | | } |
| | | } |
| | | |
| | | private Set<String> getLdapAttributesForKnownType(final Collection<JsonPointer> fields, final Resource resource) { |
| | | // Includes the LDAP attributes required by the type, etag, and name strategies. |
| | | final Set<String> ldapAttributes = new LinkedHashSet<>(); |
| | | ldapAttributes.add("objectClass"); |
| | | final String resourceIdLdapAttribute = namingStrategy.getResourceIdLdapAttribute(); |
| | | if (resourceIdLdapAttribute != null) { |
| | | ldapAttributes.add(resourceIdLdapAttribute); |
| | | } |
| | | if (etagAttribute != null) { |
| | | ldapAttributes.add(etagAttribute.toString()); |
| | | } |
| | | addLdapAttributesForFields(fields, resource, ldapAttributes); |
| | | return ldapAttributes; |
| | | } |
| | | |
| | | /** Includes the LDAP attributes required for the specified JSON fields for all sub-types. */ |
| | | private void addLdapAttributesForFields(final Collection<JsonPointer> fields, final Resource resource, |
| | | final Set<String> ldapAttributes) { |
| | | final PropertyMapper propertyMapper = resource.getPropertyMapper(); |
| | | if (fields.isEmpty()) { |
| | | // Full read. |
| | | requestedLDAPAttributes = new LinkedHashSet<>(); |
| | | propertyMapper.getLdapAttributes(connection, new JsonPointer(), new JsonPointer(), |
| | | requestedLDAPAttributes); |
| | | propertyMapper.getLdapAttributes(ROOT, ROOT, ldapAttributes); |
| | | } else { |
| | | // Partial read. |
| | | requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size()); |
| | | for (final JsonPointer requestedAttribute : requestedAttributes) { |
| | | propertyMapper.getLdapAttributes(connection, new JsonPointer(), requestedAttribute, |
| | | requestedLDAPAttributes); |
| | | for (final JsonPointer field : fields) { |
| | | propertyMapper.getLdapAttributes(ROOT, field, ldapAttributes); |
| | | } |
| | | } |
| | | |
| | | // Get the LDAP attributes required by the Etag and name stategies. |
| | | namingStrategy.getLdapAttributes(connection, requestedLDAPAttributes); |
| | | if (etagAttribute != null) { |
| | | requestedLDAPAttributes.add(etagAttribute.toString()); |
| | | } |
| | | return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]); |
| | | } |
| | | |
| | | private String getRevisionFromEntry(final Entry entry) { |
| | | return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null; |
| | | } |
| | | |
| | | private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction( |
| | | final Connection connection) { |
| | | // The handler which will be invoked for the LDAP add result. |
| | | private AsyncFunction<Result, ResourceResponse, ResourceException> encodeUpdateResourceResponse( |
| | | final Connection connection, final Resource resource) { |
| | | return new AsyncFunction<Result, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(Result result) throws ResourceException { |
| | | public Promise<ResourceResponse, ResourceException> apply(Result result) { |
| | | // FIXME: handle USE_SEARCH policy. |
| | | Entry entry; |
| | | try { |
| | | final PostReadResponseControl postReadControl = |
| | | result.getControl(PostReadResponseControl.DECODER, config.decodeOptions()); |
| | | result.getControl(PostReadResponseControl.DECODER, decodeOptions); |
| | | if (postReadControl != null) { |
| | | entry = postReadControl.getEntry(); |
| | | } else { |
| | | final PreReadResponseControl preReadControl = |
| | | result.getControl(PreReadResponseControl.DECODER, config.decodeOptions()); |
| | | if (preReadControl != null) { |
| | | entry = preReadControl.getEntry(); |
| | | } else { |
| | | entry = null; |
| | | } |
| | | return encodeResourceResponse(connection, resource, postReadControl.getEntry()); |
| | | } |
| | | final PreReadResponseControl preReadControl = |
| | | result.getControl(PreReadResponseControl.DECODER, decodeOptions); |
| | | if (preReadControl != null) { |
| | | return encodeResourceResponse(connection, resource, preReadControl.getEntry()); |
| | | } |
| | | } catch (final DecodeException e) { |
| | | logger.error(ERR_DECODING_CONTROL.get(e.getLocalizedMessage()), e); |
| | | entry = null; |
| | | } |
| | | if (entry != null) { |
| | | return adaptEntry(connection, entry); |
| | | } else { |
| | | return Promises.newResultPromise( |
| | | newResourceResponse(null, null, new JsonValue(Collections.emptyMap()))); |
| | | } |
| | | // Return an empty resource response. |
| | | return newResourceResponse(null, null, new JsonValue(Collections.emptyMap())).asPromise(); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private SearchRequest searchRequest( |
| | | final Connection connection, final String resourceId, final List<JsonPointer> requestedAttributes) { |
| | | final String[] attributes = getLdapAttributes(connection, requestedAttributes); |
| | | return namingStrategy.createSearchRequest(connection, getBaseDn(), resourceId).addAttribute(attributes); |
| | | private SearchRequest searchRequestForUnknownType(final String resourceId, final List<JsonPointer> fields) { |
| | | final String[] attributes = getLdapAttributesForUnknownType(fields).toArray(new String[0]); |
| | | return namingStrategy.createSearchRequest(baseDn, resourceId).addAttribute(attributes); |
| | | } |
| | | |
| | | private static final class Exceptions { |
| | | private static <R> AsyncFunction<LdapException, R, ResourceException> toResourceException() { |
| | | // The handler which will be invoked for the LDAP add result. |
| | | return new AsyncFunction<LdapException, R, ResourceException>() { |
| | | @Override |
| | | public Promise<R, ResourceException> apply(final LdapException ldapException) throws ResourceException { |
| | | return Promises.newExceptionPromise(asResourceException(ldapException)); |
| | | } |
| | | }; |
| | | } |
| | | @SuppressWarnings("unused") |
| | | private static <R> AsyncFunction<LdapException, R, ResourceException> adaptLdapException(final Class<R> clazz) { |
| | | return new AsyncFunction<LdapException, R, ResourceException>() { |
| | | @Override |
| | | public Promise<R, ResourceException> apply(final LdapException ldapException) { |
| | | return asResourceException(ldapException).asPromise(); |
| | | } |
| | | }; |
| | | } |
| | | } |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.http.routing.RoutingMode.EQUALS; |
| | | import static org.forgerock.http.routing.RoutingMode.STARTS_WITH; |
| | | import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher; |
| | | import static org.forgerock.opendj.ldap.Filter.objectClassPresent; |
| | | import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON; |
| | | import static org.forgerock.util.promise.Promises.newResultPromise; |
| | | |
| | | import org.forgerock.json.resource.ActionRequest; |
| | | import org.forgerock.json.resource.ActionResponse; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.CreateRequest; |
| | | import org.forgerock.json.resource.DeleteRequest; |
| | | import org.forgerock.json.resource.PatchRequest; |
| | | import org.forgerock.json.resource.QueryRequest; |
| | | import org.forgerock.json.resource.QueryResourceHandler; |
| | | import org.forgerock.json.resource.QueryResponse; |
| | | import org.forgerock.json.resource.ReadRequest; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.json.resource.UpdateRequest; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * Represents a one to one relationship between a parent resource and a child sub-resource. Removal of the parent |
| | | * resource implies that the child (the sub-resource) is also removed. Singletons only support read, update, patch, and |
| | | * action requests. |
| | | */ |
| | | public final class SubResourceSingleton extends SubResource { |
| | | /** |
| | | * A simple naming strategy that allows singletons to use the same processing logic as collections. The passed in |
| | | * resource ID will always be {@code null}. |
| | | */ |
| | | private static final NamingStrategy SINGLETON_NAMING_STRATEGY = new NamingStrategy() { |
| | | @Override |
| | | public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) { |
| | | return newSearchRequest(baseDn, BASE_OBJECT, objectClassPresent()); |
| | | } |
| | | |
| | | @Override |
| | | public String getResourceIdLdapAttribute() { |
| | | // Nothing to do. |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public String decodeResourceId(final Entry entry) { |
| | | // It's safe to return null. The resource response will default to the _id field if present. |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public void encodeResourceId(final DN baseDn, final String resourceId, final Entry entry) |
| | | throws ResourceException { |
| | | // Nothing to do because singletons cannot be created. |
| | | } |
| | | }; |
| | | |
| | | SubResourceSingleton(final String resourceId) { |
| | | super(resourceId); |
| | | } |
| | | |
| | | /** |
| | | * Sets the relative URL template of the single sub-resource. The template must comprise of at least one path |
| | | * element. Any URL template variables will be substituted into the {@link #dnTemplate(String) DN template}. |
| | | * |
| | | * @param urlTemplate |
| | | * The relative URL template. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceSingleton urlTemplate(final String urlTemplate) { |
| | | this.urlTemplate = urlTemplate; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the relative DN template of the single sub-resource LDAP entry. The template must comprise of at least one |
| | | * RDN. Any DN template variables will be substituted using values extracted from the {@link #urlTemplate(String) |
| | | * URL template}. |
| | | * |
| | | * @param dnTemplate |
| | | * The relative DN template. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceSingleton dnTemplate(final String dnTemplate) { |
| | | this.dnTemplate = dnTemplate; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates whether this sub-resource singleton only supports read operations. |
| | | * |
| | | * @param readOnly |
| | | * {@code true} if this sub-resource singleton is read-only. |
| | | * @return A reference to this object. |
| | | */ |
| | | public SubResourceSingleton isReadOnly(final boolean readOnly) { |
| | | isReadOnly = readOnly; |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | Router addRoutes(final Router router) { |
| | | router.addRoute(requestUriMatcher(EQUALS, urlTemplate), readOnly(new InstanceHandler())); |
| | | router.addRoute(requestUriMatcher(STARTS_WITH, urlTemplate), readOnly(new SubResourceHandler())); |
| | | return router; |
| | | } |
| | | |
| | | private Promise<RoutingContext, ResourceException> route(final Context context) { |
| | | return newResultPromise(new RoutingContext(context, dnFrom(context), resource)); |
| | | } |
| | | |
| | | private SubResourceImpl singleton(final Context context) { |
| | | return new SubResourceImpl(rest2Ldap, dnFrom(context), null, SINGLETON_NAMING_STRATEGY, resource); |
| | | } |
| | | |
| | | /** |
| | | * Responsible for processing instance requests (RUPA) against this singleton and collection requests (CQ) to |
| | | * any collections sharing the same base URL as this singleton. More specifically, given the |
| | | * URL template /singleton/{child} then this handler processes requests against /singleton since it is |
| | | * both a singleton and also a collection of {child}. |
| | | */ |
| | | private final class InstanceHandler extends AbstractRequestHandler { |
| | | private InstanceHandler() { |
| | | super(new BadRequestException(ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON.get().toString())); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> handleAction(final Context context, |
| | | final ActionRequest request) { |
| | | return singleton(context).action(context, null, request); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, |
| | | final CreateRequest request) { |
| | | return route(context) |
| | | .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleCreate(context, request); |
| | | } |
| | | }).thenCatch(this.<ResourceResponse>convert404To400()); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, |
| | | final PatchRequest request) { |
| | | return singleton(context).patch(context, null, request); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, |
| | | final QueryResourceHandler handler) { |
| | | return route(context) |
| | | .thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleQuery(context, request, handler); |
| | | } |
| | | }).thenCatch(this.<QueryResponse>convert404To400()); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleRead(final Context context, |
| | | final ReadRequest request) { |
| | | return singleton(context).read(context, null, request); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context, |
| | | final UpdateRequest request) { |
| | | return singleton(context).update(context, null, request); |
| | | } |
| | | |
| | | private <T> Function<ResourceException, T, ResourceException> convert404To400() { |
| | | return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON.get()); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Responsible for routing requests to sub-resources of this singleton. More specifically, given |
| | | * the URL template /singleton then this handler processes all requests beneath /singleton. |
| | | */ |
| | | private final class SubResourceHandler implements RequestHandler { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> handleAction(final Context context, |
| | | final ActionRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ActionResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleAction(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleCreate(final Context context, |
| | | final CreateRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleCreate(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleDelete(final Context context, |
| | | final DeleteRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleDelete(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, |
| | | final PatchRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handlePatch(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request, |
| | | final QueryResourceHandler handler) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleQuery(context, request, handler); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleRead(final Context context, |
| | | final ReadRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleRead(context, request); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context, |
| | | final UpdateRequest request) { |
| | | return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) { |
| | | return subResourceRouterFrom(context).handleUpdate(context, request); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | } |
| | |
| | | import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_JSON_VALUE; |
| | | |
| | | import java.io.BufferedReader; |
| | | import java.io.File; |
| | | import java.io.FileReader; |
| | | import java.io.IOException; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.Locale; |
| | |
| | | } |
| | | }; |
| | | |
| | | static String readPasswordFromFile(String fileName) throws IOException { |
| | | try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) { |
| | | return reader.readLine(); |
| | | } |
| | | } |
| | | |
| | | static Function<Object, ByteString, NeverThrowsException> base64ToByteString() { |
| | | return BASE64_TO_BYTESTRING; |
| | | } |
| | |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Stub formatter for i18n strings. |
| | | * |
| | | * @param format |
| | | * The format string. |
| | | * @param args |
| | | * The string arguments. |
| | | * @return The formatted string. |
| | | */ |
| | | static String i18n(final String format, final Object... args) { |
| | | return String.format(format, args); |
| | | } |
| | | |
| | | private static boolean isJsonPrimitive(final Object value) { |
| | | return value instanceof String || value instanceof Boolean || value instanceof Number; |
| | | } |
| | |
| | | } |
| | | return a; |
| | | } else { |
| | | throw newLocalizedIllegalArgumentException(ERR_UNRECOGNIZED_JSON_VALUE.get(value.getClass().getName())); |
| | | throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_JSON_VALUE.get(value.getClass().getName())); |
| | | } |
| | | } |
| | | |
| | |
| | | return ByteString.valueOfObject(value); |
| | | } |
| | | } else { |
| | | throw newLocalizedIllegalArgumentException( |
| | | ERR_UNRECOGNIZED_JSON_VALUE.get(value.getClass().getName())); |
| | | throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_JSON_VALUE.get(value.getClass() |
| | | .getName())); |
| | | } |
| | | } |
| | | }; |
| | |
| | | return new JsonValueException(value, message.toString()); |
| | | } |
| | | |
| | | static LocalizedIllegalArgumentException newLocalizedIllegalArgumentException(final LocalizableMessage message) { |
| | | return new LocalizedIllegalArgumentException(message); |
| | | } |
| | | |
| | | static BadRequestException newBadRequestException(final LocalizableMessage message) { |
| | | return newBadRequestException(message, null); |
| | | } |
| | |
| | | import org.forgerock.util.promise.Promises; |
| | | |
| | | final class Utils { |
| | | private static final AsyncFunction<LdapException, Response, NeverThrowsException> HANDLE_CONNECTION_FAILURE = |
| | | new AsyncFunction<LdapException, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(final LdapException exception) { |
| | | return asErrorResponse(exception); |
| | | } |
| | | }; |
| | | |
| | | private Utils() { } |
| | | |
| | |
| | | } |
| | | |
| | | static AsyncFunction<LdapException, Response, NeverThrowsException> handleConnectionFailure() { |
| | | return new AsyncFunction<LdapException, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(final LdapException exception) { |
| | | return asErrorResponse(exception); |
| | | } |
| | | }; |
| | | return HANDLE_CONNECTION_FAILURE; |
| | | } |
| | | |
| | | static 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()); |
| | | final Response response = new Response().setStatus(Status.valueOf(e.getCode())) |
| | | .setEntity(e.toJsonValue() .getObject()); |
| | | if (response.getStatus() == Status.UNAUTHORIZED) { |
| | | response.getHeaders().put("WWW-Authenticate", "Basic"); |
| | | } |
| | |
| | | ERR_ENCODING_VALUES_FOR_FIELD_52=The request cannot be processed because an error occurred while encoding \ |
| | | the values for the field '%s': '%s' |
| | | ERR_UNRECOGNIZED_JSON_VALUE_53=Unrecognized type of JSON value: '%s' |
| | | ERR_CLIENT_PROVIDER_RESOURCE_ID_MISSING_54=Resources cannot be created without a client provided resource ID |
| | | ERR_CLIENT_PROVIDED_RESOURCE_ID_MISSING_54=Resources cannot be created without a client provided resource ID |
| | | ERR_NOT_YET_IMPLEMENTED_55=Not yet implemented |
| | | ERR_ACTION_NOT_SUPPORTED_56=The action '%s' is not supported |
| | | ERR_PASSWORD_MODIFY_SECURE_CONNECTION_57=Password modify requires a secure connection |
| | |
| | | A password modify request may contain two string valued fields 'oldPassword' and 'newPassword' |
| | | ERR_CONFIG_INVALID_TRUST_MANAGER_63=The trust-manager defined in '%s' is invalid: %s |
| | | ERR_CONFIG_INVALID_KEY_MANAGER_64=The key-manager defined in '%s' is invalid: %s |
| | | ERR_MISSING_TYPE_PROPERTY_IN_CREATE_65=The resource cannot be created because it does not contain the \ |
| | | type property '%s' |
| | | ERR_UNRECOGNIZED_TYPE_IN_CREATE_66=The resource cannot be created because it specified an unrecognized resource \ |
| | | type '%s'. Please specify one of the following types: %s |
| | | ERR_ABSTRACT_TYPE_IN_CREATE_67=The resource cannot be created because it specified the abstract resource type '%s'. \ |
| | | Please specify one of the following non-abstract types: %s |
| | | ERR_QUERY_BY_ID_OR_EXPRESSION_NOT_SUPPORTED_68=Queries using _queryId or _queryExpression are not supported. Use \ |
| | | _queryFilter instead |
| | | ERR_READ_ONLY_ENDPOINT_69=This endpoint is read-only and only supports read and query requests |
| | | ERR_UNSUPPORTED_REQUEST_AGAINST_COLLECTION_70=The targeted resource is a resource collection which only \ |
| | | supports create and query requests |
| | | ERR_UNSUPPORTED_REQUEST_AGAINST_INSTANCE_71=The targeted resource is a resource instance which only supports \ |
| | | read, update, delete, patch, and action requests |
| | | ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON_72=The targeted resource is a resource singleton which only supports \ |
| | | read, update, patch, and action requests |
| | | ERR_SERVER_PROVIDED_RESOURCE_ID_UNEXPECTED_73=Resources cannot be created with a client provided resource ID. The \ |
| | | server will automatically generate a resource ID |
| | | ERR_COLLECTION_ACTIONS_NOT_SUPPORTED_74=Collections only support create or query requests |
| | | ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE_75=The resource '%s' has an unrecognized super-type '%s' |
| | | ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE_76=The resource '%s' references an unrecognized sub-resource '%s' |
| | | ERR_MISSING_REQUIRED_FIELD_77=The create request cannot be processed because it does not include the required field '%s' |
| | | ERR_INVALID_ENDPOINTS_DIRECTORY_78=The endpoints configuration directory '%s' either does not exist, is not a \ |
| | | directory or cannot be read |
| | | ERR_INVALID_ENDPOINT_DIRECTORY_79=The endpoint configuration directory '%s' either does not exist, is not a \ |
| | | directory or cannot be read |
| | | INFO_REST2LDAP_STARTING_80=Rest2Ldap starting with configuration directory '%s' |
| | | INFO_REST2LDAP_CREATING_ENDPOINT_81=Rest2Ldap created endpoint '%s' version %s |
| | |
| | | import static org.forgerock.json.resource.PatchOperation.increment; |
| | | import static org.forgerock.json.resource.PatchOperation.remove; |
| | | import static org.forgerock.json.resource.PatchOperation.replace; |
| | | import static org.forgerock.json.resource.Requests.newDeleteRequest; |
| | | import static org.forgerock.json.resource.Requests.newPatchRequest; |
| | | import static org.forgerock.json.resource.Requests.newQueryRequest; |
| | | import static org.forgerock.json.resource.Requests.newReadRequest; |
| | | import static org.forgerock.json.resource.Requests.newUpdateRequest; |
| | | import static org.forgerock.json.resource.Resources.newCollection; |
| | | import static org.forgerock.json.resource.Requests.*; |
| | | import static org.forgerock.json.resource.Resources.newInternalConnection; |
| | | import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory; |
| | | import static org.forgerock.opendj.ldap.Functions.byteStringToInteger; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.collectionOf; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.constant; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.rest2Ldap; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.object; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.resource; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.simple; |
| | | import static org.forgerock.opendj.rest2ldap.TestUtils.asResource; |
| | | import static org.forgerock.opendj.rest2ldap.TestUtils.content; |
| | | import static org.forgerock.opendj.rest2ldap.TestUtils.ctx; |
| | | import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.CREATE_ONLY; |
| | | import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_ONLY; |
| | | import static org.forgerock.util.Options.defaultOptions; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.ArrayList; |
| | |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.IntermediateResponseHandler; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.LdapResultHandler; |
| | | import org.forgerock.opendj.ldap.MemoryBackend; |
| | | import org.forgerock.opendj.ldap.RequestContext; |
| | |
| | | import org.forgerock.opendj.ldap.responses.ExtendedResult; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldif.LDIFEntryReader; |
| | | import org.forgerock.opendj.rest2ldap.Rest2Ldap.Builder; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.forgerock.util.query.QueryFilter; |
| | |
| | | public void testPatchEmpty() throws Exception { |
| | | final List<Request> requests = new LinkedList<>(); |
| | | final Context context = newAuthConnectionContext(requests); |
| | | final Connection connection = newConnection(requests); |
| | | final Connection connection = newConnection(); |
| | | final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1")); |
| | | checkResourcesAreEqual(resource1, getTestUser1(12345)); |
| | | |
| | |
| | | @Test |
| | | public void testUpdateNoChange() throws Exception { |
| | | final List<Request> requests = new LinkedList<>(); |
| | | final Connection connection = newConnection(requests); |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(requests); |
| | | final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", getTestUser1(12345))); |
| | | |
| | |
| | | } |
| | | |
| | | private Connection newConnection() throws IOException { |
| | | return newConnection(new LinkedList<Request>()); |
| | | return newInternalConnection(usersApi().newRequestHandlerFor("api")); |
| | | } |
| | | |
| | | private Connection newConnection(final List<Request> requests) throws IOException { |
| | | return newInternalConnection(newCollection(builder(requests).build())); |
| | | } |
| | | |
| | | private Builder builder(final List<Request> requests) throws IOException { |
| | | return Rest2Ldap.builder() |
| | | .baseDN("dc=test") |
| | | .useEtagAttribute() |
| | | .useClientDNNaming("uid") |
| | | .readOnUpdatePolicy(ReadOnUpdatePolicy.CONTROLS) |
| | | .additionalLDAPAttribute("objectClass", "top", "person") |
| | | .mapper(object() |
| | | .attribute("schemas", constant(asList("urn:scim:schemas:core:1.0"))) |
| | | .attribute("_id", simple("uid").isSingleValued() |
| | | .isRequired() |
| | | .writability(WritabilityPolicy.CREATE_ONLY)) |
| | | .attribute("name", object().attribute("displayName", simple("cn").isSingleValued() |
| | | .isRequired()) |
| | | .attribute("surname", simple("sn").isSingleValued().isRequired())) |
| | | .attribute("_rev", simple("etag").isSingleValued() |
| | | .isRequired() |
| | | .writability(WritabilityPolicy.READ_ONLY)) |
| | | .attribute("description", simple("description")) |
| | | .attribute("singleNumber", simple("singleNumber").decoder(byteStringToInteger()) |
| | | .isSingleValued()) |
| | | .attribute("multiNumber", simple("multiNumber").decoder(byteStringToInteger()))); |
| | | private Rest2Ldap usersApi() throws IOException { |
| | | return rest2Ldap(defaultOptions(), |
| | | resource("api").subResource(collectionOf("user").dnTemplate("dc=test") |
| | | .useClientDnNaming("uid")), |
| | | resource("user").objectClasses("top", "person") |
| | | .property("schemas", constant(asList("urn:scim:schemas:core:1.0"))) |
| | | .property("_id", simple("uid").isRequired(true).writability(CREATE_ONLY)) |
| | | .property("name", object().property("displayName", |
| | | simple("cn").isRequired(true)) |
| | | .property("surname", simple("sn").isRequired(true))) |
| | | .property("_rev", simple("etag").isRequired(true).writability(READ_ONLY)) |
| | | .property("description", simple("description").isMultiValued(true)) |
| | | .property("singleNumber", |
| | | simple("singleNumber").decoder(byteStringToInteger())) |
| | | .property("multiNumber", |
| | | simple("multiNumber").isMultiValued(true) |
| | | .decoder(byteStringToInteger()))); |
| | | } |
| | | |
| | | 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( |
| | | expectedResource.getContent().getObject()); |
| | | assertThat(actual.getContent().getObject()).isEqualTo(expectedResource.getContent().getObject()); |
| | | } |
| | | |
| | | private AuthenticatedConnectionContext newAuthConnectionContext() throws LdapException, IOException { |
| | | private AuthenticatedConnectionContext newAuthConnectionContext() throws IOException { |
| | | return newAuthConnectionContext(new ArrayList<Request>()); |
| | | } |
| | | |
| | | private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests) |
| | | throws LdapException, IOException { |
| | | private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests) throws IOException { |
| | | return new AuthenticatedConnectionContext(ctx(), getConnectionFactory(requests).getConnection()); |
| | | } |
| | | |
| 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 2016 ForgeRock AS. |
| | | * |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.assertj.core.api.Assertions.fail; |
| | | import static org.forgerock.json.JsonValue.array; |
| | | import static org.forgerock.json.JsonValue.json; |
| | | import static org.forgerock.json.resource.PatchOperation.replace; |
| | | import static org.forgerock.json.resource.Requests.*; |
| | | import static org.forgerock.json.resource.Responses.newResourceResponse; |
| | | import static org.forgerock.opendj.ldap.Connections.newInternalConnection; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*; |
| | | import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.CREATE_ONLY; |
| | | import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_ONLY; |
| | | import static org.forgerock.util.Options.defaultOptions; |
| | | |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.CreateRequest; |
| | | import org.forgerock.json.resource.DeleteRequest; |
| | | import org.forgerock.json.resource.NotFoundException; |
| | | import org.forgerock.json.resource.PatchRequest; |
| | | import org.forgerock.json.resource.QueryRequest; |
| | | import org.forgerock.json.resource.QueryResourceHandler; |
| | | import org.forgerock.json.resource.QueryResponse; |
| | | import org.forgerock.json.resource.ReadRequest; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.Requests; |
| | | import org.forgerock.json.resource.ResourceResponse; |
| | | import org.forgerock.json.resource.UpdateRequest; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | 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.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.requests.AddRequest; |
| | | import org.forgerock.opendj.ldap.requests.BindRequest; |
| | | import org.forgerock.opendj.ldap.requests.CompareRequest; |
| | | import org.forgerock.opendj.ldap.requests.ExtendedRequest; |
| | | import org.forgerock.opendj.ldap.requests.ModifyDNRequest; |
| | | import org.forgerock.opendj.ldap.requests.ModifyRequest; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.responses.BindResult; |
| | | import org.forgerock.opendj.ldap.responses.CompareResult; |
| | | import org.forgerock.opendj.ldap.responses.ExtendedResult; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldif.EntryReader; |
| | | import org.forgerock.opendj.ldif.LDIFEntryReader; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.RootContext; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.forgerock.util.query.QueryFilter; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | @SuppressWarnings({ "javadoc" }) |
| | | @Test |
| | | public final class Rest2LdapTest extends ForgeRockTestCase { |
| | | // TODO: unit test for DN template variables |
| | | // TODO: unit test for nested sub-resources |
| | | // TODO: unit test for singletons |
| | | // TODO: unit test for read-only |
| | | |
| | | private enum UseCase { |
| | | CLIENT_ID_PRIMARY_VIEW { |
| | | @Override |
| | | RequestHandler handler() { |
| | | return rest2Ldap(defaultOptions(), |
| | | top(), |
| | | userUidResource(), |
| | | resource("test") |
| | | .subResources(collectionOf("userUid").useClientDnNaming("uid") |
| | | .dnTemplate("dc=test") |
| | | .urlTemplate("users"))) |
| | | .newRequestHandlerFor("test"); |
| | | } |
| | | |
| | | @Override |
| | | CreateRequest createRequest() { |
| | | return newCreateRequest("users", "bjensen", userJson("bjensen", null, "bjensen", "created")); |
| | | } |
| | | |
| | | @Override |
| | | ResourceResponse createdResource() { |
| | | return newResourceResponse("bjensen", "1", userJson("bjensen", "1", "bjensen", "created")); |
| | | } |
| | | }, |
| | | CLIENT_ID_SECONDARY_VIEW { |
| | | @Override |
| | | RequestHandler handler() { |
| | | return rest2Ldap(defaultOptions(), |
| | | top(), |
| | | userUidResource(), |
| | | resource("test") |
| | | .subResources(collectionOf("userUid").useClientNaming("uid", "mail") |
| | | .dnTemplate("dc=test") |
| | | .urlTemplate("users"))) |
| | | .newRequestHandlerFor("test"); |
| | | } |
| | | |
| | | @Override |
| | | CreateRequest createRequest() { |
| | | return newCreateRequest("users", "bjensen@test.com", userJson("bjensen", null, "bjensen", "created")); |
| | | } |
| | | |
| | | @Override |
| | | ResourceResponse createdResource() { |
| | | return newResourceResponse("bjensen@test.com", "1", userJson("bjensen", "1", "bjensen", "created")); |
| | | } |
| | | }, |
| | | SERVER_ID_PRIMARY_VIEW { |
| | | @Override |
| | | RequestHandler handler() { |
| | | return rest2Ldap(defaultOptions(), |
| | | top(), |
| | | userEntryUuidResource(), |
| | | resource("test") |
| | | .subResources(collectionOf("userEntryUuid").useServerEntryUuidNaming("uid") |
| | | .dnTemplate("dc=test") |
| | | .urlTemplate("users"))) |
| | | .newRequestHandlerFor("test"); |
| | | } |
| | | |
| | | @Override |
| | | CreateRequest createRequest() { |
| | | return newCreateRequest("users", null, userJson(null, null, "bjensen", "created")); |
| | | } |
| | | |
| | | @Override |
| | | ResourceResponse createdResource() { |
| | | return newResourceResponse(ENTRY_UUID, "1", userJson(ENTRY_UUID, "1", "bjensen", "created")); |
| | | } |
| | | }, |
| | | SERVER_ID_SECONDARY_VIEW { |
| | | @Override |
| | | RequestHandler handler() { |
| | | return rest2Ldap(defaultOptions(), |
| | | top(), |
| | | userEntryUuidResource(), |
| | | resource("test") |
| | | .subResources(collectionOf("userEntryUuid").useClientNaming("uid", "mail") |
| | | .dnTemplate("dc=test") |
| | | .urlTemplate("users"))) |
| | | .newRequestHandlerFor("test"); |
| | | } |
| | | |
| | | @Override |
| | | CreateRequest createRequest() { |
| | | return newCreateRequest("users", "bjensen@test.com", userJson(null, null, "bjensen", "created")); |
| | | } |
| | | |
| | | @Override |
| | | ResourceResponse createdResource() { |
| | | return newResourceResponse("bjensen@test.com", "1", userJson(ENTRY_UUID, "1", "bjensen", "created")); |
| | | } |
| | | }; |
| | | |
| | | abstract ResourceResponse createdResource(); |
| | | |
| | | Context ctx() throws Exception { |
| | | final EntryReader ldif = new LDIFEntryReader("dn: dc=test", |
| | | "objectClass: top", |
| | | "objectClass: domain", |
| | | "dc: test"); |
| | | final Connection connection = newInternalConnection(updateMeta(new MemoryBackend(ldif))); |
| | | return new AuthenticatedConnectionContext(new RootContext(), connection); |
| | | } |
| | | |
| | | abstract RequestHandler handler(); |
| | | |
| | | abstract CreateRequest createRequest(); |
| | | } |
| | | |
| | | private static final String ENTRY_UUID = UUID.randomUUID().toString(); |
| | | private static final String USER_SCHEMA_URI = "fr:opendj:user:1.0"; |
| | | |
| | | // Field values may be null. |
| | | private static JsonValue userJson(final String id, final String rev, final String uid, final String description) { |
| | | return json(o(f("_id", id), |
| | | f("_rev", rev), |
| | | f("schema", USER_SCHEMA_URI), |
| | | f("uid", uid), |
| | | f("email", uid + "@test.com"), |
| | | f("name", o(f("displayName", uid + " displayName"), f("surname", uid + " surname"))), |
| | | f("description", array(description)))); |
| | | } |
| | | |
| | | private static Map.Entry<String, Object> f(final String k, final Object v) { |
| | | return v != null ? JsonValue.field(k, v) : null; |
| | | } |
| | | |
| | | private static Object o(Map.Entry<?, ?>... fields) { |
| | | return JsonValue.object(fields); |
| | | } |
| | | |
| | | private static Resource top() { |
| | | return resource("top").isAbstract(true) |
| | | .objectClass("top") |
| | | .property("_rev", simple("etag").isRequired(true).writability(READ_ONLY)); |
| | | } |
| | | |
| | | private static Resource userEntryUuidResource() { |
| | | return userResource("userEntryUuid", simple("entryUUID").writability(READ_ONLY)); |
| | | } |
| | | |
| | | private static Resource userUidResource() { |
| | | return userResource("userUid", simple("uid").isRequired(true).writability(CREATE_ONLY)); |
| | | } |
| | | |
| | | private static Resource userResource(final String resourceId, final PropertyMapper id) { |
| | | return resource(resourceId).superType("top") |
| | | .objectClasses("person", "organizationalPerson", "inetOrgPerson") |
| | | .property("schema", constant(USER_SCHEMA_URI)) |
| | | .property("_id", id) |
| | | .property("uid", simple("uid").isRequired(true).writability(CREATE_ONLY)) |
| | | .property("email", simple("mail")) |
| | | .property("name", object().property("displayName", simple("cn").isRequired(true)) |
| | | .property("surname", simple("sn").isRequired(true))) |
| | | .property("description", simple("description").isMultiValued(true)); |
| | | } |
| | | |
| | | @Test(dataProvider = "useCases") |
| | | public void canCreateResources(UseCase useCase) throws Exception { |
| | | // Given |
| | | RequestHandler handler = useCase.handler(); |
| | | Context ctx = useCase.ctx(); |
| | | |
| | | // When |
| | | ResourceResponse actual = handler.handleCreate(ctx, useCase.createRequest()).getOrThrowUninterruptibly(); |
| | | |
| | | // Then |
| | | assertThatExpectedResourceWasReturned(actual, useCase.createdResource()); |
| | | } |
| | | |
| | | @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources") |
| | | public void canReadResources(UseCase useCase) throws Exception { |
| | | // Given |
| | | RequestHandler handler = useCase.handler(); |
| | | Context ctx = useCase.ctx(); |
| | | CreateRequest createRequest = useCase.createRequest(); |
| | | ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // When |
| | | ReadRequest readRequest = newReadRequest(createRequest.getResourcePath(), resource.getId()); |
| | | ResourceResponse actual = handler.handleRead(ctx, readRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // Then |
| | | assertThatExpectedResourceWasReturned(actual, resource); |
| | | } |
| | | |
| | | @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources") |
| | | public void canUpdateResources(UseCase useCase) throws Exception { |
| | | // Given |
| | | RequestHandler handler = useCase.handler(); |
| | | Context ctx = useCase.ctx(); |
| | | CreateRequest createRequest = useCase.createRequest(); |
| | | ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // When |
| | | JsonValue newContent = resource.getContent().copy(); |
| | | newContent.put("description", array("updated")); |
| | | UpdateRequest updateRequest = newUpdateRequest(createRequest.getResourcePath(), resource.getId(), newContent); |
| | | |
| | | ResourceResponse actual = handler.handleUpdate(ctx, updateRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // Then |
| | | newContent.put("_rev", "2"); |
| | | ResourceResponse expected = newResourceResponse(resource.getId(), "2", newContent); |
| | | |
| | | assertThatExpectedResourceWasReturned(actual, expected); |
| | | |
| | | ReadRequest readRequest = newReadRequest(createRequest.getResourcePath(), resource.getId()); |
| | | ResourceResponse actual2 = handler.handleRead(ctx, readRequest).getOrThrowUninterruptibly(); |
| | | assertThatExpectedResourceWasReturned(actual2, expected); |
| | | } |
| | | |
| | | @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources") |
| | | public void canDeleteResources(UseCase useCase) throws Exception { |
| | | // Given |
| | | RequestHandler handler = useCase.handler(); |
| | | Context ctx = useCase.ctx(); |
| | | CreateRequest createRequest = useCase.createRequest(); |
| | | ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // When |
| | | DeleteRequest deleteRequest = Requests.newDeleteRequest(createRequest.getResourcePath(), resource.getId()); |
| | | ResourceResponse actual = handler.handleDelete(ctx, deleteRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // Then |
| | | assertThatExpectedResourceWasReturned(actual, resource); |
| | | |
| | | ReadRequest readRequest = newReadRequest(createRequest.getResourcePath(), resource.getId()); |
| | | try { |
| | | handler.handleRead(ctx, readRequest).getOrThrowUninterruptibly(); |
| | | fail("Deleted resource can still be read"); |
| | | } catch (NotFoundException e) { |
| | | // Expected. |
| | | } |
| | | } |
| | | |
| | | @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources") |
| | | public void canPatchResources(UseCase useCase) throws Exception { |
| | | // Given |
| | | RequestHandler handler = useCase.handler(); |
| | | Context ctx = useCase.ctx(); |
| | | CreateRequest createRequest = useCase.createRequest(); |
| | | ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // When |
| | | PatchRequest patchRequest = newPatchRequest(createRequest.getResourcePath(), |
| | | resource.getId(), |
| | | replace("description", array("patched"))); |
| | | ResourceResponse actual = handler.handlePatch(ctx, patchRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // Then |
| | | JsonValue newContent = resource.getContent().copy(); |
| | | newContent.put("description", array("patched")); |
| | | newContent.put("_rev", "2"); |
| | | ResourceResponse expected = newResourceResponse(resource.getId(), "2", newContent); |
| | | |
| | | assertThatExpectedResourceWasReturned(actual, expected); |
| | | |
| | | ReadRequest readRequest = newReadRequest(createRequest.getResourcePath(), resource.getId()); |
| | | ResourceResponse actual2 = handler.handleRead(ctx, readRequest).getOrThrowUninterruptibly(); |
| | | assertThatExpectedResourceWasReturned(actual2, expected); |
| | | } |
| | | |
| | | @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources") |
| | | public void canQueryResources(UseCase useCase) throws Exception { |
| | | // Given |
| | | RequestHandler handler = useCase.handler(); |
| | | Context ctx = useCase.ctx(); |
| | | CreateRequest createRequest = useCase.createRequest(); |
| | | ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly(); |
| | | |
| | | // When |
| | | QueryRequest queryRequest = newQueryRequest(createRequest.getResourcePath()); |
| | | queryRequest.setQueryFilter(QueryFilter.<JsonPointer>alwaysTrue()); |
| | | |
| | | final AtomicReference<ResourceResponse> actualResource = new AtomicReference<>(); |
| | | QueryResponse actualResponse = handler.handleQuery(ctx, queryRequest, new QueryResourceHandler() { |
| | | @Override |
| | | public boolean handleResource(final ResourceResponse resource) { |
| | | if (!actualResource.compareAndSet(null, resource)) { |
| | | fail("Too many resources returned during query"); |
| | | } |
| | | return true; |
| | | } |
| | | }).getOrThrowUninterruptibly(); |
| | | |
| | | // Then |
| | | assertThat(actualResponse).isNotNull(); |
| | | assertThatExpectedResourceWasReturned(actualResource.get(), resource); |
| | | } |
| | | |
| | | @DataProvider |
| | | Object[][] useCases() throws Exception { |
| | | UseCase[] values = UseCase.values(); |
| | | Object[][] data = new Object[values.length][]; |
| | | for (int i = 0; i < values.length; i++) { |
| | | data[i] = new Object[] { values[i] }; |
| | | } |
| | | return data; |
| | | } |
| | | |
| | | private void assertThatExpectedResourceWasReturned(final ResourceResponse actual, final ResourceResponse expected) { |
| | | assertThat(actual.getId()).isEqualTo(expected.getId()); |
| | | assertThat(actual.getRevision()).isEqualTo(expected.getRevision()); |
| | | assertThat(actual.getContent().asMap()).isEqualTo(expected.getContent().asMap()); |
| | | } |
| | | |
| | | private static org.forgerock.opendj.ldap.RequestHandler<RequestContext> updateMeta(final MemoryBackend delegate) { |
| | | return new org.forgerock.opendj.ldap.RequestHandler<RequestContext>() { |
| | | public void handleAdd(final RequestContext requestContext, final AddRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final LdapResultHandler<Result> resultHandler) { |
| | | request.addAttribute("entryUuid", ENTRY_UUID); |
| | | request.addAttribute("etag", 1); |
| | | delegate.handleAdd(requestContext, request, intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | public void handleBind(final RequestContext requestContext, final int version, final BindRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final LdapResultHandler<BindResult> resultHandler) { |
| | | delegate.handleBind(requestContext, version, request, intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | public void handleCompare(final RequestContext requestContext, final CompareRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final LdapResultHandler<CompareResult> resultHandler) { |
| | | delegate.handleCompare(requestContext, request, intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | public void handleDelete(final RequestContext requestContext, |
| | | final org.forgerock.opendj.ldap.requests.DeleteRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final LdapResultHandler<Result> resultHandler) { |
| | | delegate.handleDelete(requestContext, request, intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | public <R extends ExtendedResult> void handleExtendedRequest(final RequestContext requestContext, |
| | | final ExtendedRequest<R> request, |
| | | final IntermediateResponseHandler |
| | | intermediateResponseHandler, |
| | | final LdapResultHandler<R> resultHandler) { |
| | | delegate.handleExtendedRequest(requestContext, request, intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | public void handleModify(final RequestContext requestContext, final ModifyRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final LdapResultHandler<Result> resultHandler) { |
| | | incrementEtag(request.getName()); |
| | | delegate.handleModify(requestContext, request, intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | private void incrementEtag(final DN name) { |
| | | final Entry entry = delegate.get(name); |
| | | if (entry != null) { |
| | | final int etag = entry.parseAttribute("etag").asInteger(1); |
| | | entry.replaceAttribute("etag", etag + 1); |
| | | } |
| | | } |
| | | |
| | | public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final LdapResultHandler<Result> resultHandler) { |
| | | incrementEtag(request.getName()); |
| | | delegate.handleModifyDN(requestContext, request, intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | public void handleSearch(final RequestContext requestContext, final SearchRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final SearchResultHandler entryHandler, |
| | | final LdapResultHandler<Result> resultHandler) { |
| | | delegate.handleSearch(requestContext, |
| | | request, |
| | | intermediateResponseHandler, |
| | | entryHandler, |
| | | resultHandler); |
| | | } |
| | | }; |
| | | } |
| | | } |
| | |
| | | ds-cfg-enabled: true |
| | | ds-cfg-java-class: org.opends.server.protocols.http.rest2ldap.Rest2LdapEndpoint |
| | | ds-cfg-base-path: /api |
| | | ds-cfg-config-url: config/http-config.json |
| | | ds-cfg-config-directory: config/rest2ldap/endpoints/api |
| | | ds-cfg-http-authorization-mechanism: cn=HTTP Basic,cn=HTTP Authorization Mechanisms,cn=config |
| | | |
| | | dn: cn=HTTP Authorization Mechanisms,cn=config |
| New file |
| | |
| | | { |
| | | // This file defines an example Rest2Ldap API mapping exposing a multi-tenant deployment exposing users, |
| | | // POSIX users, and groups, as follows: |
| | | // |
| | | // /api/{tenant}/users/{uid} - users for a given tenant, e.g. "/api/example/users/bjensen" |
| | | // /api/{tenant}/groups/{cn} - groups for a given tenant, e.g. "/api/example/groups/administrators" |
| | | // |
| | | "version": "1.0", |
| | | |
| | | // This section defines all of the resources, their inheritance, and relationships. |
| | | "resourceTypes": { |
| | | // This resource represents the entry point into the user/group API. It only defines sub-resources and |
| | | // does not have any properties itself. The URL and DN templates include a template variable allowing |
| | | // this API to support multi-tenancy. Multiple template variables are permitted. |
| | | "users-and-groups-v1": { |
| | | "subResources": { |
| | | "{tenant}/users": { |
| | | "type": "collection", |
| | | "dnTemplate": "ou=people,dc={tenant},dc=com", |
| | | "resource": "frapi:opendj:rest2ldap:user:1.0", |
| | | "namingStrategy": { |
| | | "type": "clientDnNaming", |
| | | "dnAttribute": "uid" |
| | | } |
| | | }, |
| | | "{tenant}/groups": { |
| | | "type": "collection", |
| | | "dnTemplate": "ou=groups,dc={tenant},dc=com", |
| | | "resource": "frapi:opendj:rest2ldap:group:1.0", |
| | | "namingStrategy": { |
| | | "type": "clientDNNaming", |
| | | "dnAttribute": "cn" |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | // This resource will act as the common parent of all resources that have a JSON representation. |
| | | "frapi:opendj:rest2ldap:object:1.0": { |
| | | "isAbstract": true, |
| | | "objectClasses": [ "top" ], |
| | | // This property will store type information in a resource's JSON representation. It is the |
| | | // equivalent of the "objectClass" attribute, except that it is single valued and will contain |
| | | // the resource name, e.g. "frapi:opendj:rest2ldap:user:1.0" or "frapi:opendj:rest2ldap:group:1.0". |
| | | "resourceTypeProperty": "_schema", |
| | | "properties": { |
| | | // Resource type property mappers store the resource's type and don't have any configuration. |
| | | "_schema": { |
| | | "type": "resourceType" |
| | | }, |
| | | "_rev": { |
| | | "type": "simple", |
| | | "ldapAttribute": "etag", |
| | | "writability": "readOnly" |
| | | }, |
| | | "_meta": { |
| | | "type": "object", |
| | | "properties": { |
| | | "created": { |
| | | "type": "simple", |
| | | "ldapAttribute": "createTimestamp", |
| | | "writability": "readOnly" |
| | | }, |
| | | "lastModified": { |
| | | "type": "simple", |
| | | "ldapAttribute": "modifyTimestamp", |
| | | "writability": "readOnly" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | // A "user" resource includes property mapping for the inetOrgPerson LDAP object class and is identified by |
| | | // the "uid" LDAP attribute. Users have a single sub-type representing users with POSIX account information. |
| | | "frapi:opendj:rest2ldap:user:1.0": { |
| | | "superType": "frapi:opendj:rest2ldap:object:1.0", |
| | | "objectClasses": [ "person", "organizationalPerson", "inetOrgPerson" ], |
| | | "supportedActions": [ "passwordModify" ], |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "uid", |
| | | "isRequired": true, |
| | | "writability": "createOnly" |
| | | }, |
| | | "userName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "mail" |
| | | }, |
| | | "displayName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "isMultiValued": true, |
| | | "isRequired": true |
| | | }, |
| | | "name": { |
| | | "type": "object", |
| | | "properties": { |
| | | "givenName": { |
| | | "type": "simple" |
| | | }, |
| | | "familyName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "sn", |
| | | "isRequired": true |
| | | } |
| | | } |
| | | }, |
| | | "description": { |
| | | "type": "simple" |
| | | }, |
| | | "manager": { |
| | | "type": "reference", |
| | | "ldapAttribute": "manager", |
| | | "baseDn": "ou=people,dc=example,dc=com", |
| | | "primaryKey": "uid", |
| | | "mapper": { |
| | | "type": "object", |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "uid", |
| | | "isRequired": true |
| | | }, |
| | | "displayName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "writability": "readOnlyDiscardWrites" |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "groups": { |
| | | "type": "reference", |
| | | "ldapAttribute": "isMemberOf", |
| | | "baseDn": "ou=groups,dc=example,dc=com", |
| | | "isMultiValued": true, |
| | | "writability": "readOnly", |
| | | "primaryKey": "cn", |
| | | "mapper": { |
| | | "type": "object", |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn" |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | "contactInformation": { |
| | | "type": "object", |
| | | "properties": { |
| | | "telephoneNumber": { |
| | | "type": "simple" |
| | | }, |
| | | "emailAddress": { |
| | | "type": "simple", |
| | | "ldapAttribute": "mail" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | // A user with POSIX account information. |
| | | "frapi:opendj:rest2ldap:posixUser:1.0": { |
| | | "superType": "frapi:opendj:rest2ldap:user:1.0", |
| | | "objectClasses": [ "posixAccount" ], |
| | | "properties": { |
| | | "uidNumber": { |
| | | "type": "simple", |
| | | "isRequired": true |
| | | }, |
| | | "gidNumber": { |
| | | "type": "simple", |
| | | "isRequired": true |
| | | }, |
| | | "homeDirectory": { |
| | | "type": "simple", |
| | | "isRequired": true |
| | | }, |
| | | "loginShell": { |
| | | "type": "simple" |
| | | }, |
| | | "gecos": { |
| | | "type": "simple" |
| | | } |
| | | } |
| | | }, |
| | | // A "group" resource includes property mapping for the inetOrgPerson LDAP object class and is identified by |
| | | // the "uid" LDAP attribute. Users have a single sub-type representing users with POSIX account information. |
| | | "frapi:opendj:rest2ldap:group:1.0": { |
| | | "superType": "frapi:opendj:rest2ldap:object:1.0", |
| | | "objectClasses": [ "groupOfUniqueNames" ], |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "isRequired": true, |
| | | "writability": "createOnly" |
| | | }, |
| | | "displayName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "isRequired": true, |
| | | "writability": "readOnly" |
| | | }, |
| | | "description": { |
| | | "type": "simple" |
| | | }, |
| | | "members": { |
| | | "type": "reference", |
| | | "ldapAttribute": "uniqueMember", |
| | | "baseDn": "dc=example,dc=com", |
| | | "primaryKey": "uid", |
| | | "isMultiValued": true, |
| | | "mapper": { |
| | | "type": "object", |
| | | "properties": { |
| | | "_id": { |
| | | "type": "simple", |
| | | "ldapAttribute": "uid", |
| | | "isRequired": true |
| | | }, |
| | | "displayName": { |
| | | "type": "simple", |
| | | "ldapAttribute": "cn", |
| | | "writability": "readOnlyDiscardWrites" |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | SINGLE-VALUE |
| | | X-ORIGIN 'OpenDJ Directory Server' ) |
| | | attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.161 |
| | | NAME 'ds-cfg-config-url' |
| | | NAME 'ds-cfg-config-directory' |
| | | EQUALITY caseIgnoreMatch |
| | | SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 |
| | | SINGLE-VALUE |
| | |
| | | NAME 'ds-cfg-rest2ldap-endpoint' |
| | | SUP ds-cfg-http-endpoint |
| | | STRUCTURAL |
| | | MUST ( ds-cfg-config-url $ ds-cfg-resource ) |
| | | MUST ( ds-cfg-config-directory ) |
| | | X-ORIGIN 'OpenDJ Directory Server' ) |
| | | objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.36 |
| | | NAME 'ds-cfg-http-authorization-mechanism' |
| | |
| | | Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | information: "Portions Copyright [year] [name of copyright owner]". |
| | | |
| | | Copyright 2015 ForgeRock AS. |
| | | Copyright 2015-2016 ForgeRock AS. |
| | | --> |
| | | <!-- OpenDJ final archive content descriptor --> |
| | | <component> |
| | |
| | | <fileSet> |
| | | <directory>${basedir}/resource/config</directory> |
| | | <outputDirectory>template/config</outputDirectory> |
| | | <includes> |
| | | <include>*.*</include> |
| | | </includes> |
| | | <excludes> |
| | | <exclude>config.ldif</exclude> |
| | | </excludes> |
| | |
| | | */ |
| | | package org.opends.server.protocols.http.rest2ldap; |
| | | |
| | | import static org.forgerock.http.util.Json.readJsonLenient; |
| | | import static org.opends.messages.ConfigMessages.*; |
| | | import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LdapJsonConfigurator.configureEndpoint; |
| | | import static org.forgerock.util.Options.defaultOptions; |
| | | import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_INVALID; |
| | | import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_UNABLE_READ; |
| | | import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_UNEXPECTED_JSON; |
| | | import static org.opends.server.util.StaticUtils.getFileForPath; |
| | | import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; |
| | | |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.net.MalformedURLException; |
| | | import java.net.URI; |
| | | import java.net.URISyntaxException; |
| | | import java.net.URL; |
| | | |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.HttpApplication; |
| | | import org.forgerock.http.HttpApplicationException; |
| | | import org.forgerock.http.io.Buffer; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.JsonValueException; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.json.resource.http.CrestHttp; |
| | | import org.forgerock.opendj.rest2ldap.Rest2Ldap; |
| | | import org.forgerock.opendj.server.config.server.Rest2ldapEndpointCfg; |
| | | import org.forgerock.util.Factory; |
| | | import org.opends.server.api.HttpEndpoint; |
| | |
| | | |
| | | /** |
| | | * Encapsulates configuration required to start a REST2LDAP application embedded |
| | | * in this LDAP server. Acts as a factory for {@link Rest2LDAPHttpApplication}. |
| | | * in this LDAP server. Acts as a factory for {@link HttpApplication}. |
| | | */ |
| | | public final class Rest2LdapEndpoint extends HttpEndpoint<Rest2ldapEndpointCfg> |
| | | { |
| | | |
| | | /** |
| | | * Create a new Rest2LdapEnpoint with the supplied configuration. |
| | | * Create a new Rest2LdapEndpoint with the supplied configuration. |
| | | * |
| | | * @param configuration |
| | | * Configuration to use for the {@link HttpApplication} |
| | |
| | | } |
| | | |
| | | /** |
| | | * Specialized {@link Rest2LDAPHttpApplication} using internal connections to |
| | | * this local LDAP server. |
| | | * Specialized {@link HttpApplication} using internal connections to this local LDAP server. |
| | | */ |
| | | private final class InternalRest2LDAPHttpApplication implements HttpApplication |
| | | { |
| | | private final URL configURL; |
| | | |
| | | InternalRest2LDAPHttpApplication() throws InitializationException |
| | | { |
| | | try |
| | | { |
| | | final URI configURI = new URI(configuration.getConfigUrl()); |
| | | configURL = configURI.isAbsolute() |
| | | ? configURI.toURL() |
| | | : getFileForPath(configuration.getConfigUrl()).toURI().toURL(); |
| | | } |
| | | catch (MalformedURLException | URISyntaxException e) |
| | | { |
| | | throw new InitializationException( |
| | | ERR_CONFIG_REST2LDAP_MALFORMED_URL.get(configuration.dn(), stackTraceToSingleLineString(e))); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public Handler start() throws HttpApplicationException |
| | | { |
| | | JsonValue mappingConfiguration; |
| | | final File endpointConfig = getFileForPath(configuration.getConfigDirectory(), serverContext); |
| | | try |
| | | { |
| | | mappingConfiguration = readJson(configURL); |
| | | return newHttpHandler(configureEndpoint(endpointConfig, defaultOptions())); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | throw new LocalizedHttpApplicationException( |
| | | ERR_CONFIG_REST2LDAP_UNABLE_READ.get(configURL, configuration.dn(), stackTraceToSingleLineString(e)), e); |
| | | } |
| | | final JsonValue mappings = mappingConfiguration.get("mappings").required(); |
| | | final Router router = new Router(); |
| | | try |
| | | { |
| | | for (final String mappingUrl : mappings.keys()) |
| | | { |
| | | final JsonValue mapping = mappings.get(mappingUrl); |
| | | router.addRoute(Router.uriTemplate(mappingUrl), Rest2Ldap.builder().configureMapping(mapping).build()); |
| | | } |
| | | throw new LocalizedHttpApplicationException(ERR_CONFIG_REST2LDAP_UNABLE_READ.get( |
| | | endpointConfig, configuration.dn(), stackTraceToSingleLineString(e)), e); |
| | | } |
| | | catch (JsonValueException e) |
| | | { |
| | | throw new LocalizedHttpApplicationException( |
| | | ERR_CONFIG_REST2LDAP_UNEXPECTED_JSON.get(e.getJsonValue().getPointer(), configURL, configuration.dn(), |
| | | stackTraceToSingleLineString(e)), e); |
| | | throw new LocalizedHttpApplicationException(ERR_CONFIG_REST2LDAP_UNEXPECTED_JSON.get( |
| | | e.getJsonValue().getPointer(), endpointConfig, configuration.dn(), stackTraceToSingleLineString(e)), e); |
| | | } |
| | | catch (IllegalArgumentException e) |
| | | { |
| | | throw new LocalizedHttpApplicationException( |
| | | ERR_CONFIG_REST2LDAP_INVALID.get(configURL, configuration.dn(), stackTraceToSingleLineString(e)), e); |
| | | } |
| | | return CrestHttp.newHttpHandler(router); |
| | | } |
| | | |
| | | private JsonValue readJson(final URL resource) throws IOException |
| | | { |
| | | try (InputStream in = resource.openStream()) |
| | | { |
| | | return new JsonValue(readJsonLenient(in)); |
| | | throw new LocalizedHttpApplicationException(ERR_CONFIG_REST2LDAP_INVALID.get( |
| | | endpointConfig, configuration.dn(), stackTraceToSingleLineString(e)), e); |
| | | } |
| | | } |
| | | |