| | |
| | | // Servlet and authentication filter. |
| | | "ldapConnectionFactories" : { |
| | | // Unauthenticated connections used for performing bind requests. |
| | | "default" : { |
| | | "bind" : { |
| | | // Indicates whether or not LDAP connections should be secured using |
| | | // SSL or StartTLS. Acceptable values are: |
| | | // |
| | |
| | | // authentication and proxied operations (if enabled). This factory |
| | | // will re-use the server "default" configuration. |
| | | "root" : { |
| | | "inheritFrom" : "default", |
| | | "inheritFrom" : "bind", |
| | | |
| | | // Defines how authentication should be performed. Only "simple" |
| | | // authentication is supported at the moment. |
| | |
| | | } |
| | | }, |
| | | |
| | | // The Rest2LDAP authentication filter configuration. The filter will be |
| | | // disabled if the configuration is not present. Upon successful |
| | | // authentication the filter will create a security context containing the |
| | | // following principals: |
| | | // |
| | | // "dn" - the DN of the user if known (may not be the case for sasl-plain) |
| | | // "id" - the username used for authentication. |
| | | "authenticationFilter" : { |
| | | // Indicates whether the filter should allow HTTP BASIC authentication. |
| | | "supportHTTPBasicAuthentication" : true, |
| | | "authorization": { |
| | | // The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2". |
| | | "policies": [ "basic" ], |
| | | |
| | | // Indicates whether the filter should allow alternative authentication |
| | | // and, if so, which HTTP headers it should obtain the username and |
| | | // password from. |
| | | "supportAltAuthentication" : true, |
| | | "altAuthenticationUsernameHeader" : "X-OpenIDM-Username", |
| | | "altAuthenticationPasswordHeader" : "X-OpenIDM-Password", |
| | | // Perform all operations using a pre-authorization connection. |
| | | "anonymous": { |
| | | // Specify the connection factory to use to perform LDAP operations. |
| | | // If missing, "root" factory will be used. |
| | | "ldapConnectionFactory": "root", |
| | | |
| | | // Enable proxied authorization using the specified user DN. |
| | | // If empty, anonymous proxied authorization will be used. |
| | | // If missing, connection from the ldapConnectionFactory will be used as-is. |
| | | "userDN": "" |
| | | }, |
| | | |
| | | // Indicates whether the authenticated LDAP connection should be cached |
| | | // for use within the Rest2LDAP Servlet for subsequent LDAP operations. |
| | | // If this is set to true then the Servlet will not need its own LDAP |
| | | // connection factory and will also not need to use proxied |
| | | // authorization. |
| | | "reuseAuthenticatedConnection" : true, |
| | | // Use HTTP Basic authentication's information to bind to the LDAP server. |
| | | "basic": { |
| | | // Indicates whether the filter should allow alternative authentication |
| | | // and, if so, which HTTP headers it should obtain the username and |
| | | // password from. |
| | | "supportAltAuthentication" : true, |
| | | "altAuthenticationUsernameHeader" : "X-OpenIDM-Username", |
| | | "altAuthenticationPasswordHeader" : "X-OpenIDM-Password", |
| | | |
| | | // Specifies how LDAP authentications should be performed. The method |
| | | // must be one of: |
| | | // |
| | | // "simple" - the username is an LDAP DN |
| | | // "sasl-plain" - the username is an authzid which will be |
| | | // substituted into the "saslAuthzIdTemplate" using |
| | | // %s substitution |
| | | // "search-simple" - the user's DN will be resolved by performing an |
| | | // LDAP search using a filter constructed by |
| | | // substituting the username into the |
| | | // "searchFilterTemplate" using %s substitution. |
| | | "method" : "search-simple", |
| | | // For server which are not supporting proxied-authorization control, you can |
| | | // set this flag to true. Subsequent LDAP operations will then be performed |
| | | // by re-using the authenticated connection. |
| | | // If missing, proxied-authorization control will be used. |
| | | "reuseAuthenticatedConnection": false, |
| | | |
| | | // The connection factory which will be exclusively used for |
| | | // authenticating users using LDAP bind operations. |
| | | "bindLDAPConnectionFactory" : "default", |
| | | // Define which LDAP bind mechanism to use |
| | | // Supported mechanisms are "simple", "sasl-plain", "search" |
| | | "bind": "search", |
| | | |
| | | // The SASL AuthzID template which will be used for "sasl-plain" |
| | | // authentication. The %s format parameters will be substituted with |
| | | // the client-provided username, using DN character escaping for DN |
| | | // AuthzIDs. |
| | | "saslAuthzIdTemplate" : "dn:uid=%s,ou=people,dc=example,dc=com", |
| | | // Bind to the LDAP server using the DN built from the HTTP Basic's username |
| | | "simple": { |
| | | // Connection factory used to perform the bind operation. |
| | | // If missing, "bind" factory will be used. |
| | | "ldapConnectionFactory": "bind", |
| | | |
| | | // The connection factory which will be used for performing LDAP |
| | | // searches to locate users when "search-simple" authentication is |
| | | // enabled. |
| | | "searchLDAPConnectionFactory" : "root", |
| | | // The Bind DN Template containing a single %s which will be replaced by the authenticating |
| | | // user's name. (i.e: uid=%s,ou=People,dc=example,dc=com) |
| | | // If missing, "%s" is used. |
| | | "bindDNTemplate": "uid=%s,ou=People,dc=example,dc=com" |
| | | }, |
| | | |
| | | // The search parameters to use for "search-simple" authentication. The |
| | | // %s filter format parameters will be substituted with the |
| | | // client-provided username, using LDAP filter string character escaping. |
| | | "searchBaseDN" : "ou=people,dc=example,dc=com", |
| | | "searchScope" : "sub", // Or "one". |
| | | "searchFilterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))" |
| | | // Bind to the LDAP server using a SASL Plain request |
| | | "sasl-plain": { |
| | | // Connection factory used to perform the bind operation. |
| | | // If missing, "bind" factory will be used. |
| | | "ldapConnectionFactory": "bind", |
| | | |
| | | // TODO: support for HTTP sessions? |
| | | // Authentication identity template containing a single %s which will be replaced by the authenticating |
| | | // user's name. (i.e: u:%s) |
| | | "authcIdTemplate": "u:%s" |
| | | }, |
| | | |
| | | // Bind to the LDAP server using the resulting DN of a search request. |
| | | "search": { |
| | | // Connection factory used to perform the search operation. |
| | | // If missing, "root" factory will be used. |
| | | "searchLDAPConnectionFactory": "root", |
| | | |
| | | // Connection factory used to perform the bind operation. |
| | | // If missing, "bind" factory will be used. |
| | | "bindLDAPConnectionFactory": "bind", |
| | | |
| | | // The %s filter format parameters will be substituted with the client-provided username, |
| | | // using LDAP filter string character escaping. |
| | | "baseDN" : "ou=people,dc=example,dc=com", |
| | | "scope" : "sub", // Or "one". |
| | | "filterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))" |
| | | } |
| | | // TODO: support for HTTP sessions? |
| | | } |
| | | }, |
| | | |
| | | // The Rest2LDAP Servlet configuration. |
| | | "servlet" : { |
| | | // The connection factory which will be used for performing LDAP |
| | | // operations. Pre-authenticated connections passed through from the |
| | | // authentication filter (see "reuseAuthenticatedConnection") will be |
| | | // used in preference to this factory. Specifically, a connection |
| | | // factory does not need to be configured if a connection will always |
| | | // be passed on from the filter, which may not always be the case |
| | | // if the filter is configured to use HTTP sessions. |
| | | "ldapConnectionFactory" : "root", |
| | | |
| | | // Specifies how LDAP authorization should be performed. The method |
| | | // must be one of: |
| | | // |
| | | // "none" - use connections acquired from the LDAP connection |
| | | // factory. Don't use proxied authorization, and don't |
| | | // use cached pre-authenticated connections, |
| | | // "reuse" - use the connection obtained during LDAP |
| | | // authentication. If no connection was passed through |
| | | // the authorization will fail, |
| | | // "proxy" - use proxied authorization with an authorization ID |
| | | // derived from the "proxyAuthzIdTemplate". Proxied |
| | | // authorization will only be used if there is no |
| | | // pre-authenticated connection available. |
| | | "authorizationPolicy" : "proxy", |
| | | |
| | | // The AuthzID template which will be used for proxied authorization. |
| | | // The template should contain fields which are expected to be found in |
| | | // the security context create during authentication, e.g. "dn" and "id". |
| | | "proxyAuthzIdTemplate" : "dn:{dn}", |
| | | |
| | | // 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" } } |
| | | } } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | // 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" } } |
| | | } } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | <groupId>org.forgerock.opendj</groupId> |
| | | <version>4.0.0-SNAPSHOT</version> |
| | | </parent> |
| | | |
| | | |
| | | <artifactId>opendj-rest2ldap</artifactId> |
| | | <name>OpenDJ Commons REST Adapter</name> |
| | | <description>This module includes APIs for accessing LDAP repositories using commons REST.</description> |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013-2015 ForgeRock AS. |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.LinkedAttribute; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | |
| | | |
| | | @Override |
| | | Promise<List<Attribute>, ResourceException> create( |
| | | final RequestState requestState, final JsonPointer path, final JsonValue v) { |
| | | return getNewLDAPAttributes(requestState, path, v).then( |
| | | final Connection connection, final JsonPointer path, final JsonValue v) { |
| | | return getNewLDAPAttributes(connection, path, v).then( |
| | | new Function<Attribute, List<Attribute>, ResourceException>() { |
| | | @Override |
| | | public List<Attribute> apply(Attribute newLDAPAttribute) throws ResourceException { |
| | |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final RequestState requestState, final JsonPointer path, |
| | | void getLDAPAttributes(final Connection connection, final JsonPointer path, |
| | | final JsonPointer subPath, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(ldapAttributeName.toString()); |
| | | } |
| | | |
| | | abstract Promise<Attribute, ResourceException> getNewLDAPAttributes( |
| | | RequestState requestState, JsonPointer path, List<Object> newValues); |
| | | abstract Promise<Attribute, ResourceException> getNewLDAPAttributes(Connection connection, JsonPointer path, |
| | | List<Object> newValues); |
| | | |
| | | abstract T getThis(); |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> patch( |
| | | final RequestState requestState, final JsonPointer path, final PatchOperation operation) { |
| | | final Connection connection, final JsonPointer path, final PatchOperation operation) { |
| | | try { |
| | | final JsonPointer field = operation.getField(); |
| | | final JsonValue v = operation.getValue(); |
| | |
| | | singletonList(new Modification(modType, emptyAttribute(ldapAttributeName)))); |
| | | } |
| | | } else { |
| | | return getNewLDAPAttributes(requestState, path, newValues) |
| | | return getNewLDAPAttributes(connection, path, newValues) |
| | | .then(new Function<Attribute, List<Modification>, ResourceException>() { |
| | | @Override |
| | | public List<Modification> apply(final Attribute value) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> update( |
| | | final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | return getNewLDAPAttributes(requestState, path, v).then( |
| | | Promise<List<Modification>, ResourceException> update(final Connection connection, final JsonPointer path, |
| | | final Entry e, final JsonValue v) { |
| | | return getNewLDAPAttributes(connection, 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 RequestState requestState, final JsonPointer path, final JsonValue v) { |
| | | private Promise<Attribute, ResourceException> getNewLDAPAttributes(final Connection connection, |
| | | final JsonPointer path, final JsonValue v) { |
| | | try { |
| | | // Ensure that the value is of the correct type. |
| | | checkSchema(path, v); |
| | |
| | | // Skip sub-class implementation if there are no values. |
| | | return Promises.newResultPromise(emptyAttribute(ldapAttributeName)); |
| | | } else { |
| | | return getNewLDAPAttributes(requestState, path, newValues); |
| | | return getNewLDAPAttributes(connection, path, newValues); |
| | | } |
| | | } catch (final Exception ex) { |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | * Copyright 2012-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | |
| | | * action in this case, perhaps by substituting default LDAP values, or by |
| | | * returning a failed promise with an appropriate {@link ResourceException}. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute 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( |
| | | RequestState requestState, JsonPointer path, JsonValue v); |
| | | abstract Promise<List<Attribute>, ResourceException> create(Connection connection, JsonPointer path, JsonValue v); |
| | | |
| | | /** |
| | | * Adds the names of the LDAP attributes required by this attribute mapper |
| | |
| | | * Implementations should only add the names of attributes found in the LDAP |
| | | * entry directly associated with the resource. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute mapper. This may be used when constructing error |
| | |
| | | * The set into which the required LDAP attribute names should be |
| | | * put. |
| | | */ |
| | | abstract void getLDAPAttributes( |
| | | RequestState requestState, JsonPointer path, JsonPointer subPath, Set<String> ldapAttributes); |
| | | abstract void getLDAPAttributes(Connection connection, JsonPointer path, JsonPointer subPath, |
| | | Set<String> ldapAttributes); |
| | | |
| | | /** |
| | | * Transforms the provided REST comparison filter parameters to an LDAP |
| | |
| | | * promise must be returned with an appropriate {@link ResourceException} |
| | | * indicating the problem which occurred. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute 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(RequestState requestState, JsonPointer path, |
| | | abstract Promise<Filter, ResourceException> getLDAPFilter(Connection connection, JsonPointer path, |
| | | JsonPointer subPath, FilterType type, String operator, Object valueAssertion); |
| | | |
| | | /** |
| | |
| | | * a promise once the transformation has completed. This method is invoked |
| | | * when a REST resource is modified using a patch request. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute mapper. This may be used when constructing error |
| | |
| | | * @return A {@link Promise} containing the result of the operation. |
| | | */ |
| | | abstract Promise<List<Modification>, ResourceException> patch( |
| | | RequestState requestState, JsonPointer path, PatchOperation operation); |
| | | Connection connection, JsonPointer path, PatchOperation operation); |
| | | |
| | | /** |
| | | * Maps one or more LDAP attributes to their JSON representation, returning |
| | |
| | | * they contain unexpected content, then a failed promise must be returned |
| | | * with an appropriate exception indicating the problem which occurred. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param path |
| | | * The pointer from the root of the JSON resource to this |
| | | * attribute 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(RequestState requestState, JsonPointer path, Entry e); |
| | | abstract Promise<JsonValue, ResourceException> read(Connection connection, JsonPointer path, Entry e); |
| | | |
| | | /** |
| | | * Maps a JSON value to one or more LDAP modifications, returning a promise |
| | |
| | | * action in this case, perhaps by substituting default LDAP values, or by |
| | | * returning a failed promise with an appropriate {@link ResourceException}. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @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( |
| | | RequestState requestState, JsonPointer path, Entry e, JsonValue v); |
| | | abstract Promise<List<Modification>, ResourceException> update(Connection connection, JsonPointer path, Entry e, |
| | | JsonValue v); |
| | | |
| | | // TODO: methods for obtaining schema information (e.g. name, description, type information). |
| | | // TODO: methods for creating sort controls. |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013-2015 ForgeRock AS. |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | * @return The cached pre-authenticated LDAP connection which should be |
| | | * re-used for subsequent LDAP operations. |
| | | */ |
| | | Connection getConnection() { |
| | | public Connection getConnection() { |
| | | return connection; |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013 ForgeRock AS. |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DecodeOptions; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | |
| | |
| | | * Common configuration options. |
| | | */ |
| | | final class Config { |
| | | private final AuthorizationPolicy authzPolicy; |
| | | private final ConnectionFactory factory; |
| | | private final DecodeOptions options; |
| | | private final AuthzIdTemplate proxiedAuthzTemplate; |
| | | private final ReadOnUpdatePolicy readOnUpdatePolicy; |
| | | private final Schema schema; |
| | | private final boolean useSubtreeDelete; |
| | | private final boolean usePermissiveModify; |
| | | |
| | | Config(final ConnectionFactory factory, final ReadOnUpdatePolicy readOnUpdatePolicy, |
| | | final AuthorizationPolicy authzPolicy, final AuthzIdTemplate proxiedAuthzTemplate, |
| | | final boolean useSubtreeDelete, final boolean usePermissiveModify, final Schema schema) { |
| | | this.factory = factory; |
| | | Config(final ReadOnUpdatePolicy readOnUpdatePolicy, final boolean useSubtreeDelete, |
| | | final boolean usePermissiveModify, final Schema schema) { |
| | | this.readOnUpdatePolicy = readOnUpdatePolicy; |
| | | this.authzPolicy = authzPolicy; |
| | | this.proxiedAuthzTemplate = proxiedAuthzTemplate; |
| | | this.useSubtreeDelete = useSubtreeDelete; |
| | | this.usePermissiveModify = usePermissiveModify; |
| | | this.schema = schema; |
| | | this.options = new DecodeOptions().setSchema(schema); |
| | | } |
| | | |
| | | /** |
| | | * Returns the LDAP SDK connection factory which should be used when |
| | | * performing LDAP operations. |
| | | * |
| | | * @return The LDAP SDK connection factory which should be used when |
| | | * performing LDAP operations. |
| | | */ |
| | | ConnectionFactory connectionFactory() { |
| | | return factory; |
| | | } |
| | | |
| | | /** |
| | | * Returns the decoding options which should be used when decoding controls |
| | | * in responses. |
| | | * |
| | |
| | | } |
| | | |
| | | /** |
| | | * Returns the authorization policy which should be used for performing LDAP |
| | | * operations. |
| | | * |
| | | * @return The authorization policy which should be used for performing LDAP |
| | | * operations. |
| | | */ |
| | | AuthorizationPolicy getAuthorizationPolicy() { |
| | | return authzPolicy; |
| | | } |
| | | |
| | | /** |
| | | * Returns the authorization ID template which should be used when proxied |
| | | * authorization is enabled. |
| | | * |
| | | * @return The authorization ID template which should be used when proxied |
| | | * authorization is enabled. |
| | | */ |
| | | AuthzIdTemplate getProxiedAuthorizationTemplate() { |
| | | return proxiedAuthzTemplate; |
| | | } |
| | | |
| | | /** |
| | | * Returns {@code true} if modify requests should include the permissive |
| | | * modify control. |
| | | * |
| | |
| | | ReadOnUpdatePolicy readOnUpdatePolicy() { |
| | | return readOnUpdatePolicy; |
| | | } |
| | | |
| | | /** |
| | | * Returns the schema which should be used when attribute types and |
| | | * controls. |
| | | * |
| | | * @return The schema which should be used when attribute types and |
| | | * controls. |
| | | */ |
| | | Schema schema() { |
| | | return schema; |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | * Copyright 2012-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Attribute>, ResourceException> create( |
| | | final RequestState requestState, final JsonPointer path, final JsonValue v) { |
| | | Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path, |
| | | final JsonValue v) { |
| | | if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) { |
| | | return Promises.<List<Attribute>, ResourceException> newExceptionPromise(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to create the read-only field '%s'", path))); |
| | |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath, |
| | | void getLDAPAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | // Nothing to do. |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path, |
| | | Promise<Filter, ResourceException> getLDAPFilter(final Connection connection, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) { |
| | | final Filter filter; |
| | | final JsonValue subValue = value.get(subPath); |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> patch(final RequestState requestState, final JsonPointer path, |
| | | Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path, |
| | | final PatchOperation operation) { |
| | | return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to patch the read-only field '%s'", path))); |
| | | } |
| | | |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) { |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) { |
| | | return Promises.newResultPromise(value.copy()); |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> update( |
| | | final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | final Connection connection, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) { |
| | | return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to modify the read-only field '%s'", path))); |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | * Copyright 2012-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.json.JsonPointer; |
| | | import org.forgerock.json.JsonValue; |
| | |
| | | ResourceException.newResourceException(ResourceException.BAD_REQUEST, e.getLocalizedMessage(), e)); |
| | | } |
| | | |
| | | final RequestState requestState = wrap(context); |
| | | return requestState.getConnection() |
| | | .thenAsync(new AsyncFunction<Connection, ActionResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ActionResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | List<JsonPointer> attrs = Collections.emptyList(); |
| | | return connection.searchSingleEntryAsync(searchRequest(requestState, 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(); |
| | | } |
| | | }, ldapExceptionToResourceException()); |
| | | } |
| | | }, ldapExceptionToResourceException()); |
| | | } |
| | | |
| | | private AsyncFunction<LdapException, ActionResponse, ResourceException> |
| | | ldapExceptionToResourceException() { |
| | | return ldapToResourceException(); |
| | | } |
| | | }).thenFinally(close(requestState)); |
| | | 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()); |
| | | } |
| | | |
| | | private byte[] asBytes(final String s) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> createInstance( |
| | | final Context context, final CreateRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | |
| | | return requestState.getConnection().thenAsync( |
| | | new AsyncFunction<Connection, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | // Calculate entry content. |
| | | return attributeMapper.create(requestState, 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 { |
| | | nameStrategy.setResourceId(requestState, getBaseDN(), |
| | | request.getNewResourceId(), addRequest); |
| | | } catch (final ResourceException e) { |
| | | return Promises.newExceptionPromise(e); |
| | | } |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | addRequest.addControl(PostReadRequestControl.newControl( |
| | | false, getLDAPAttributes(requestState, request.getFields()))); |
| | | } |
| | | return connection.applyChangeAsync(addRequest) |
| | | .thenAsync(postUpdateResultAsyncFunction(requestState), |
| | | ldapExceptionToResourceException()); |
| | | } |
| | | }); |
| | | } |
| | | }).thenFinally(close(requestState)); |
| | | public Promise<ResourceResponse, ResourceException> createInstance(final Context context, |
| | | final CreateRequest request) { |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | // Calculate entry content. |
| | | return attributeMapper |
| | | .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 { |
| | | nameStrategy.setResourceId(connection, getBaseDN(), |
| | | request.getNewResourceId(), |
| | | addRequest); |
| | | } catch (final ResourceException 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 RequestState requestState = wrap(context); |
| | | final AtomicReference<Connection> connectionHolder = new AtomicReference<>(); |
| | | return requestState.getConnection() |
| | | .thenOnResult(saveConnection(connectionHolder)) |
| | | .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision())) |
| | | 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(requestState, request.getFields()); |
| | | 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 connectionHolder.get().applyChangeAsync(deleteRequest) |
| | | .thenAsync(postUpdateResultAsyncFunction(requestState), |
| | | ldapExceptionToResourceException()); |
| | | return connection.applyChangeAsync(deleteRequest) |
| | | .thenAsync( |
| | | postUpdateResultAsyncFunction(connection), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }).thenFinally(close(requestState)); |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> patchInstance( |
| | | final Context context, final String resourceId, final PatchRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | if (request.getPatchOperations().isEmpty()) { |
| | | return emptyPatchInstance(requestState, resourceId, request); |
| | | return emptyPatchInstance(connection, resourceId, request); |
| | | } |
| | | |
| | | final AtomicReference<Connection> connectionHolder = new AtomicReference<>(); |
| | | return requestState.getConnection() |
| | | .thenOnResult(saveConnection(connectionHolder)) |
| | | .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision())) |
| | | return doUpdateFunction(connection, resourceId, request.getRevision()) |
| | | .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final DN dn) throws ResourceException { |
| | |
| | | List<Promise<List<Modification>, ResourceException>> promises = |
| | | new ArrayList<>(request.getPatchOperations().size()); |
| | | for (final PatchOperation operation : request.getPatchOperations()) { |
| | | promises.add(attributeMapper.patch(requestState, new JsonPointer(), operation)); |
| | | promises.add(attributeMapper.patch(connection, new JsonPointer(), operation)); |
| | | } |
| | | |
| | | return Promises.when(promises).thenAsync( |
| | |
| | | } |
| | | |
| | | final List<String> attributes = |
| | | asList(getLDAPAttributes(requestState, request.getFields())); |
| | | 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 connectionHolder.get() |
| | | .readEntryAsync(dn, attributes) |
| | | .thenAsync(postEmptyPatchAsyncFunction(requestState, request), |
| | | ldapExceptionToResourceException()); |
| | | return |
| | | connection |
| | | .readEntryAsync(dn, attributes) |
| | | .thenAsync(postEmptyPatchAsyncFunction(connection, request), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | } else { |
| | | // Add controls and perform the modify request. |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | |
| | | PermissiveModifyRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | return connectionHolder.get() |
| | | return connection |
| | | .applyChangeAsync(modifyRequest) |
| | | .thenAsync(postUpdateResultAsyncFunction(requestState), |
| | | ldapExceptionToResourceException()); |
| | | .thenAsync( |
| | | postUpdateResultAsyncFunction(connection), |
| | | Exceptions.<ResourceResponse>toResourceException()); |
| | | } |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | |
| | | } |
| | | }); |
| | | } |
| | | }).thenFinally(close(requestState)); |
| | | } |
| | | |
| | | /** Just read the entry and check its version. */ |
| | | private Promise<ResourceResponse, ResourceException> emptyPatchInstance( |
| | | final RequestState requestState, final String resourceId, final PatchRequest request) { |
| | | return requestState.getConnection() |
| | | .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | SearchRequest searchRequest = searchRequest(requestState, resourceId, request.getFields()); |
| | | return connection.searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(postEmptyPatchAsyncFunction(requestState, request), |
| | | ldapExceptionToResourceException()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** 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()); |
| | | } |
| | | |
| | | private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction( |
| | | final RequestState requestState, final PatchRequest request) { |
| | | final Connection connection, final PatchRequest request) { |
| | | return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) |
| | |
| | | try { |
| | | // Fail if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | | return adaptEntry(requestState, entry); |
| | | return adaptEntry(connection, entry); |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> queryCollection( |
| | | final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) { |
| | | final RequestState requestState = wrap(context); |
| | | |
| | | return requestState.getConnection() |
| | | .thenAsync(new AsyncFunction<Connection, QueryResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<QueryResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | // Calculate the filter (this may require the connection). |
| | | return getLDAPFilter(requestState, request.getQueryFilter()) |
| | | .thenAsync(runQuery(request, resourceHandler, requestState, connection)); |
| | | } |
| | | }) |
| | | .thenFinally(close(requestState)); |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | // Calculate the filter (this may require the connection). |
| | | return getLDAPFilter(connection, request.getQueryFilter()) |
| | | .thenAsync(runQuery(request, resourceHandler, connection)); |
| | | } |
| | | |
| | | private Promise<Filter, ResourceException> getLDAPFilter( |
| | | final RequestState requestState, final QueryFilter<JsonPointer> queryFilter) { |
| | | private Promise<Filter, ResourceException> getLDAPFilter(final Connection connection, |
| | | final QueryFilter<JsonPointer> queryFilter) { |
| | | final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor = |
| | | new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() { |
| | | |
| | |
| | | public Promise<Filter, ResourceException> visitContainsFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion); |
| | | connection, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitEqualsFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion); |
| | | connection, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused, |
| | | final JsonPointer field, final String operator, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion); |
| | | connection, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitGreaterThanFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion); |
| | | connection, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field, |
| | | return attributeMapper.getLDAPFilter(connection, new JsonPointer(), field, |
| | | FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | |
| | | public Promise<Filter, ResourceException> visitLessThanFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion); |
| | | connection, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field, |
| | | return attributeMapper.getLDAPFilter(connection, new JsonPointer(), field, |
| | | FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion); |
| | | } |
| | | |
| | |
| | | public Promise<Filter, ResourceException> visitPresentFilter( |
| | | final Void unused, final JsonPointer field) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.PRESENT, null, null); |
| | | connection, new JsonPointer(), field, FilterType.PRESENT, null, null); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Filter, ResourceException> visitStartsWithFilter( |
| | | final Void unused, final JsonPointer field, final Object valueAssertion) { |
| | | return attributeMapper.getLDAPFilter( |
| | | requestState, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion); |
| | | connection, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion); |
| | | } |
| | | |
| | | }; |
| | |
| | | } |
| | | |
| | | private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request, |
| | | final QueryResourceHandler resourceHandler, final RequestState requestState, final Connection connection) { |
| | | final QueryResourceHandler resourceHandler, final Connection connection) { |
| | | return new AsyncFunction<Filter, QueryResponse, ResourceException>() { |
| | | /** |
| | | * The following fields are guarded by sequenceLock. In addition, |
| | |
| | | } |
| | | final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create(); |
| | | // Perform the search. |
| | | final String[] attributes = getLDAPAttributes(requestState, request.getFields()); |
| | | final String[] attributes = getLDAPAttributes(connection, request.getFields()); |
| | | final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent() |
| | | : ldapFilter; |
| | | final SearchRequest searchRequest = newSearchRequest( |
| | |
| | | * The best solution is probably to process the primary search results in batches using |
| | | * the paged results control. |
| | | */ |
| | | final String id = nameStrategy.getResourceId(requestState, entry); |
| | | final String id = nameStrategy.getResourceId(connection, entry); |
| | | final String revision = getRevisionFromEntry(entry); |
| | | attributeMapper.read(requestState, new JsonPointer(), entry) |
| | | attributeMapper.read(connection, new JsonPointer(), entry) |
| | | .thenOnResult(new ResultHandler<JsonValue>() { |
| | | @Override |
| | | public void handleResult(final JsonValue result) { |
| | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> readInstance( |
| | | final Context context, final String resourceId, final ReadRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | |
| | | return requestState.getConnection() |
| | | .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() { |
| | | 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(Connection connection) |
| | | public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) |
| | | throws ResourceException { |
| | | // Do the search. |
| | | SearchRequest searchRequest = searchRequest(requestState, 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(requestState, entry); |
| | | } |
| | | }, |
| | | ldapExceptionToResourceException()); |
| | | return adaptEntry(connection, entry); |
| | | } |
| | | }) |
| | | .thenFinally(close(requestState)); |
| | | }, Exceptions.<ResourceResponse>toResourceException()); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> updateInstance( |
| | | final Context context, final String resourceId, final UpdateRequest request) { |
| | | final RequestState requestState = wrap(context); |
| | | final AtomicReference<Connection> connectionHolder = new AtomicReference<>(); |
| | | |
| | | return requestState.getConnection().thenOnResult(saveConnection(connectionHolder)) |
| | | .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() { |
| | | final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | List<JsonPointer> attrs = Collections.emptyList(); |
| | | SearchRequest searchRequest = searchRequest(connection, resourceId, attrs); |
| | | return connection |
| | | .searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | List<JsonPointer> attrs = Collections.emptyList(); |
| | | SearchRequest searchRequest = searchRequest(requestState, resourceId, attrs); |
| | | return connection.searchSingleEntryAsync(searchRequest) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | final SearchResultEntry entry) { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | final SearchResultEntry entry) { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | | |
| | | // Create the modify request. |
| | | final ModifyRequest modifyRequest = newModifyRequest(entry.getName()); |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | final String[] attributes = |
| | | getLDAPAttributes(requestState, request.getFields()); |
| | | modifyRequest.addControl( |
| | | PostReadRequestControl.newControl(false, attributes)); |
| | | } |
| | | if (config.usePermissiveModify()) { |
| | | modifyRequest.addControl( |
| | | PermissiveModifyRequestControl.newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | // 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()); |
| | | |
| | | // Determine the set of changes that need to be performed. |
| | | return attributeMapper.update( |
| | | requestState, new JsonPointer(), entry, request.getContent()) |
| | | .thenAsync(new AsyncFunction< |
| | | List<Modification>, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public Promise<ResourceResponse, ResourceException> apply( |
| | | List<Modification> modifications) |
| | | throws ResourceException { |
| | | if (modifications.isEmpty()) { |
| | | // No changes to be performed so just return |
| | | // the entry that we read. |
| | | return adaptEntry(requestState, entry); |
| | | } |
| | | // Perform the modify operation. |
| | | modifyRequest.getModifications().addAll(modifications); |
| | | return connection.applyChangeAsync(modifyRequest).thenAsync( |
| | | postUpdateResultAsyncFunction(requestState), |
| | | ldapExceptionToResourceException()); |
| | | } |
| | | }); |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | // Determine the set of changes that need to be performed. |
| | | return attributeMapper.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()); |
| | | } |
| | | } |
| | | }, ldapExceptionToResourceException()); |
| | | }); |
| | | } catch (final Exception e) { |
| | | return Promises.newExceptionPromise(asResourceException(e)); |
| | | } |
| | | } |
| | | }).thenFinally(close(requestState)); |
| | | }, Exceptions.<ResourceResponse>toResourceException()); |
| | | } |
| | | |
| | | private Promise<ResourceResponse, ResourceException> adaptEntry( |
| | | final RequestState requestState, final Entry entry) { |
| | | final String actualResourceId = nameStrategy.getResourceId(requestState, entry); |
| | | private Promise<ResourceResponse, ResourceException> adaptEntry(final Connection connection, final Entry entry) { |
| | | final String actualResourceId = nameStrategy.getResourceId(connection, entry); |
| | | final String revision = getRevisionFromEntry(entry); |
| | | return attributeMapper.read(requestState, new JsonPointer(), entry) |
| | | return attributeMapper.read(connection, new JsonPointer(), entry) |
| | | .then(new Function<JsonValue, ResourceResponse, ResourceException>() { |
| | | @Override |
| | | public ResourceResponse apply(final JsonValue value) { |
| | |
| | | } |
| | | } |
| | | |
| | | private AsyncFunction<Connection, DN, ResourceException> doUpdateFunction( |
| | | final RequestState requestState, final String resourceId, final String revision) { |
| | | return new AsyncFunction<Connection, DN, ResourceException>() { |
| | | @Override |
| | | public Promise<DN, ResourceException> apply(Connection connection) { |
| | | final String ldapAttribute = |
| | | (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1"; |
| | | final SearchRequest searchRequest = |
| | | nameStrategy.createSearchRequest(requestState, 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()); |
| | | } |
| | | 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)); |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | 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 = nameStrategy.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()); |
| | | } |
| | | 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)); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private void ensureMVCCSupported() throws NotSupportedException { |
| | |
| | | * Determines the set of LDAP attributes to request in an LDAP read (search, |
| | | * post-read), based on the provided list of JSON pointers. |
| | | * |
| | | * @param requestState |
| | | * @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 RequestState requestState, final Collection<JsonPointer> requestedAttributes) { |
| | | private String[] getLDAPAttributes(final Connection connection, final Collection<JsonPointer> requestedAttributes) { |
| | | // Get all the LDAP attributes required by the attribute mappers. |
| | | final Set<String> requestedLDAPAttributes; |
| | | if (requestedAttributes.isEmpty()) { |
| | | // Full read. |
| | | requestedLDAPAttributes = new LinkedHashSet<>(); |
| | | attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), new JsonPointer(), |
| | | attributeMapper.getLDAPAttributes(connection, new JsonPointer(), new JsonPointer(), |
| | | requestedLDAPAttributes); |
| | | } else { |
| | | // Partial read. |
| | | requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size()); |
| | | for (final JsonPointer requestedAttribute : requestedAttributes) { |
| | | attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), requestedAttribute, |
| | | attributeMapper.getLDAPAttributes(connection, new JsonPointer(), requestedAttribute, |
| | | requestedLDAPAttributes); |
| | | } |
| | | } |
| | | |
| | | // Get the LDAP attributes required by the Etag and name stategies. |
| | | nameStrategy.getLDAPAttributes(requestState, requestedLDAPAttributes); |
| | | nameStrategy.getLDAPAttributes(connection, requestedLDAPAttributes); |
| | | if (etagAttribute != null) { |
| | | requestedLDAPAttributes.add(etagAttribute.toString()); |
| | | } |
| | |
| | | } |
| | | |
| | | private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction( |
| | | final RequestState requestState) { |
| | | final Connection connection) { |
| | | // The handler which will be invoked for the LDAP add result. |
| | | return new AsyncFunction<Result, ResourceResponse, ResourceException>() { |
| | | @Override |
| | |
| | | entry = null; |
| | | } |
| | | if (entry != null) { |
| | | return adaptEntry(requestState, entry); |
| | | return adaptEntry(connection, entry); |
| | | } else { |
| | | return Promises.newResultPromise( |
| | | Responses.newResourceResponse(null, null, new JsonValue(Collections.emptyMap()))); |
| | |
| | | }; |
| | | } |
| | | |
| | | private AsyncFunction<LdapException, ResourceResponse, ResourceException> ldapExceptionToResourceException() { |
| | | return ldapToResourceException(); |
| | | } |
| | | |
| | | private <R> AsyncFunction<LdapException, R, ResourceException> ldapToResourceException() { |
| | | // 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)); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private SearchRequest searchRequest( |
| | | final RequestState requestState, final String resourceId, final List<JsonPointer> requestedAttributes) { |
| | | final String[] attributes = getLDAPAttributes(requestState, requestedAttributes); |
| | | return nameStrategy.createSearchRequest(requestState, getBaseDN(), resourceId).addAttribute(attributes); |
| | | final Connection connection, final String resourceId, final List<JsonPointer> requestedAttributes) { |
| | | final String[] attributes = getLDAPAttributes(connection, requestedAttributes); |
| | | return nameStrategy.createSearchRequest(connection, getBaseDN(), resourceId).addAttribute(attributes); |
| | | } |
| | | |
| | | private RequestState wrap(final Context context) { |
| | | return new RequestState(config, context); |
| | | } |
| | | |
| | | private Runnable close(final RequestState requestState) { |
| | | return new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | requestState.close(); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | private ResultHandler<Connection> saveConnection(final AtomicReference<Connection> connectionHolder) { |
| | | return new ResultHandler<Connection>() { |
| | | @Override |
| | | public void handleResult(Connection connection) { |
| | | connectionHolder.set(connection); |
| | | } |
| | | }; |
| | | 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)); |
| | | } |
| | | }; |
| | | } |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013-2015 ForgeRock AS. |
| | | * 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; |
| | |
| | | * Returns a search request which can be used to obtain the specified REST |
| | | * resource. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param baseDN |
| | | * The search base DN. |
| | | * @param resourceId |
| | |
| | | * @return A search request which can be used to obtain the specified REST |
| | | * resource. |
| | | */ |
| | | abstract SearchRequest createSearchRequest(RequestState requestState, DN baseDN, String resourceId); |
| | | abstract SearchRequest createSearchRequest(Connection connection, DN baseDN, String resourceId); |
| | | |
| | | /** |
| | | * Adds the name of any LDAP attribute required by this name strategy to the |
| | | * provided set. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @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. |
| | | */ |
| | | abstract void getLDAPAttributes(RequestState requestState, Set<String> ldapAttributes); |
| | | abstract void getLDAPAttributes(Connection connection, Set<String> ldapAttributes); |
| | | |
| | | /** |
| | | * Retrieves the resource ID from the provided LDAP entry. Implementations |
| | | * may use the entry DN as well as any attributes in order to determine the |
| | | * resource ID. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @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. |
| | | */ |
| | | abstract String getResourceId(RequestState requestState, Entry entry); |
| | | abstract String getResourceId(Connection connection, Entry entry); |
| | | |
| | | /** |
| | | * Sets the resource ID in the provided LDAP entry. Implementations are |
| | | * responsible for setting the entry DN as well as any attributes associated |
| | | * with the resource ID. |
| | | * |
| | | * @param requestState |
| | | * The request state. |
| | | * @param connection |
| | | * The LDAP connection to use to perform the operation. |
| | | * @param baseDN |
| | | * The baseDN to use when constructing the entry's DN. |
| | | * @param resourceId |
| | |
| | | * @throws ResourceException |
| | | * If the resource ID cannot be determined. |
| | | */ |
| | | abstract void setResourceId(RequestState requestState, DN baseDN, String resourceId, Entry entry) |
| | | abstract void setResourceId(Connection connection, DN baseDN, String resourceId, Entry entry) |
| | | throws ResourceException; |
| | | |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | * Copyright 2012-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import org.forgerock.json.resource.PatchOperation; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Attribute>, ResourceException> create( |
| | | final RequestState requestState, final JsonPointer path, final JsonValue v) { |
| | | Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path, |
| | | final JsonValue v) { |
| | | try { |
| | | /* |
| | | * First check that the JSON value is an object and that the fields |
| | |
| | | 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(requestState, path.child(me.getKey()), subValue)); |
| | | promises.add(mapping.mapper.create(connection, 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(requestState, path.child(mapping.name), null)); |
| | | promises.add(mapping.mapper.create(connection, path.child(mapping.name), null)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath, |
| | | void getLDAPAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | if (subPath.isEmpty()) { |
| | | // Request all subordinate mappings. |
| | | for (final Mapping mapping : mappings.values()) { |
| | | mapping.mapper.getLDAPAttributes(requestState, path.child(mapping.name), subPath, ldapAttributes); |
| | | mapping.mapper.getLDAPAttributes(connection, path.child(mapping.name), subPath, ldapAttributes); |
| | | } |
| | | } else { |
| | | // Request single subordinate mapping. |
| | | final Mapping mapping = getMapping(subPath); |
| | | if (mapping != null) { |
| | | mapping.mapper.getLDAPAttributes( |
| | | requestState, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes); |
| | | connection, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path, |
| | | 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); |
| | | if (mapping != null) { |
| | | return mapping.mapper.getLDAPFilter(requestState, path.child(subPath.get(0)), |
| | | return mapping.mapper.getLDAPFilter(connection, path.child(subPath.get(0)), |
| | | subPath.relativePointer(), type, operator, valueAssertion); |
| | | } else { |
| | | /* |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> patch( |
| | | final RequestState requestState, final JsonPointer path, final PatchOperation operation) { |
| | | Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path, |
| | | final PatchOperation operation) { |
| | | try { |
| | | final JsonPointer field = operation.getField(); |
| | | final JsonValue v = operation.getValue(); |
| | |
| | | final JsonValue subValue = new JsonValue(me.getValue()); |
| | | final PatchOperation subOperation = |
| | | operation(operation.getOperation(), field /* empty */, subValue); |
| | | promises.add(mapping.mapper.patch(requestState, path.child(me.getKey()), subOperation)); |
| | | promises.add(mapping.mapper.patch(connection, path.child(me.getKey()), subOperation)); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | final PatchOperation subOperation = |
| | | operation(operation.getOperation(), field.relativePointer(), v); |
| | | return mapping.mapper.patch(requestState, path.child(fieldName), subOperation); |
| | | return mapping.mapper.patch(connection, path.child(fieldName), subOperation); |
| | | } |
| | | } catch (final Exception ex) { |
| | | return Promises.newExceptionPromise(asResourceException(ex)); |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) { |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, 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 |
| | |
| | | new ArrayList<>(mappings.size()); |
| | | |
| | | for (final Mapping mapping : mappings.values()) { |
| | | promises.add(mapping.mapper.read(requestState, path.child(mapping.name), e) |
| | | 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) { |
| | |
| | | |
| | | @Override |
| | | Promise<List<Modification>, ResourceException> update( |
| | | final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) { |
| | | final Connection connection, 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. |
| | |
| | | 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(requestState, path.child(me.getKey()), e, subValue)); |
| | | promises.add(mapping.mapper.update(connection, 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(requestState, path.child(mapping.name), e, null)); |
| | | promises.add(mapping.mapper.update(connection, path.child(mapping.name), e, null)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | * Copyright 2012-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultReference; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.ExceptionHandler; |
| | |
| | | private static final int SEARCH_MAX_CANDIDATES = 1000; |
| | | |
| | | private final DN baseDN; |
| | | private final Schema schema; |
| | | private Filter filter; |
| | | private final AttributeMapper mapper; |
| | | private final AttributeDescription primaryKey; |
| | | private SearchScope scope = SearchScope.WHOLE_SUBTREE; |
| | | |
| | | ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN, |
| | | ReferenceAttributeMapper(final Schema schema, final AttributeDescription ldapAttributeName, final DN baseDN, |
| | | final AttributeDescription primaryKey, final AttributeMapper mapper) { |
| | | super(ldapAttributeName); |
| | | this.schema = schema; |
| | | this.baseDN = baseDN; |
| | | this.primaryKey = primaryKey; |
| | | this.mapper = mapper; |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path, |
| | | 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(requestState, path, subPath, type, operator, valueAssertion) |
| | | return mapper.getLDAPFilter(connection, path, subPath, type, operator, valueAssertion) |
| | | .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() { |
| | | @Override |
| | | public Promise<Filter, ResourceException> apply(final Filter result) { |
| | |
| | | final SearchRequest request = createSearchRequest(result); |
| | | final List<Filter> subFilters = new LinkedList<>(); |
| | | |
| | | return requestState.getConnection().thenAsync( |
| | | new AsyncFunction<Connection, Filter, ResourceException>() { |
| | | @Override |
| | | public Promise<Filter, ResourceException> apply(final Connection connection) |
| | | throws ResourceException { |
| | | return connection.searchAsync(request, new SearchResultHandler() { |
| | | @Override |
| | | public boolean handleEntry(final SearchResultEntry entry) { |
| | | if (subFilters.size() < SEARCH_MAX_CANDIDATES) { |
| | | subFilters.add(Filter.equality( |
| | | ldapAttributeName.toString(), entry.getName())); |
| | | return true; |
| | | } else { |
| | | // No point in continuing - maximum candidates reached. |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public boolean handleReference(final SearchResultReference reference) { |
| | | // Ignore references. |
| | | return true; |
| | | } |
| | | }).then(new Function<Result, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(Result result) throws ResourceException { |
| | | if (subFilters.size() >= SEARCH_MAX_CANDIDATES) { |
| | | throw asResourceException( |
| | | newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED)); |
| | | } else if (subFilters.size() == 1) { |
| | | return subFilters.get(0); |
| | | } else { |
| | | return Filter.or(subFilters); |
| | | } |
| | | } |
| | | }, new Function<LdapException, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(LdapException exception) throws ResourceException { |
| | | throw asResourceException(exception); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | return connection.searchAsync(request, new SearchResultHandler() { |
| | | @Override |
| | | public boolean handleEntry(final SearchResultEntry entry) { |
| | | if (subFilters.size() < SEARCH_MAX_CANDIDATES) { |
| | | subFilters.add(Filter.equality(ldapAttributeName.toString(), entry.getName())); |
| | | return true; |
| | | } else { |
| | | // No point in continuing - maximum candidates reached. |
| | | return false; |
| | | } |
| | | } |
| | | @Override |
| | | public boolean handleReference(final SearchResultReference reference) { |
| | | // Ignore references. |
| | | return true; |
| | | } |
| | | }).then(new Function<Result, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(Result result) throws ResourceException { |
| | | if (subFilters.size() >= SEARCH_MAX_CANDIDATES) { |
| | | throw asResourceException( |
| | | newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED)); |
| | | } else if (subFilters.size() == 1) { |
| | | return subFilters.get(0); |
| | | } else { |
| | | return Filter.or(subFilters); |
| | | } |
| | | } |
| | | }, new Function<LdapException, Filter, ResourceException>() { |
| | | @Override |
| | | public Filter apply(LdapException exception) throws ResourceException { |
| | | throw asResourceException(exception); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | Promise<Attribute, ResourceException> getNewLDAPAttributes( |
| | | final RequestState requestState, final JsonPointer path, final List<Object> newValues) { |
| | | Promise<Attribute, ResourceException> getNewLDAPAttributes(final Connection connection, 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(requestState, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() { |
| | | mapper.create(connection, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() { |
| | | @Override |
| | | public void handleResult(List<Attribute> result) { |
| | | Attribute primaryKeyAttribute = null; |
| | |
| | | final ByteString primaryKeyValue = primaryKeyAttribute.firstValue(); |
| | | final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue); |
| | | final SearchRequest search = createSearchRequest(filter); |
| | | requestState.getConnection().thenOnResult(new ResultHandler<Connection>() { |
| | | @Override |
| | | public void handleResult(Connection connection) { |
| | | connection.searchSingleEntryAsync(search) |
| | | .thenOnResult(new ResultHandler<SearchResultEntry>() { |
| | | @Override |
| | | public void handleResult(final SearchResultEntry result) { |
| | | synchronized (newLDAPAttribute) { |
| | | 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 = new BadRequestException(i18n( |
| | | "The request cannot be processed because the resource " |
| | | + "'%s' referenced in field '%s' does not exist", |
| | | primaryKeyValue.toString(), path)); |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | re = new BadRequestException(i18n( |
| | | "The request cannot be processed because the resource " |
| | | + "'%s' referenced in field '%s' is ambiguous", |
| | | primaryKeyValue.toString(), path)); |
| | | } catch (final LdapException e) { |
| | | re = asResourceException(e); |
| | | } |
| | | exception.compareAndSet(null, re); |
| | | completeIfNecessary(); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | 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 = new BadRequestException(i18n( |
| | | "The request cannot be processed because the resource " |
| | | + "'%s' referenced in field '%s' does not exist", |
| | | primaryKeyValue.toString(), path)); |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | re = new BadRequestException(i18n( |
| | | "The request cannot be processed because the resource " |
| | | + "'%s' referenced in field '%s' is ambiguous", |
| | | primaryKeyValue.toString(), path)); |
| | | } catch (final LdapException e) { |
| | | re = asResourceException(e); |
| | | } |
| | | exception.compareAndSet(null, re); |
| | | completeIfNecessary(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private void completeIfNecessary() { |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final RequestState c, final JsonPointer path, final Entry e) { |
| | | 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(c.getConfig().schema()).asDN(); |
| | | return readEntry(c, path, dn); |
| | | 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)); |
| | | } |
| | | } else { |
| | | try { |
| | | final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN(); |
| | | 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(c, path, dn)); |
| | | promises.add(readEntry(connection, path, dn)); |
| | | } |
| | | |
| | | return Promises.when(promises) |
| | |
| | | } |
| | | |
| | | private Promise<JsonValue, ResourceException> readEntry( |
| | | final RequestState requestState, final JsonPointer path, final DN dn) { |
| | | final Connection connection, final JsonPointer path, final DN dn) { |
| | | final Set<String> requestedLDAPAttributes = new LinkedHashSet<>(); |
| | | mapper.getLDAPAttributes(requestState, path, new JsonPointer(), requestedLDAPAttributes); |
| | | return requestState.getConnection().thenAsync(new AsyncFunction<Connection, JsonValue, ResourceException>() { |
| | | @Override |
| | | public Promise<JsonValue, ResourceException> apply(Connection connection) throws ResourceException { |
| | | final Filter searchFilter = filter != null ? filter : Filter.alwaysTrue(); |
| | | final String[] attributes = requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]); |
| | | final SearchRequest request = newSearchRequest(dn, SearchScope.BASE_OBJECT, searchFilter, attributes); |
| | | mapper.getLDAPAttributes(connection, path, new JsonPointer(), requestedLDAPAttributes); |
| | | |
| | | return connection.searchSingleEntryAsync(request) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() { |
| | | @Override |
| | | public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) { |
| | | return mapper.read(requestState, path, result); |
| | | } |
| | | }, new AsyncFunction<LdapException, JsonValue, ResourceException>() { |
| | | @Override |
| | | public Promise<JsonValue, ResourceException> apply(final LdapException error) { |
| | | if (error instanceof EntryNotFoundException) { |
| | | // Ignore missing entry since it cannot be mapped. |
| | | return Promises.newResultPromise(null); |
| | | } |
| | | return Promises.newExceptionPromise(asResourceException(error)); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | final Filter searchFilter = filter != null ? filter : Filter.alwaysTrue(); |
| | | final String[] attributes = requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]); |
| | | final SearchRequest request = newSearchRequest(dn, SearchScope.BASE_OBJECT, searchFilter, attributes); |
| | | |
| | | return connection |
| | | .searchSingleEntryAsync(request) |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() { |
| | | @Override |
| | | public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) { |
| | | return mapper.read(connection, path, result); |
| | | } |
| | | }, new AsyncFunction<LdapException, JsonValue, ResourceException>() { |
| | | @Override |
| | | public Promise<JsonValue, ResourceException> apply(final LdapException error) { |
| | | if (error instanceof EntryNotFoundException) { |
| | | // Ignore missing entry since it cannot be mapped. |
| | | return Promises.newResultPromise(null); |
| | | } |
| | | return Promises.newExceptionPromise(asResourceException(error)); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | /** A builder for incrementally constructing LDAP resource collections. */ |
| | | public static final class Builder { |
| | | private final List<Attribute> additionalLDAPAttributes = new LinkedList<>(); |
| | | private AuthorizationPolicy authzPolicy = AuthorizationPolicy.NONE; |
| | | private DN baseDN; // TODO: support template variables. |
| | | private AttributeDescription etagAttribute; |
| | | private ConnectionFactory factory; |
| | | private NameStrategy nameStrategy; |
| | | private AuthzIdTemplate proxiedAuthzTemplate; |
| | | private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS; |
| | | private AttributeMapper rootMapper; |
| | | private Schema schema = Schema.getDefaultSchema(); |
| | |
| | | } |
| | | |
| | | /** |
| | | * Sets the policy which should be for performing authorization. |
| | | * |
| | | * @param policy |
| | | * The policy which should be for performing authorization. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder authorizationPolicy(final AuthorizationPolicy policy) { |
| | | this.authzPolicy = ensureNotNull(policy); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the base DN beneath which LDAP entries (resources) are to be found. |
| | | * |
| | | * @param dn |
| | |
| | | if (rootMapper == null) { |
| | | throw new IllegalStateException("No mappings provided"); |
| | | } |
| | | switch (authzPolicy) { |
| | | case NONE: |
| | | if (factory == null) { |
| | | throw new IllegalStateException( |
| | | "A connection factory must be specified when the authorization policy is 'none'"); |
| | | } |
| | | break; |
| | | case PROXY: |
| | | if (proxiedAuthzTemplate == null) { |
| | | throw new IllegalStateException( |
| | | "Proxied authorization enabled but no template defined"); |
| | | } |
| | | if (factory == null) { |
| | | throw new IllegalStateException( |
| | | "A connection factory must be specified when using proxied authorization"); |
| | | } |
| | | break; |
| | | case REUSE: |
| | | // This is always ok. |
| | | break; |
| | | } |
| | | return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy, |
| | | etagAttribute, new Config(factory, readOnUpdatePolicy, authzPolicy, |
| | | proxiedAuthzTemplate, useSubtreeDelete, usePermissiveModify, schema), |
| | | return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy, etagAttribute, |
| | | new Config(readOnUpdatePolicy, useSubtreeDelete, usePermissiveModify, schema), |
| | | additionalLDAPAttributes); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * Sets the LDAP connection factory to be used for accessing the LDAP |
| | | * directory. Each HTTP request will obtain a single connection from the |
| | | * factory and then close it once the HTTP response has been sent. It is |
| | | * recommended that the provided connection factory supports connection |
| | | * pooling. |
| | | * |
| | | * @param factory |
| | | * The LDAP connection factory to be used for accessing the |
| | | * LDAP directory. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder ldapConnectionFactory(final ConnectionFactory factory) { |
| | | this.factory = factory; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the attribute mapper which should be used for mapping JSON |
| | | * resources to and from LDAP entries. |
| | | * |
| | |
| | | } |
| | | |
| | | /** |
| | | * Sets the authorization ID template which will be used for proxied |
| | | * authorization. Template parameters are specified by including the |
| | | * parameter name surrounded by curly braces. The template should |
| | | * contain fields which are expected to be found in the security context |
| | | * create during authentication, e.g. "dn:{dn}" or "u:{id}". |
| | | * |
| | | * @param template |
| | | * The authorization ID template which will be used for |
| | | * proxied authorization. |
| | | * @return A reference to this LDAP resource collection builder. |
| | | */ |
| | | public Builder proxyAuthzIdTemplate(final String template) { |
| | | this.proxiedAuthzTemplate = template != null ? new AuthzIdTemplate(template) : null; |
| | | 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}. |
| | |
| | | } |
| | | |
| | | @Override |
| | | SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) { |
| | | 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 RequestState requestState, final Set<String> ldapAttributes) { |
| | | void getLDAPAttributes(final Connection connection, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(idAttribute.toString()); |
| | | } |
| | | |
| | | @Override |
| | | String getResourceId(final RequestState requestState, final Entry entry) { |
| | | String getResourceId(final Connection connection, final Entry entry) { |
| | | return entry.parseAttribute(idAttribute).asString(); |
| | | } |
| | | |
| | | @Override |
| | | void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId, |
| | | void setResourceId(final Connection connection, final DN baseDN, final String resourceId, |
| | | final Entry entry) throws ResourceException { |
| | | if (isServerProvided) { |
| | | if (resourceId != null) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) { |
| | | 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 RequestState requestState, final Set<String> ldapAttributes) { |
| | | void getLDAPAttributes(final Connection connection, final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(attribute.toString()); |
| | | } |
| | | |
| | | @Override |
| | | String getResourceId(final RequestState requestState, final Entry entry) { |
| | | String getResourceId(final Connection connection, final Entry entry) { |
| | | return entry.parseAttribute(attribute).asString(); |
| | | } |
| | | |
| | | @Override |
| | | void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId, |
| | | 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))); |
| | |
| | | */ |
| | | public static ReferenceAttributeMapper reference(final AttributeDescription attribute, |
| | | final DN baseDN, final AttributeDescription primaryKey, final AttributeMapper mapper) { |
| | | return new ReferenceAttributeMapper(attribute, baseDN, primaryKey, mapper); |
| | | return new ReferenceAttributeMapper(Schema.getDefaultSchema(), attribute, baseDN, primaryKey, mapper); |
| | | } |
| | | |
| | | /** |
| | |
| | | * 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. |
| | | */ |
| | | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.http.util.Json.*; |
| | | import static org.forgerock.http.util.Json.readJsonLenient; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory; |
| | | import static org.forgerock.util.Utils.*; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | import static org.forgerock.util.Utils.closeSilently; |
| | | |
| | | import java.io.Closeable; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.net.URL; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.TreeMap; |
| | | |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.HttpApplication; |
| | | import org.forgerock.http.HttpApplicationException; |
| | | import org.forgerock.http.filter.Filters; |
| | | import org.forgerock.http.handler.Handlers; |
| | | import org.forgerock.http.io.Buffer; |
| | | import org.forgerock.http.protocol.Headers; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.json.JsonValue; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.Router; |
| | | import org.forgerock.json.resource.http.CrestHttp; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategy; |
| | | import org.forgerock.opendj.rest2ldap.authz.DirectConnectionFilter; |
| | | import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter; |
| | | import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.CustomHeaderExtractor; |
| | | import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor; |
| | | import org.forgerock.opendj.rest2ldap.authz.OptionalFilter; |
| | | import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.ConditionalFilter; |
| | | import org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter; |
| | | import org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter.IntrospectionAuthzProvider; |
| | | import org.forgerock.opendj.rest2ldap.authz.SASLPlainStrategy; |
| | | import org.forgerock.opendj.rest2ldap.authz.SearchThenBindStrategy; |
| | | import org.forgerock.opendj.rest2ldap.authz.SimpleBindStrategy; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.Factory; |
| | | import org.forgerock.util.Reject; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.Pair; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | |
| | | /** Rest2ldap HTTP application. */ |
| | | public final class Rest2LDAPHttpApplication implements HttpApplication { |
| | | public class Rest2LDAPHttpApplication implements HttpApplication { |
| | | private static final String DEFAULT_ROOT_FACTORY = "root"; |
| | | private static final String DEFAULT_BIND_FACTORY = "bind"; |
| | | |
| | | private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class); |
| | | |
| | | private static final class HttpHandler implements Handler, Closeable { |
| | | private final ConnectionFactory ldapConnectionFactory; |
| | | private final Handler delegate; |
| | | /** URL to the JSON configuration file. */ |
| | | protected final URL configurationUrl; |
| | | |
| | | HttpHandler(final JsonValue configuration) { |
| | | ldapConnectionFactory = createLdapConnectionFactory(configuration); |
| | | try { |
| | | delegate = CrestHttp.newHttpHandler(createRouter(configuration, ldapConnectionFactory)); |
| | | } catch (final RuntimeException e) { |
| | | closeSilently(ldapConnectionFactory); |
| | | throw e; |
| | | } |
| | | } |
| | | /** Schema used to perform DN validations. */ |
| | | protected final Schema schema; |
| | | |
| | | private static RequestHandler createRouter( |
| | | final JsonValue configuration, final ConnectionFactory ldapConnectionFactory) { |
| | | final AuthorizationPolicy authzPolicy = configuration.get("servlet") |
| | | .get("authorizationPolicy") |
| | | .required() |
| | | .asEnum(AuthorizationPolicy.class); |
| | | final String proxyAuthzTemplate = configuration.get("servlet").get("proxyAuthzIdTemplate").asString(); |
| | | final JsonValue mappings = configuration.get("servlet").get("mappings").required(); |
| | | private final Map<String, ConnectionFactory> connectionFactories = new HashMap<>(); |
| | | |
| | | final Router router = new Router(); |
| | | for (final String mappingUrl : mappings.keys()) { |
| | | final JsonValue mapping = mappings.get(mappingUrl); |
| | | final CollectionResourceProvider provider = Rest2LDAP.builder() |
| | | .ldapConnectionFactory(ldapConnectionFactory) |
| | | .authorizationPolicy(authzPolicy) |
| | | .proxyAuthzIdTemplate(proxyAuthzTemplate) |
| | | .configureMapping(mapping) |
| | | .build(); |
| | | router.addRoute(Router.uriTemplate(mappingUrl), provider); |
| | | } |
| | | return router; |
| | | } |
| | | private enum Policy { |
| | | oauth2 (0), |
| | | basic (50), |
| | | anonymous (100); |
| | | |
| | | private static ConnectionFactory createLdapConnectionFactory(final JsonValue configuration) { |
| | | final String ldapFactoryName = configuration.get("servlet").get("ldapConnectionFactory").asString(); |
| | | if (ldapFactoryName != null) { |
| | | return configureConnectionFactory( |
| | | configuration.get("ldapConnectionFactories").required(), ldapFactoryName); |
| | | } |
| | | return null; |
| | | } |
| | | private final int priority; |
| | | |
| | | @Override |
| | | public void close() { |
| | | closeSilently(ldapConnectionFactory); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) { |
| | | return delegate.handle(context, request); |
| | | Policy(int priority) { |
| | | this.priority = priority; |
| | | } |
| | | } |
| | | |
| | | private final URL configurationUrl; |
| | | private HttpHandler handler; |
| | | private HttpAuthenticationFilter filter; |
| | | private enum BindStrategy { |
| | | simple, search, sasl_plain |
| | | } |
| | | |
| | | /** |
| | | * 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 file location. |
| | | */ |
| | | public Rest2LDAPHttpApplication() { |
| | | this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json"); |
| | | this.schema = Schema.getDefaultSchema(); |
| | | } |
| | | |
| | | /** |
| | | * Creates a new Rest2LDAP HTTP application using the provided configuration URL. |
| | | * |
| | | * @param configurationURL |
| | | * The URL to the JSON configuration file. |
| | | * The URL to the JSON configuration file |
| | | * @param schema |
| | | * The {@link Schema} used to perform DN validations |
| | | */ |
| | | public Rest2LDAPHttpApplication(final URL configurationURL) { |
| | | Reject.ifNull(configurationURL, "The configuration URL must not be null"); |
| | | this.configurationUrl = configurationURL; |
| | | public Rest2LDAPHttpApplication(final URL configurationURL, final Schema schema) { |
| | | this.configurationUrl = checkNotNull(configurationURL, "configurationURL cannot be null"); |
| | | this.schema = checkNotNull(schema, "schema cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public final Handler start() throws HttpApplicationException { |
| | | try { |
| | | final JsonValue configuration = readJson(configurationUrl); |
| | | configureConnectionFactories(configuration.get("ldapConnectionFactories")); |
| | | return Handlers.chainOf( |
| | | CrestHttp.newHttpHandler(configureRest2Ldap(configuration)), |
| | | newAuthorizationFilter(configuration.get("authorization").required())); |
| | | } catch (final Exception e) { |
| | | // TODO i18n, once supported in opendj-rest2ldap |
| | | final String errorMsg = "Unable to start Rest2Ldap Http Application"; |
| | | LOG.error(errorMsg, e); |
| | | stop(); |
| | | throw new HttpApplicationException(errorMsg, e); |
| | | } |
| | | } |
| | | |
| | | private static JsonValue readJson(final URL resource) throws IOException { |
| | |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public Handler start() throws HttpApplicationException { |
| | | try { |
| | | final JsonValue configuration = readJson(configurationUrl); |
| | | handler = new HttpHandler(configuration); |
| | | filter = new HttpAuthenticationFilter(configuration); |
| | | return Handlers.chainOf(handler, filter); |
| | | } catch (final Exception e) { |
| | | // TODO i18n, once supported in opendj-rest2ldap |
| | | final String errorMsg = "Unable to start Rest2Ldap Http Application"; |
| | | LOG.error(errorMsg, e); |
| | | stop(); |
| | | throw new HttpApplicationException(errorMsg, e); |
| | | 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 void configureConnectionFactories(final JsonValue config) { |
| | | connectionFactories.clear(); |
| | | for (String name : config.keys()) { |
| | | connectionFactories.put(name, configureConnectionFactory(config, name)); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | @Override |
| | | public void stop() { |
| | | closeSilently(handler, filter); |
| | | handler = null; |
| | | filter = null; |
| | | for (ConnectionFactory factory : connectionFactories.values()) { |
| | | closeSilently(factory); |
| | | } |
| | | connectionFactories.clear(); |
| | | } |
| | | |
| | | private Filter newAuthorizationFilter(final JsonValue config) { |
| | | final List<Policy> configuredPolicies = new ArrayList<>(); |
| | | for (String policy : config.get("policies").required().asList(String.class)) { |
| | | configuredPolicies.add(Policy.valueOf(policy.toLowerCase())); |
| | | } |
| | | final TreeMap<Integer, Filter> policyFilters = new TreeMap<>(); |
| | | final int lastIndex = configuredPolicies.size() - 1; |
| | | for (int i = 0; i < configuredPolicies.size(); i++) { |
| | | final Policy policy = configuredPolicies.get(i); |
| | | policyFilters.put(policy.priority, |
| | | buildAuthzPolicyFilter(policy, config.get(policy.toString()), i != lastIndex)); |
| | | } |
| | | return Filters.chainOf(new ArrayList<>(policyFilters.values())); |
| | | } |
| | | |
| | | private Filter buildAuthzPolicyFilter(final Policy policy, final JsonValue config, boolean optional) { |
| | | switch (policy) { |
| | | case anonymous: |
| | | return buildAnonymousFilter(config); |
| | | case basic: |
| | | final ConditionalFilter basicFilter = buildBasicFilter(config.required()); |
| | | final Filter basicFilterChain = |
| | | config.get("reuseAuthenticatedConnection").defaultTo(Boolean.FALSE).asBoolean() |
| | | ? basicFilter |
| | | : Filters.chainOf(basicFilter, newProxyAuthzFilter(getConnectionFactory(DEFAULT_ROOT_FACTORY), |
| | | IntrospectionAuthzProvider.INSTANCE)); |
| | | return optional ? new OptionalFilter(basicFilterChain, basicFilter) : basicFilterChain; |
| | | default: |
| | | throw new IllegalArgumentException("Unsupported policy '" + policy + "'"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Create a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext}. |
| | | * |
| | | * @param connectionFactory |
| | | * The {@link ConnectionFactory} providing the {@link Connection} injected as |
| | | * {@link AuthenticatedConnectionContext} |
| | | * @param authzIdProvider |
| | | * Function computing the authzId to use for the LDAP's ProxiedAuth control. |
| | | * @return a newly created {@link Filter} |
| | | */ |
| | | protected Filter newProxyAuthzFilter(final ConnectionFactory connectionFactory, |
| | | final Function<SecurityContext, String, LdapException> authzIdProvider) { |
| | | return new ProxiedAuthV2Filter(connectionFactory, authzIdProvider); |
| | | } |
| | | |
| | | private Filter buildAnonymousFilter(final JsonValue config) { |
| | | if (config.contains("userDN")) { |
| | | final DN userDN = DN.valueOf(config.get("userDN").asString(), schema); |
| | | final Map<String, Object> authz = new HashMap<>(1); |
| | | authz.put(SecurityContext.AUTHZID_DN, userDN.toString()); |
| | | return Filters.chainOf( |
| | | newStaticSecurityContextFilter(null, authz), |
| | | newProxyAuthzFilter( |
| | | getConnectionFactory(config.get("ldapConnectionFactory") |
| | | .defaultTo(DEFAULT_ROOT_FACTORY) |
| | | .asString()), |
| | | IntrospectionAuthzProvider.INSTANCE)); |
| | | } |
| | | return newDirectConnectionFilter(getConnectionFactory(config.get("ldapConnectionFactory") |
| | | .defaultTo(DEFAULT_ROOT_FACTORY).asString())); |
| | | } |
| | | |
| | | /** |
| | | * Create a new {@link Filter} injecting a predefined {@link SecurityContext}. |
| | | * |
| | | * @param authenticationId |
| | | * AuthenticationID of the {@link SecurityContext}. |
| | | * @param authorization |
| | | * Authorization of the {@link SecurityContext} |
| | | * @return a newly created {@link Filter} |
| | | */ |
| | | protected Filter newStaticSecurityContextFilter(final String authenticationId, |
| | | final Map<String, Object> authorization) { |
| | | return new Filter() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) { |
| | | return next.handle(new SecurityContext(context, authenticationId, authorization), request); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Create a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext} directly from a |
| | | * {@link ConnectionFactory}. |
| | | * |
| | | * @param connectionFactory |
| | | * The {@link ConnectionFactory} used to get the {@link Connection} |
| | | * @return a newly created {@link Filter} |
| | | */ |
| | | protected Filter newDirectConnectionFilter(ConnectionFactory connectionFactory) { |
| | | return new DirectConnectionFilter(connectionFactory); |
| | | } |
| | | |
| | | /** |
| | | * Get a {@link ConnectionFactory} from its name. |
| | | * |
| | | * @param name |
| | | * Name of the {@link ConnectionFactory} as specified in the configuration |
| | | * @return The associated {@link ConnectionFactory} or null if none can be found |
| | | */ |
| | | protected ConnectionFactory getConnectionFactory(final String name) { |
| | | return connectionFactories.get(name); |
| | | } |
| | | |
| | | private ConditionalFilter buildBasicFilter(final JsonValue config) { |
| | | final String bind = config.get("bind").required().asString(); |
| | | final BindStrategy strategy = BindStrategy.valueOf(bind.toLowerCase().replace('-', '_')); |
| | | return newBasicAuthenticationFilter(buildBindStrategy(strategy, config.get(bind).required()), |
| | | config.get("supportAltAuthentication").defaultTo(Boolean.FALSE).asBoolean() |
| | | ? new CustomHeaderExtractor( |
| | | config.get("altAuthenticationUsernameHeader").required().asString(), |
| | | config.get("altAuthenticationPasswordHeader").required().asString()) |
| | | : HttpBasicExtractor.INSTANCE, |
| | | config.get("reuseAuthenticatedConnection").defaultTo(Boolean.FALSE).asBoolean()); |
| | | } |
| | | |
| | | /** |
| | | * Get a {@link Filter} in charge of performing the HTTP-Basic Authentication. This filter create a |
| | | * {@link SecurityContext} reflecting the authenticated users. |
| | | * |
| | | * @param authenticationStrategy |
| | | * The {@link AuthenticationStrategy} to use to authenticate the user. |
| | | * @param credentialsExtractor |
| | | * Extract the user's credentials from the {@link Headers}. |
| | | * @param reuseAuthenticatedConnection |
| | | * Let the bound connection open so that it can be reused to perform the LDAP operations. |
| | | * @return A new {@link Filter} |
| | | */ |
| | | protected ConditionalFilter newBasicAuthenticationFilter(AuthenticationStrategy authenticationStrategy, |
| | | Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor, |
| | | boolean reuseAuthenticatedConnection) { |
| | | return new HttpBasicAuthenticationFilter(authenticationStrategy, credentialsExtractor, |
| | | reuseAuthenticatedConnection); |
| | | } |
| | | |
| | | private AuthenticationStrategy buildBindStrategy(final BindStrategy strategy, final JsonValue config) { |
| | | switch (strategy) { |
| | | case simple: |
| | | return buildSimpleBindStrategy(config); |
| | | case search: |
| | | return buildSearchThenBindStrategy(config); |
| | | case sasl_plain: |
| | | return buildSASLBindStrategy(config); |
| | | default: |
| | | throw new IllegalArgumentException("Unsupported strategy '" + strategy + "'"); |
| | | } |
| | | } |
| | | |
| | | private AuthenticationStrategy buildSimpleBindStrategy(final JsonValue config) { |
| | | return newSimpleBindStrategy(getConnectionFactory(config.get("ldapConnectionFactory") |
| | | .defaultTo(DEFAULT_BIND_FACTORY).asString()), |
| | | config.get("bindDNTemplate").defaultTo("%s").asString(), |
| | | schema); |
| | | } |
| | | |
| | | /** |
| | | * {@link AuthenticationStrategy} performing an LDAP Bind request with a computed DN. |
| | | * |
| | | * @param connectionFactory |
| | | * The {@link ConnectionFactory} to use to perform the bind operation |
| | | * @param schema |
| | | * {@link Schema} used to perform the DN validation. |
| | | * @param bindDNTemplate |
| | | * DN template containing a single %s which will be replaced by the authenticating user's name. (i.e: |
| | | * uid=%s,ou=people,dc=example,dc=com) |
| | | * @return A new {@link AuthenticationStrategy} |
| | | */ |
| | | protected AuthenticationStrategy newSimpleBindStrategy(ConnectionFactory connectionFactory, String bindDNTemplate, |
| | | Schema schema) { |
| | | return new SimpleBindStrategy(connectionFactory, bindDNTemplate, schema); |
| | | } |
| | | |
| | | private AuthenticationStrategy buildSASLBindStrategy(JsonValue config) { |
| | | return newSASLBindStrategy(getConnectionFactory(config.get("ldapConnectionFactory") |
| | | .defaultTo(DEFAULT_BIND_FACTORY).asString()), |
| | | config.get("authcIdTemplate").defaultTo("u:%s").asString()); |
| | | } |
| | | |
| | | /** |
| | | * {@link AuthenticationStrategy} performing an LDAP SASL-Plain Bind. |
| | | * |
| | | * @param connectionFactory |
| | | * The {@link ConnectionFactory} to use to perform the bind operation |
| | | * @param authcIdTemplate |
| | | * Authentication identity template containing a single %s which will be replaced by the authenticating |
| | | * user's name. (i.e: (u:%s) |
| | | * @return A new {@link AuthenticationStrategy} |
| | | */ |
| | | protected AuthenticationStrategy newSASLBindStrategy(ConnectionFactory connectionFactory, String authcIdTemplate) { |
| | | return new SASLPlainStrategy(connectionFactory, schema, authcIdTemplate); |
| | | } |
| | | |
| | | private AuthenticationStrategy buildSearchThenBindStrategy(JsonValue config) { |
| | | return newSearchThenBindStrategy( |
| | | getConnectionFactory( |
| | | config.get("searchLDAPConnectionFactory").defaultTo(DEFAULT_ROOT_FACTORY).asString()), |
| | | getConnectionFactory( |
| | | config.get("bindLDAPConnectionFactory").defaultTo(DEFAULT_BIND_FACTORY).asString()), |
| | | DN.valueOf(config.get("baseDN").required().asString(), schema), |
| | | SearchScope.valueOf(config.get("scope").required().asString().toLowerCase()), |
| | | config.get("filterTemplate").required().asString()); |
| | | } |
| | | |
| | | /** |
| | | * {@link AuthenticationStrategy} performing an LDAP Search to get a DN to bind with. |
| | | * |
| | | * @param searchConnectionFactory |
| | | * The {@link ConnectionFactory} to sue to perform the search operation. |
| | | * @param bindConnectionFactory |
| | | * The {@link ConnectionFactory} to use to perform the bind operation |
| | | * @param baseDN |
| | | * The base DN of the search request |
| | | * @param scope |
| | | * {@link SearchScope} of the search request |
| | | * @param filterTemplate |
| | | * filter template containing a single %s which will be replaced by the authenticating user's name. (i.e: |
| | | * (&(uid=%s)(objectClass=inetOrgPerson)) |
| | | * @return A new {@link AuthenticationStrategy} |
| | | */ |
| | | protected AuthenticationStrategy newSearchThenBindStrategy(ConnectionFactory searchConnectionFactory, |
| | | ConnectionFactory bindConnectionFactory, DN baseDN, SearchScope scope, String filterTemplate) { |
| | | return new SearchThenBindStrategy( |
| | | searchConnectionFactory, bindConnectionFactory, baseDN, scope, filterTemplate); |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012-2015 ForgeRock AS. |
| | | * Copyright 2012-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | 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.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.util.Function; |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path, |
| | | Promise<Filter, ResourceException> getLDAPFilter(final Connection connection, final JsonPointer path, |
| | | final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) { |
| | | if (subPath.isEmpty()) { |
| | | try { |
| | |
| | | |
| | | @Override |
| | | Promise<Attribute, ResourceException> getNewLDAPAttributes( |
| | | final RequestState requestState, final JsonPointer path, final List<Object> newValues) { |
| | | final Connection connection, final JsonPointer path, final List<Object> newValues) { |
| | | try { |
| | | return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder())); |
| | | } catch (final Exception ex) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) { |
| | | Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) { |
| | | try { |
| | | final Object value; |
| | | if (attributeIsSingleValued()) { |
| 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.authz; |
| | | |
| | | import static org.forgerock.util.Reject.*; |
| | | |
| | | import org.forgerock.opendj.ldap.AbstractAsynchronousConnection; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionEventListener; |
| | | import org.forgerock.opendj.ldap.IntermediateResponseHandler; |
| | | import org.forgerock.opendj.ldap.LdapPromise; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.requests.AbandonRequest; |
| | | 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.DeleteRequest; |
| | | 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.requests.UnbindRequest; |
| | | 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; |
| | | |
| | | abstract class AbstractAsynchronousConnectionDecorator extends AbstractAsynchronousConnection { |
| | | |
| | | protected final Connection delegate; |
| | | |
| | | AbstractAsynchronousConnectionDecorator(Connection delegate) { |
| | | this.delegate = checkNotNull(delegate, "delegate cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Void> abandonAsync(AbandonRequest request) { |
| | | return delegate.abandonAsync(request); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> addAsync(AddRequest request, IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.addAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public void addConnectionEventListener(ConnectionEventListener listener) { |
| | | delegate.addConnectionEventListener(listener); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<BindResult> bindAsync(BindRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.bindAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public void close(UnbindRequest request, String reason) { |
| | | delegate.close(request, reason); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<CompareResult> compareAsync(CompareRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.compareAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> deleteAsync(DeleteRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.deleteAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.extendedRequestAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public boolean isClosed() { |
| | | return delegate.isClosed(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean isValid() { |
| | | return delegate.isValid(); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> modifyAsync(ModifyRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.modifyAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> modifyDNAsync(ModifyDNRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.modifyDNAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public void removeConnectionEventListener(ConnectionEventListener listener) { |
| | | delegate.removeConnectionEventListener(listener); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> searchAsync(SearchRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler) { |
| | | return delegate.searchAsync(request, intermediateResponseHandler, entryHandler); |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return delegate.toString(); |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** Authenticate a user and create a {@link SecurityContext} as a result. */ |
| | | public interface AuthenticationStrategy { |
| | | /** |
| | | * Authenticate a user. |
| | | * |
| | | * @param username |
| | | * User to authenticate. |
| | | * @param password |
| | | * Password used to perform the authentication. |
| | | * @param parentContext |
| | | * Context to use as parent for the created {@link SecurityContext} |
| | | * @param authenticatedConnectionHolder |
| | | * Output parameter. If supported, the implementations will set the reference to a ready to be used LDAP |
| | | * connection bound to the given credentials. |
| | | * @return A {@link Context} if the authentication succeed or an {@link LdapException} otherwise. |
| | | */ |
| | | Promise<SecurityContext, LdapException> authenticate(String username, String password, Context parentContext, |
| | | AtomicReference<Connection> authenticatedConnectionHolder); |
| | | } |
| File was renamed from opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013-2015 ForgeRock AS. |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.isJSONPrimitive; |
| | | package org.forgerock.opendj.rest2ldap.authz; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | import org.forgerock.json.resource.ForbiddenException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | |
| | |
| | | */ |
| | | final class AuthzIdTemplate { |
| | | private static interface Impl { |
| | | String formatAsAuthzId(AuthzIdTemplate t, Object[] templateVariables, Schema schema) |
| | | throws ResourceException; |
| | | String formatAsAuthzId(AuthzIdTemplate t, Object[] templateVariables, Schema schema); |
| | | } |
| | | |
| | | private static final Impl DN_IMPL = new Impl() { |
| | | |
| | | @Override |
| | | public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables, |
| | | final Schema schema) throws ResourceException { |
| | | final Schema schema) { |
| | | final String authzId = String.format(Locale.ENGLISH, t.formatString, templateVariables); |
| | | try { |
| | | // Validate the DN. |
| | | DN.valueOf(authzId.substring(3), schema); |
| | | } catch (final IllegalArgumentException e) { |
| | | throw new ForbiddenException( |
| | | i18n("The request could not be authorized because the required " |
| | | + "security principal was not a valid LDAP DN")); |
| | | throw new IllegalArgumentException( |
| | | "The request could not be authorized because the required security principal " |
| | | + "was not a valid LDAP DN"); |
| | | } |
| | | return authzId; |
| | | } |
| | |
| | | |
| | | @Override |
| | | public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables, |
| | | final Schema schema) throws ResourceException { |
| | | final Schema schema) { |
| | | return "dn:" + DN.format(t.dnFormatString, schema, templateVariables); |
| | | } |
| | | |
| | |
| | | |
| | | @Override |
| | | public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables, |
| | | final Schema schema) throws ResourceException { |
| | | final Schema schema) { |
| | | return String.format(Locale.ENGLISH, t.formatString, templateVariables); |
| | | } |
| | | |
| | |
| | | private final Impl pimpl; |
| | | private final String template; |
| | | |
| | | AuthzIdTemplate(final String template) { |
| | | /** |
| | | * Create a new authorization ID template. |
| | | * |
| | | * @param template |
| | | * Authorization ID template |
| | | * @throws IllegalArgumentException |
| | | * if template doesn't start with "u:" or "dn:" |
| | | */ |
| | | public AuthzIdTemplate(final String template) { |
| | | if (!template.startsWith("u:") && !template.startsWith("dn:")) { |
| | | throw new IllegalArgumentException("Invalid authorization ID template: " + template); |
| | | } |
| | |
| | | return template; |
| | | } |
| | | |
| | | String formatAsAuthzId(final Map<String, Object> principals, final Schema schema) |
| | | throws ResourceException { |
| | | /** |
| | | * Return the template with all the variable replaced. |
| | | * |
| | | * @param principals |
| | | * Value to use to replace the variables. |
| | | * @param schema |
| | | * Schema to perform validation. |
| | | * @return The template with all the variable replaced. |
| | | */ |
| | | public String formatAsAuthzId(final Map<String, Object> principals, final Schema schema) { |
| | | final String[] templateVariables = getPrincipalsForFormatting(principals); |
| | | return pimpl.formatAsAuthzId(this, templateVariables, schema); |
| | | } |
| | | |
| | | private String[] getPrincipalsForFormatting(final Map<String, Object> principals) |
| | | throws ForbiddenException { |
| | | private String[] getPrincipalsForFormatting(final Map<String, Object> principals) { |
| | | final String[] values = new String[keys.size()]; |
| | | for (int i = 0; i < values.length; i++) { |
| | | final String key = keys.get(i); |
| | |
| | | if (isJSONPrimitive(value)) { |
| | | values[i] = String.valueOf(value); |
| | | } else if (value != null) { |
| | | throw new ForbiddenException(i18n( |
| | | throw new IllegalArgumentException(String.format( |
| | | "The request could not be authorized because the required " |
| | | + "security principal '%s' had an invalid data type", key)); |
| | | } else { |
| | | throw new ForbiddenException(i18n( |
| | | throw new IllegalArgumentException(String.format( |
| | | "The request could not be authorized because the required " |
| | | + "security principal '%s' could not be determined", key)); |
| | | } |
| | | } |
| | | return values; |
| | | } |
| | | |
| | | static boolean isJSONPrimitive(final Object value) { |
| | | return value instanceof String || value instanceof Boolean || value instanceof Number; |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentLinkedQueue; |
| | | import java.util.concurrent.CountDownLatch; |
| | | |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.IntermediateResponseHandler; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.LdapPromise; |
| | | import org.forgerock.opendj.ldap.LdapResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl; |
| | | import org.forgerock.opendj.ldap.requests.BindRequest; |
| | | import org.forgerock.opendj.ldap.requests.DeleteRequest; |
| | | 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.ExtendedResult; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultReference; |
| | | |
| | | /** |
| | | * Cache entries by intercepting the result of base search requests. Entries in cache are automatically evicted if their |
| | | * DN is involved in a modify/delete operation. This cache is used to prevent multiple read operations on the same DN. |
| | | * This happens frequently when we have to resolve entry references in a collection. |
| | | */ |
| | | final class CachedReadConnectionDecorator extends AbstractAsynchronousConnectionDecorator { |
| | | |
| | | @SuppressWarnings("serial") |
| | | private final Map<DN, CachedRead> cachedReads = new LinkedHashMap<DN, CachedRead>() { |
| | | private static final int MAX_CACHED_ENTRIES = 32; |
| | | |
| | | @Override |
| | | protected boolean removeEldestEntry(final Map.Entry<DN, CachedRead> eldest) { |
| | | return size() > MAX_CACHED_ENTRIES; |
| | | } |
| | | }; |
| | | |
| | | /** A cached read request - see cachedReads for more information. */ |
| | | private static final class CachedRead implements SearchResultHandler, LdapResultHandler<Result> { |
| | | private SearchResultEntry cachedEntry; |
| | | private final String cachedFilterString; |
| | | |
| | | /** Promise of the pending read operation. @GuardedBy("cachedPromiseLatch"). */ |
| | | private LdapPromise<Result> cachedPromise; |
| | | private final CountDownLatch cachedPromiseLatch = new CountDownLatch(1); |
| | | private final SearchRequest cachedRequest; |
| | | private volatile Result cachedResult; |
| | | private final ConcurrentLinkedQueue<SearchResultHandler> waitingResultHandlers = new ConcurrentLinkedQueue<>(); |
| | | |
| | | CachedRead(final SearchRequest request, final SearchResultHandler resultHandler) { |
| | | this.cachedRequest = request; |
| | | this.cachedFilterString = request.getFilter().toString(); |
| | | this.waitingResultHandlers.add(resultHandler); |
| | | } |
| | | |
| | | @Override |
| | | public boolean handleEntry(final SearchResultEntry entry) { |
| | | cachedEntry = entry; |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public void handleException(final LdapException exception) { |
| | | handleResult(exception.getResult()); |
| | | } |
| | | |
| | | @Override |
| | | public boolean handleReference(final SearchResultReference reference) { |
| | | // Ignore - should never happen for a base object search. |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final Result result) { |
| | | cachedResult = result; |
| | | drainQueue(); |
| | | } |
| | | |
| | | void addResultHandler(final SearchResultHandler resultHandler) { |
| | | // Fast path. |
| | | if (cachedResult != null) { |
| | | invokeResultHandler(resultHandler); |
| | | return; |
| | | } |
| | | // Enqueue and re-check. |
| | | waitingResultHandlers.add(resultHandler); |
| | | if (cachedResult != null) { |
| | | drainQueue(); |
| | | } |
| | | } |
| | | |
| | | LdapPromise<Result> getPromise() { |
| | | // Perform uninterrupted wait since this method is unlikely to block for a long time. |
| | | boolean wasInterrupted = false; |
| | | while (true) { |
| | | try { |
| | | cachedPromiseLatch.await(); |
| | | if (wasInterrupted) { |
| | | Thread.currentThread().interrupt(); |
| | | } |
| | | return cachedPromise; |
| | | } catch (final InterruptedException e) { |
| | | wasInterrupted = true; |
| | | } |
| | | } |
| | | } |
| | | |
| | | boolean isMatchingRead(final SearchRequest request) { |
| | | // Cached reads are always base object. |
| | | return request.getScope().equals(SearchScope.BASE_OBJECT) |
| | | // Filters must match. |
| | | && request.getFilter().toString().equals(cachedFilterString) |
| | | // List of requested attributes must match. |
| | | && request.getAttributes().equals(cachedRequest.getAttributes()); |
| | | } |
| | | |
| | | void setPromise(final LdapPromise<Result> promise) { |
| | | cachedPromise = promise; |
| | | cachedPromiseLatch.countDown(); |
| | | } |
| | | |
| | | private void drainQueue() { |
| | | SearchResultHandler resultHandler; |
| | | while ((resultHandler = waitingResultHandlers.poll()) != null) { |
| | | invokeResultHandler(resultHandler); |
| | | } |
| | | } |
| | | |
| | | private void invokeResultHandler(final SearchResultHandler searchResultHandler) { |
| | | if (cachedEntry != null) { |
| | | searchResultHandler.handleEntry(cachedEntry); |
| | | } |
| | | } |
| | | } |
| | | |
| | | CachedReadConnectionDecorator(Connection delegate) { |
| | | super(delegate); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<BindResult> bindAsync(BindRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | /* |
| | | * Simple brute force implementation in case the bind operation |
| | | * modifies an entry: clear the cachedReads. |
| | | */ |
| | | evictAll(); |
| | | return delegate.bindAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | private void evictAll() { |
| | | synchronized (cachedReads) { |
| | | cachedReads.clear(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(ExtendedRequest<R> request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | /* |
| | | * Simple brute force implementation in case the extended |
| | | * operation modifies an entry: clear the cachedReads. |
| | | */ |
| | | evictAll(); |
| | | return delegate.extendedRequestAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> modifyAsync(ModifyRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | evict(request.getName()); |
| | | return delegate.modifyAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | private void evict(final DN name) { |
| | | synchronized (cachedReads) { |
| | | cachedReads.remove(name); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> deleteAsync(DeleteRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | // Simple brute force implementation: clear the cachedReads. |
| | | if (request.containsControl(SubtreeDeleteRequestControl.OID)) { |
| | | evictAll(); |
| | | } else { |
| | | evict(request.getName()); |
| | | } |
| | | return delegate.deleteAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> modifyDNAsync(ModifyDNRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | // Simple brute force implementation: clear the cachedReads. |
| | | evictAll(); |
| | | return delegate.modifyDNAsync(request, intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> searchAsync(SearchRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler) { |
| | | /* |
| | | * Don't attempt caching if this search is not a read (base |
| | | * object), or if the search request passed in an intermediate |
| | | * response handler. |
| | | */ |
| | | if (!request.getScope().equals(SearchScope.BASE_OBJECT) || intermediateResponseHandler != null) { |
| | | return delegate.searchAsync(request, intermediateResponseHandler, entryHandler); |
| | | } |
| | | |
| | | // This is a read request and a candidate for caching. |
| | | final CachedRead cachedRead; |
| | | synchronized (cachedReads) { |
| | | cachedRead = cachedReads.get(request.getName()); |
| | | } |
| | | if (cachedRead != null && cachedRead.isMatchingRead(request)) { |
| | | // The cached read matches this read request. |
| | | cachedRead.addResultHandler(entryHandler); |
| | | return cachedRead.getPromise(); |
| | | } else { |
| | | // Cache the read, possibly evicting a non-matching cached read. |
| | | final CachedRead pendingCachedRead = new CachedRead(request, entryHandler); |
| | | synchronized (cachedReads) { |
| | | cachedReads.put(request.getName(), pendingCachedRead); |
| | | } |
| | | final LdapPromise<Result> promise = delegate |
| | | .searchAsync(request, intermediateResponseHandler, pendingCachedRead) |
| | | .thenOnResult(pendingCachedRead).thenOnException(pendingCachedRead); |
| | | pendingCachedRead.setPromise(promise); |
| | | return promise; |
| | | } |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.authz.Utils.*; |
| | | import static org.forgerock.util.Reject.*; |
| | | |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * Inject {@link Connection} into a {@link AuthenticatedConnectionContext}. |
| | | */ |
| | | public final class DirectConnectionFilter implements Filter { |
| | | |
| | | private final ConnectionFactory connectionFactory; |
| | | |
| | | /** |
| | | * Create a new {@link DirectConnectionFilter}. |
| | | * |
| | | * @param connectionFactory |
| | | * The factory used to get the {@link Connection} |
| | | * @throws NullPointerException |
| | | * if connectionFactory is null. |
| | | */ |
| | | public DirectConnectionFilter(ConnectionFactory connectionFactory) { |
| | | this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> filter(final Context context, final Request request, |
| | | final Handler next) { |
| | | final AtomicReference<Connection> connectionHolder = new AtomicReference<Connection>(); |
| | | return connectionFactory |
| | | .getConnectionAsync() |
| | | .thenAsync(new AsyncFunction<Connection, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(Connection connection) { |
| | | connectionHolder.set(connection); |
| | | return next.handle(new AuthenticatedConnectionContext(context, connection), request); |
| | | } |
| | | }, new AsyncFunction<LdapException, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(LdapException exception) { |
| | | return asErrorResponse(exception); |
| | | } |
| | | }) |
| | | .thenFinally(close(connectionHolder)); |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.authz.Utils.asErrorResponse; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | import static org.forgerock.opendj.rest2ldap.authz.Utils.close; |
| | | |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Headers; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.EntryNotFoundException; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext; |
| | | import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.ConditionalFilter; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.Pair; |
| | | import org.forgerock.util.encode.Base64; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * Inject a {@link SecurityContext} if the credentials provided in the {@link Request} headers have been successfully |
| | | * verified. |
| | | */ |
| | | public final class HttpBasicAuthenticationFilter implements ConditionalFilter { |
| | | |
| | | private final AuthenticationStrategy authenticationStrategy; |
| | | private final Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor; |
| | | private final boolean reuseAuthenticatedConnection; |
| | | |
| | | /** |
| | | * Create a new HttpBasicAuthenticationFilter. |
| | | * |
| | | * @param authenticationStrategy |
| | | * The strategy to use to perform the authentication. |
| | | * @param credentialsExtractor |
| | | * The function to use to extract credentials from the {@link Headers}. |
| | | * @param reuseAuthenticatedConnection |
| | | * Let the bound connection open so that it can be reused to perform the LDAP operations. |
| | | * @throws NullPointerException |
| | | * If a parameter is null. |
| | | */ |
| | | public HttpBasicAuthenticationFilter(AuthenticationStrategy authenticationStrategy, |
| | | Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor, |
| | | boolean reuseAuthenticatedConnection) { |
| | | this.authenticationStrategy = checkNotNull(authenticationStrategy, "authenticationStrategy cannot be null"); |
| | | this.credentialsExtractor = checkNotNull(credentialsExtractor, "credentialsExtractor cannot be null"); |
| | | this.reuseAuthenticatedConnection = reuseAuthenticatedConnection; |
| | | } |
| | | |
| | | @Override |
| | | public boolean canApplyFilter(Context context, Request request) { |
| | | return credentialsExtractor.apply(request.getHeaders()) != null; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> filter(final Context context, final Request request, |
| | | final Handler next) { |
| | | final Pair<String, String> credentials = credentialsExtractor.apply(request.getHeaders()); |
| | | if (credentials == null) { |
| | | return asErrorResponse(LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS)); |
| | | } |
| | | final AtomicReference<Connection> authConnHolder = new AtomicReference<Connection>(); |
| | | return authenticationStrategy |
| | | .authenticate(credentials.getFirst(), credentials.getSecond(), context, authConnHolder) |
| | | .thenAsync(new AsyncFunction<SecurityContext, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(final SecurityContext securityContext) { |
| | | if (reuseAuthenticatedConnection) { |
| | | return next |
| | | .handle(new AuthenticatedConnectionContext(securityContext, authConnHolder.get()), |
| | | request); |
| | | } |
| | | close(authConnHolder); |
| | | return next.handle(securityContext, request); |
| | | } |
| | | }, new AsyncFunction<LdapException, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<? extends Response, ? extends NeverThrowsException> apply(LdapException exception) { |
| | | return asErrorResponse(exception instanceof EntryNotFoundException |
| | | ? LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS, exception.getMessage()) |
| | | : exception); |
| | | } |
| | | }) |
| | | .thenFinally(close(authConnHolder)); |
| | | } |
| | | |
| | | /** Extract the user's credentials from custom {@link Headers}. */ |
| | | public static final class CustomHeaderExtractor |
| | | implements Function<Headers, Pair<String, String>, NeverThrowsException> { |
| | | |
| | | private final String customHeaderUsername; |
| | | private final String customHeaderPassword; |
| | | |
| | | /** |
| | | * Create a new CustomHeaderExtractor. |
| | | * |
| | | * @param customHeaderUsername |
| | | * Name of the header containing the username |
| | | * @param customHeaderPassword |
| | | * Name of the header containing the password |
| | | * @throws NullPointerException |
| | | * if a parameter is null. |
| | | */ |
| | | public CustomHeaderExtractor(String customHeaderUsername, String customHeaderPassword) { |
| | | this.customHeaderUsername = checkNotNull(customHeaderUsername, "customHeaderUsername cannot be null"); |
| | | this.customHeaderPassword = checkNotNull(customHeaderPassword, "customHeaderPassword cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public Pair<String, String> apply(Headers headers) { |
| | | final String userName = headers.getFirst(customHeaderUsername); |
| | | final String password = headers.getFirst(customHeaderPassword); |
| | | if (userName != null && password != null) { |
| | | return Pair.of(userName, password); |
| | | } |
| | | return HttpBasicExtractor.INSTANCE.apply(headers); |
| | | } |
| | | } |
| | | |
| | | /** Extract the user's credentials from the standard HTTP Basic {@link Headers}. */ |
| | | public static final class HttpBasicExtractor |
| | | implements Function<Headers, Pair<String, String>, NeverThrowsException> { |
| | | |
| | | /** HTTP Header sent by the client with HTTP basic authentication. */ |
| | | public static final String HTTP_BASIC_AUTH_HEADER = "Authorization"; |
| | | |
| | | /** Reference to the HttpBasicExtractor Singleton. */ |
| | | public static final HttpBasicExtractor INSTANCE = new HttpBasicExtractor(); |
| | | |
| | | private HttpBasicExtractor() { } |
| | | |
| | | @Override |
| | | public Pair<String, String> apply(Headers headers) { |
| | | final String httpBasicAuthHeader = headers.getFirst(HTTP_BASIC_AUTH_HEADER); |
| | | if (httpBasicAuthHeader != null) { |
| | | final Pair<String, String> userCredentials = parseUsernamePassword(httpBasicAuthHeader); |
| | | if (userCredentials != null) { |
| | | return userCredentials; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Pair<String, String> parseUsernamePassword(String authHeader) { |
| | | if (authHeader != null && (authHeader.toLowerCase().startsWith("basic"))) { |
| | | // We received authentication info |
| | | // Example received header: |
| | | // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" |
| | | final String base64UserCredentials = authHeader.substring("basic".length() + 1); |
| | | // Example usage of base64: |
| | | // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==" |
| | | final String userCredentials = new String(Base64.decode(base64UserCredentials)); |
| | | String[] split = userCredentials.split(":"); |
| | | if (split.length == 2) { |
| | | return Pair.of(split[0], split[1]); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** Encapsulate a {@link Filter} which can be conditionally applied. */ |
| | | public final class OptionalFilter implements Filter { |
| | | |
| | | /** Condition which have to be fulfilled in order to apply the {@link Filter}. */ |
| | | public interface Condition { |
| | | /** |
| | | * Check if a {@link Filter} must be executed or not. |
| | | * |
| | | * @param context |
| | | * Current {@link Context} of the request processing. |
| | | * @param request |
| | | * the {@link Request} currently processed. |
| | | * @return true if the filter must be applied. |
| | | */ |
| | | boolean canApplyFilter(Context context, Request request); |
| | | } |
| | | |
| | | /** A {@link Filter} which can be conditionally applied. */ |
| | | public interface ConditionalFilter extends Filter, Condition { |
| | | } |
| | | |
| | | private final Filter delegate; |
| | | private final Condition condition; |
| | | |
| | | /** |
| | | * Make {@link Filter} optional. |
| | | * |
| | | * @param delegate |
| | | * {@link Filter} which will be conditionally applied; |
| | | * @param condition |
| | | * The {@link Condition} which have to be fulfilled in order to apply the filter. |
| | | */ |
| | | public OptionalFilter(Filter delegate, Condition condition) { |
| | | this.delegate = delegate; |
| | | this.condition = condition; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) { |
| | | if (condition.canApplyFilter(context, request)) { |
| | | return delegate.filter(context, request, next); |
| | | } |
| | | return next.handle(context, request); |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl.newControl; |
| | | import static org.forgerock.opendj.rest2ldap.authz.Utils.asErrorResponse; |
| | | import static org.forgerock.opendj.rest2ldap.authz.Utils.close; |
| | | import static org.forgerock.services.context.SecurityContext.AUTHZID_DN; |
| | | import static org.forgerock.services.context.SecurityContext.AUTHZID_ID; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.IntermediateResponseHandler; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.LdapPromise; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.controls.Control; |
| | | import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl; |
| | | import org.forgerock.opendj.ldap.requests.AddRequest; |
| | | import org.forgerock.opendj.ldap.requests.CompareRequest; |
| | | import org.forgerock.opendj.ldap.requests.DeleteRequest; |
| | | 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.CompareResult; |
| | | import org.forgerock.opendj.ldap.responses.ExtendedResult; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** |
| | | * Inject an {@link AuthenticatedConnectionContext} following the information provided in the {@link SecurityContext}. |
| | | * This connection will add a {@link ProxiedAuthV2RequestControl} to each LDAP requests. |
| | | */ |
| | | public final class ProxiedAuthV2Filter implements Filter { |
| | | |
| | | private final ConnectionFactory connectionFactory; |
| | | private final Function<SecurityContext, String, LdapException> authzProvider; |
| | | |
| | | /** |
| | | * Create a new ProxyAuthzFilter. The {@link Connection} contained in the injected |
| | | * {@link AuthenticatedConnectionContext} will use a {@link ProxiedAuthV2RequestControl} to perform authorization. |
| | | * The authorizationID of this control is computed for each request by resolving the authzTemplate against the |
| | | * {@link SecurityContext}. |
| | | * |
| | | * @param connectionFactory |
| | | * Factory used to get the {@link Connection} |
| | | * @param authzIdProvider |
| | | * Function in charge of providing the authzid to use for the ProxiedAuth control |
| | | * @throws NullPointerException |
| | | * If a parameter is null |
| | | */ |
| | | public ProxiedAuthV2Filter(final ConnectionFactory connectionFactory, |
| | | final Function<SecurityContext, String, LdapException> authzIdProvider) { |
| | | this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null"); |
| | | this.authzProvider = checkNotNull(authzIdProvider, "authzIdProvider cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> filter(final Context context, final Request request, |
| | | final Handler next) { |
| | | final AtomicReference<Connection> connectionHolder = new AtomicReference<>(); |
| | | return connectionFactory |
| | | .getConnectionAsync() |
| | | .then(new Function<Connection, Connection, LdapException>() { |
| | | @Override |
| | | public Connection apply(Connection connection) throws LdapException { |
| | | connectionHolder.set(connection); |
| | | final Connection proxiedConnection = newProxiedConnection( |
| | | connection, authzProvider.apply(context.asContext(SecurityContext.class))); |
| | | connectionHolder.set(proxiedConnection); |
| | | return proxiedConnection; |
| | | } |
| | | }) |
| | | .thenAsync(new AsyncFunction<Connection, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(Connection connection) { |
| | | return next.handle(new AuthenticatedConnectionContext(context, connection), request); |
| | | } |
| | | }, new AsyncFunction<LdapException, Response, NeverThrowsException>() { |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> apply(LdapException ldapException) { |
| | | return asErrorResponse(ldapException); |
| | | } |
| | | }) |
| | | .thenFinally(close(connectionHolder)); |
| | | } |
| | | |
| | | private Connection newProxiedConnection(Connection baseConnection, String authzId) { |
| | | return new CachedReadConnectionDecorator( |
| | | new ProxiedAuthConnectionDecorator(baseConnection, newControl(authzId))); |
| | | } |
| | | |
| | | /** |
| | | * Introspect the content of the {@link SecurityContext} and return the contained DN, or the user's ID if DN is not |
| | | * present. |
| | | */ |
| | | public static final class IntrospectionAuthzProvider implements Function<SecurityContext, String, LdapException> { |
| | | |
| | | /** Singleton instance. */ |
| | | public static final Function<SecurityContext, String, LdapException> INSTANCE = |
| | | new IntrospectionAuthzProvider(); |
| | | |
| | | private IntrospectionAuthzProvider() { |
| | | } |
| | | |
| | | @Override |
| | | public String apply(SecurityContext securityContext) throws LdapException { |
| | | Object candidate; |
| | | candidate = securityContext.getAuthorization().get(AUTHZID_DN); |
| | | if (candidate != null) { |
| | | return "dn:" + candidate; |
| | | } |
| | | candidate = securityContext.getAuthorization().get(AUTHZID_ID); |
| | | if (candidate != null) { |
| | | return "u:" + candidate; |
| | | } |
| | | throw LdapException.newLdapException(ResultCode.AUTH_METHOD_NOT_SUPPORTED); |
| | | } |
| | | } |
| | | |
| | | /** Use a {@link AuthzIdTemplate} to compute the authzId from the provided {@link SecurityContext}. */ |
| | | public static final class TemplateAuthzProvider implements Function<SecurityContext, String, LdapException> { |
| | | private final AuthzIdTemplate template; |
| | | private final Schema schema; |
| | | |
| | | TemplateAuthzProvider(AuthzIdTemplate template, Schema schema) { |
| | | this.template = checkNotNull(template, "template cannot be null"); |
| | | this.schema = checkNotNull(schema, "schema cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public String apply(SecurityContext securityContext) throws LdapException { |
| | | try { |
| | | return template.formatAsAuthzId(securityContext.getAuthorization(), schema); |
| | | } catch (IllegalArgumentException e) { |
| | | throw LdapException.newLdapException(ResultCode.OPERATIONS_ERROR); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static final class ProxiedAuthConnectionDecorator extends AbstractAsynchronousConnectionDecorator { |
| | | |
| | | private final Control proxiedAuthzControl; |
| | | |
| | | ProxiedAuthConnectionDecorator(Connection delegate, Control proxiedAuthzControl) { |
| | | super(delegate); |
| | | this.proxiedAuthzControl = proxiedAuthzControl; |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> addAsync(AddRequest request, |
| | | IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.addAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<CompareResult> compareAsync(final CompareRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.compareAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> deleteAsync(final DeleteRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.deleteAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request, |
| | | final IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.extendedRequestAsync(request.addControl(proxiedAuthzControl), |
| | | intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> modifyAsync(final ModifyRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.modifyAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler) { |
| | | return delegate.modifyDNAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler); |
| | | } |
| | | |
| | | @Override |
| | | public LdapPromise<Result> searchAsync(final SearchRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler) { |
| | | return delegate.searchAsync(request.addControl(proxiedAuthzControl), intermediateResponseHandler, |
| | | entryHandler); |
| | | } |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newPlainSASLBindRequest; |
| | | import static org.forgerock.services.context.SecurityContext.AUTHZID_DN; |
| | | import static org.forgerock.services.context.SecurityContext.AUTHZID_ID; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.DecodeOptions; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.controls.AuthorizationIdentityRequestControl; |
| | | import org.forgerock.opendj.ldap.controls.AuthorizationIdentityResponseControl; |
| | | import org.forgerock.opendj.ldap.responses.BindResult; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | |
| | | /** Bind using a computed DN from a template and the current request/context. */ |
| | | public final class SASLPlainStrategy implements AuthenticationStrategy { |
| | | |
| | | private final ConnectionFactory connectionFactory; |
| | | private final Function<String, String, LdapException> formatter; |
| | | |
| | | /** |
| | | * Create a new SASLPlainStrategy. |
| | | * |
| | | * @param connectionFactory |
| | | * Factory used to get {@link Connection} receiving the sasl-bind requests |
| | | * @param authcIdTemplate |
| | | * Authentication identity template containing a single %s which will be replaced by the authenticating |
| | | * user's name. (i.e: (u:%s) |
| | | * @param schema |
| | | * Schema used to perform DN validation. |
| | | * @throws NullPointerException |
| | | * If a parameter is null |
| | | */ |
| | | public SASLPlainStrategy(final ConnectionFactory connectionFactory, final Schema schema, |
| | | final String authcIdTemplate) { |
| | | this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null"); |
| | | checkNotNull(schema, "schema cannot be null"); |
| | | checkNotNull(authcIdTemplate, "authcIdTemplate cannot be null"); |
| | | if (authcIdTemplate.startsWith("dn:")) { |
| | | formatter = new Function<String, String, LdapException>() { |
| | | @Override |
| | | public String apply(String value) throws LdapException { |
| | | try { |
| | | return DN.format(authcIdTemplate, schema, value).toString(); |
| | | } catch (LocalizedIllegalArgumentException e) { |
| | | throw LdapException.newLdapException(ResultCode.INVALID_DN_SYNTAX, e.getMessageObject(), e); |
| | | } |
| | | } |
| | | }; |
| | | } else { |
| | | formatter = new Function<String, String, LdapException>() { |
| | | @Override |
| | | public String apply(String value) throws LdapException { |
| | | return String.format(authcIdTemplate, value); |
| | | } |
| | | }; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public Promise<SecurityContext, LdapException> authenticate(final String username, final String password, |
| | | final Context parentContext, final AtomicReference<Connection> authenticateConnectionHolder) { |
| | | return connectionFactory |
| | | .getConnectionAsync() |
| | | .thenAsync(new AsyncFunction<Connection, SecurityContext, LdapException>() { |
| | | @Override |
| | | public Promise<SecurityContext, LdapException> apply(Connection connection) throws LdapException { |
| | | authenticateConnectionHolder.set(connection); |
| | | final String authcId = formatter.apply(username); |
| | | return doSASLPlainBind(connection, parentContext, username, authcId, password); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private Promise<SecurityContext, LdapException> doSASLPlainBind(final Connection connection, |
| | | final Context parentContext, final String authzId, final String authcId, final String password) { |
| | | return connection |
| | | .bindAsync(newPlainSASLBindRequest(authcId, password.toCharArray()) |
| | | .addControl(AuthorizationIdentityRequestControl.newControl(true))) |
| | | .then(new Function<BindResult, SecurityContext, LdapException>() { |
| | | @Override |
| | | public SecurityContext apply(BindResult result) throws LdapException { |
| | | final Map<String, Object> authz = new LinkedHashMap<>(2); |
| | | try { |
| | | final AuthorizationIdentityResponseControl control = |
| | | result.getControl(AuthorizationIdentityResponseControl.DECODER, |
| | | new DecodeOptions()); |
| | | if (control != null) { |
| | | final String authzDN = control.getAuthorizationID(); |
| | | if (authzDN.startsWith("dn:")) { |
| | | authz.put(AUTHZID_DN, authzDN.substring(3)); |
| | | } |
| | | } |
| | | } catch (DecodeException e) { |
| | | // Just ignore |
| | | } |
| | | authz.put(AUTHZID_ID, authzId); |
| | | return new SecurityContext(parentContext, authcId, authz); |
| | | } |
| | | }); |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; |
| | | import static org.forgerock.opendj.rest2ldap.authz.SimpleBindStrategy.doSimpleBind; |
| | | import static org.forgerock.opendj.rest2ldap.authz.Utils.close; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** Bind using the result of a search request computed from the current request/context. */ |
| | | public final class SearchThenBindStrategy implements AuthenticationStrategy { |
| | | private final ConnectionFactory searchConnectionFactory; |
| | | private final ConnectionFactory bindConnectionFactory; |
| | | |
| | | private final DN baseDN; |
| | | private final SearchScope searchScope; |
| | | private final String filterTemplate; |
| | | |
| | | /** |
| | | * Create a new SearchThenBindStrategy. |
| | | * |
| | | * @param searchConnectionFactory |
| | | * Factory to use to perform the search operation |
| | | * @param bindConnectionFactory |
| | | * Factory to use to perform the bind operation |
| | | * @param baseDN |
| | | * BaseDN of the search request |
| | | * @param searchScope |
| | | * Scope of the search request |
| | | * @param filterTemplate |
| | | * Filter of the search request (i.e: (&(uid=%s)(objectClass=inetOrgPerson) |
| | | * @throws NullPointerException |
| | | * If a parameter is null |
| | | */ |
| | | public SearchThenBindStrategy(ConnectionFactory searchConnectionFactory, ConnectionFactory bindConnectionFactory, |
| | | DN baseDN, SearchScope searchScope, String filterTemplate) { |
| | | this.searchConnectionFactory = checkNotNull(searchConnectionFactory, "searchConnectionFactory cannot be null"); |
| | | this.bindConnectionFactory = checkNotNull(bindConnectionFactory, "bindConnectionFactory cannot be null"); |
| | | this.baseDN = checkNotNull(baseDN, "baseDN cannot be null"); |
| | | this.searchScope = checkNotNull(searchScope, "searchScope cannot be null"); |
| | | this.filterTemplate = checkNotNull(filterTemplate, "filterTemplate cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<SecurityContext, LdapException> authenticate(final String username, final String password, |
| | | final Context parentContext, final AtomicReference<Connection> authenticatedConnectionHolder) { |
| | | final AtomicReference<Connection> searchConnectionHolder = new AtomicReference<>(); |
| | | return searchConnectionFactory |
| | | .getConnectionAsync() |
| | | // Search |
| | | .thenAsync(new AsyncFunction<Connection, SearchResultEntry, LdapException>() { |
| | | @Override |
| | | public Promise<SearchResultEntry, LdapException> apply(final Connection connection) |
| | | throws LdapException { |
| | | searchConnectionHolder.set(connection); |
| | | return connection.searchSingleEntryAsync( |
| | | newSearchRequest(baseDN, searchScope, Filter.format(filterTemplate, username), "1.1")); |
| | | } |
| | | }) |
| | | .thenFinally(close(searchConnectionHolder)) |
| | | // Bind |
| | | .thenAsync(new AsyncFunction<SearchResultEntry, SecurityContext, LdapException>() { |
| | | @Override |
| | | public Promise<SecurityContext, LdapException> apply(final SearchResultEntry searchResult) |
| | | throws LdapException { |
| | | return bindConnectionFactory |
| | | .getConnectionAsync() |
| | | .thenAsync( |
| | | doSimpleBind(authenticatedConnectionHolder, parentContext, username, |
| | | searchResult.getName(), password)); |
| | | } |
| | | }); |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest; |
| | | import static org.forgerock.services.context.SecurityContext.AUTHZID_DN; |
| | | import static org.forgerock.services.context.SecurityContext.AUTHZID_ID; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.responses.BindResult; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.AsyncFunction; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.Promise; |
| | | |
| | | /** Bind using a computed DN from a template and the current request/context. */ |
| | | public final class SimpleBindStrategy implements AuthenticationStrategy { |
| | | |
| | | private final ConnectionFactory connectionFactory; |
| | | private final Schema schema; |
| | | private final String bindDNTemplate; |
| | | |
| | | /** |
| | | * Create a new SimpleBindStrategy. |
| | | * |
| | | * @param connectionFactory |
| | | * Factory used to get {@link Connection} receiving the bind requests |
| | | * @param schema |
| | | * Schema used to validate DN |
| | | * @param bindDNTemplate |
| | | * The template which will be replaced by the authenticating user (i.e: |
| | | * uid=%s,ou=People,dc=example,dc=com) |
| | | * @throws NullPointerException |
| | | * If a parameter is null |
| | | */ |
| | | public SimpleBindStrategy(ConnectionFactory connectionFactory, String bindDNTemplate, Schema schema) { |
| | | this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null"); |
| | | this.bindDNTemplate = checkNotNull(bindDNTemplate, "bindDNTemplate cannot be null"); |
| | | this.schema = checkNotNull(schema, "schema cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public Promise<SecurityContext, LdapException> authenticate(final String username, final String password, |
| | | final Context parentContext, final AtomicReference<Connection> authenticatedConnectionHolder) { |
| | | return connectionFactory |
| | | .getConnectionAsync() |
| | | .thenAsync(doSimpleBind(authenticatedConnectionHolder, parentContext, username, |
| | | DN.format(bindDNTemplate, schema, username), password)); |
| | | } |
| | | |
| | | static AsyncFunction<Connection, SecurityContext, LdapException> doSimpleBind( |
| | | final AtomicReference<Connection> connectionHolder, final Context parentContext, final String username, |
| | | final DN bindDN, final String password) { |
| | | return new AsyncFunction<Connection, SecurityContext, LdapException>() { |
| | | @Override |
| | | public Promise<SecurityContext, LdapException> apply(Connection connection) throws LdapException { |
| | | connectionHolder.set(connection); |
| | | return connection |
| | | .bindAsync(newSimpleBindRequest(bindDN.toString(), password.toCharArray())) |
| | | .then(new Function<BindResult, SecurityContext, LdapException>() { |
| | | @Override |
| | | public SecurityContext apply(BindResult result) throws LdapException { |
| | | final Map<String, Object> authzid = new LinkedHashMap<>(2); |
| | | authzid.put(AUTHZID_DN, bindDN.toString()); |
| | | authzid.put(AUTHZID_ID, username); |
| | | return new SecurityContext(parentContext, username, authzid); |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | import static org.forgerock.util.Utils.closeSilently; |
| | | |
| | | import java.io.Closeable; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.http.protocol.Status; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | |
| | | final class Utils { |
| | | |
| | | private Utils() { } |
| | | |
| | | static Runnable close(final AtomicReference<? extends Closeable> holder) { |
| | | return new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | closeSilently(holder.get()); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | 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()); |
| | | if (response.getStatus() == Status.UNAUTHORIZED) { |
| | | response.getHeaders().put("WWW-Authenticate", "Basic"); |
| | | } |
| | | return Promises.newResultPromise(response); |
| | | } |
| | | } |
| 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. |
| | | */ |
| | | |
| | | /** |
| | | * This package contains {@link org.forgerock.http.Filter} to authenticate and authorize LDAP connections. Authorization |
| | | * filter injects a {@link org.forgerock.services.context.SecurityContext} populated with authorization information like |
| | | * user's id, user's DN or anything else. This {@link org.forgerock.services.context.SecurityContext} can then be used |
| | | * by {@link org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter} to inject an |
| | | * {@link org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext} containing the |
| | | * {@link org.forgerock.opendj.ldap.Connection} with user specific privileges. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap.authz; |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2012 ForgeRock AS. |
| | | * Copyright 2012-2016 ForgeRock AS. |
| | | */ |
| | | |
| | | /** |
| | | * APIs for implementing REST to LDAP gateways. |
| | | * APIs for implementing REST to LDAP gateways. The API is implemented by |
| | | * {@link org.forgerock.opendj.rest2ldap.LDAPCollectionResourceProvider} which is using a pre-established |
| | | * {@link org.forgerock.opendj.ldap.Connection} encapsulated in the |
| | | * {@link org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext}. This context is injected by the |
| | | * {@link org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter} depending on the |
| | | * {@link org.forgerock.services.context.SecurityContext} injected by one of the configured authorization filters. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013-2015 ForgeRock AS. |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | |
| | | import org.forgerock.json.resource.NotSupportedException; |
| | | import org.forgerock.json.resource.PreconditionFailedException; |
| | | import org.forgerock.json.resource.QueryResponse; |
| | | import org.forgerock.json.resource.Requests; |
| | | 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.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; |
| | | import org.testng.annotations.Test; |
| | |
| | | final Connection connection = newConnection(); |
| | | final List<ResourceResponse> resources = new LinkedList<>(); |
| | | final QueryResponse result = connection.query( |
| | | ctx(), Requests.newQueryRequest("").setQueryFilter(NO_FILTER), resources); |
| | | newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER), resources); |
| | | assertThat(resources).hasSize(5); |
| | | assertThat(result.getPagedResultsCookie()).isNull(); |
| | | assertThat(result.getTotalPagedResults()).isEqualTo(-1); |
| | |
| | | public void testQueryNone() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final List<ResourceResponse> resources = new LinkedList<>(); |
| | | final QueryResponse result = connection.query( |
| | | ctx(), Requests.newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer>alwaysFalse()), resources); |
| | | final QueryResponse result = connection.query(newAuthConnectionContext(), |
| | | newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()), resources); |
| | | assertThat(resources).hasSize(0); |
| | | assertThat(result.getPagedResultsCookie()).isNull(); |
| | | assertThat(result.getTotalPagedResults()).isEqualTo(-1); |
| | |
| | | |
| | | // Read first page. |
| | | QueryResponse result = connection.query( |
| | | ctx(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources); |
| | | newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources); |
| | | assertThat(result.getPagedResultsCookie()).isNotNull(); |
| | | assertThat(resources).hasSize(2); |
| | | assertThat(resources.get(0).getId()).isEqualTo("test1"); |
| | |
| | | resources.clear(); |
| | | |
| | | // Read second page. |
| | | result = connection.query(ctx(), |
| | | result = connection.query(newAuthConnectionContext(), |
| | | newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources); |
| | | assertThat(result.getPagedResultsCookie()).isNotNull(); |
| | | assertThat(resources).hasSize(2); |
| | |
| | | resources.clear(); |
| | | |
| | | // Read third page. |
| | | result = connection.query(ctx(), |
| | | result = connection.query(newAuthConnectionContext(), |
| | | newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources); |
| | | assertThat(result.getPagedResultsCookie()).isNull(); |
| | | assertThat(resources).hasSize(1); |
| | |
| | | public void testQueryPageResultsIndexed() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final List<ResourceResponse> resources = new ArrayList<>(); |
| | | QueryResponse result = connection.query(ctx(), |
| | | QueryResponse result = connection.query(newAuthConnectionContext(), |
| | | newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources); |
| | | assertThat(result.getPagedResultsCookie()).isNotNull(); |
| | | assertThat(resources).hasSize(2); |
| | |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testDelete() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1")); |
| | | final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | connection.read(ctx(), newReadRequest("/test1")); |
| | | connection.read(context, newReadRequest("/test1")); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testDeleteMVCCMatch() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345")); |
| | | final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1").setRevision("12345")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | connection.read(ctx(), newReadRequest("/test1")); |
| | | connection.read(context, newReadRequest("/test1")); |
| | | } |
| | | |
| | | @Test(expectedExceptions = PreconditionFailedException.class) |
| | | public void testDeleteMVCCNoMatch() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12346")); |
| | | connection.delete(context, newDeleteRequest("/test1").setRevision("12346")); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testDeleteNotFound() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | connection.delete(ctx(), newDeleteRequest("/missing")); |
| | | connection.delete(context, newDeleteRequest("/missing")); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatch() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | final ResourceResponse resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed"))); |
| | | final ResourceResponse resource1 = connection.patch(context, |
| | | newPatchRequest("/test1", add("/name/displayName", "changed"))); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchEmpty() throws Exception { |
| | | final List<Request> requests = new LinkedList<>(); |
| | | final Context context = newAuthConnectionContext(requests); |
| | | final Connection connection = newConnection(requests); |
| | | final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1")); |
| | | final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1")); |
| | | checkResourcesAreEqual(resource1, getTestUser1(12345)); |
| | | |
| | | /* |
| | |
| | | assertThat(requests).hasSize(1); |
| | | assertThat(requests.get(0)).isInstanceOf(SearchRequest.class); |
| | | |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1(12345)); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchAddOptionalAttribute() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1(12345); |
| | | newContent.put("description", asList("one", "two")); |
| | | final ResourceResponse resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", |
| | | "two")))); |
| | | connection.patch(context, |
| | | newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchAddOptionalAttributeIndexAppend() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1(12345); |
| | | newContent.put("description", asList("one", "two")); |
| | | final ResourceResponse resource1 = connection.patch( |
| | | ctx(), newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two"))); |
| | | context, newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two"))); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchConstantAttribute() throws Exception { |
| | | newConnection().patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk")))); |
| | | newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/schemas", asList("junk")))); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchDeleteOptionalAttribute() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", remove("/description"))); |
| | | connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1", remove("/description"))); |
| | | checkResourcesAreEqual(resource1, getTestUser1(12345)); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1(12345)); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchIncrement() throws Exception { |
| | | final Context context = newAuthConnectionContext(); |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1(12345); |
| | | newContent.put("singleNumber", 100); |
| | | newContent.put("multiNumber", asList(200, 300)); |
| | | |
| | | final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", |
| | | final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1", |
| | | add("/singleNumber", 0), |
| | | add("/multiNumber", asList(100, 200)), |
| | | increment("/singleNumber", 100), |
| | | increment("/multiNumber", 100))); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchMissingRequiredAttribute() throws Exception { |
| | | newConnection().patch(ctx(), newPatchRequest("/test1", remove("/name/surname"))); |
| | | newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", remove("/name/surname"))); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchModifyOptionalAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | final Context context = newAuthConnectionContext(); |
| | | connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two")))); |
| | | final ResourceResponse resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("three")))); |
| | | connection.patch(context, newPatchRequest("/test1", add("/description", asList("three")))); |
| | | final JsonValue newContent = getTestUser1(12345); |
| | | newContent.put("description", asList("one", "two", "three")); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotSupportedException.class) |
| | | public void testPatchMultiValuedAttributeIndexAppend() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description/0", "junk"))); |
| | | connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/0", "junk"))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchMultiValuedAttributeIndexAppendWithList() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description/-", |
| | | connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/-", |
| | | asList("one", "two")))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchMultiValuedAttributeWithSingleValue() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description", "one"))); |
| | | connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description", "one"))); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchMVCCMatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(); |
| | | final ResourceResponse resource1 = connection.patch( |
| | | ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345")); |
| | | context, newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345")); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | | @Test(expectedExceptions = PreconditionFailedException.class) |
| | | public void testPatchMVCCNoMatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346")); |
| | | connection.patch( |
| | | newAuthConnectionContext(), |
| | | newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346")); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testPatchNotFound() throws Exception { |
| | | newConnection().patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed"))); |
| | | newConnection().patch( |
| | | newAuthConnectionContext(), |
| | | newPatchRequest("/missing", add("/name/displayName", "changed"))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchReadOnlyAttribute() throws Exception { |
| | | // Etag is read-only. |
| | | newConnection().patch(ctx(), newPatchRequest("/test1", add("_rev", "99999"))); |
| | | newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("_rev", "99999"))); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchReplacePartialObject() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(); |
| | | final JsonValue expected = json(object( |
| | | field("schemas", asList("urn:scim:schemas:core:1.0")), |
| | | field("_id", "test1"), |
| | | field("_rev", "12345"), |
| | | field("name", object(field("displayName", "Humpty"), |
| | | field("surname", "Dumpty"))))); |
| | | final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", |
| | | final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1", |
| | | replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty"))))); |
| | | checkResourcesAreEqual(resource1, expected); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, expected); |
| | | } |
| | | |
| | | @Test |
| | | public void testPatchReplaceWholeObject() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(); |
| | | final JsonValue newContent = json(object( |
| | | field("name", object(field("displayName", "Humpty"), |
| | | field("surname", "Dumpty"))))); |
| | |
| | | field("name", object(field("displayName", "Humpty"), |
| | | field("surname", "Dumpty"))))); |
| | | final ResourceResponse resource1 = |
| | | connection.patch(ctx(), newPatchRequest("/test1", replace("/", newContent))); |
| | | connection.patch(context, newPatchRequest("/test1", replace("/", newContent))); |
| | | checkResourcesAreEqual(resource1, expected); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, expected); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchSingleValuedAttributeIndexAppend() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname/-", "junk"))); |
| | | connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname/-", "junk"))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotSupportedException.class) |
| | | public void testPatchSingleValuedAttributeIndexNumber() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname/0", "junk"))); |
| | | connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname/0", "junk"))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname", asList("black", |
| | | connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname", asList("black", |
| | | "white")))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchUnknownAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/dummy", "junk"))); |
| | | connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/dummy", "junk"))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchUnknownSubAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy", "junk"))); |
| | | connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/dummy", "junk"))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testPatchUnknownSubSubAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy/dummy", "junk"))); |
| | | connection.patch(newAuthConnectionContext(), |
| | | newPatchRequest("/test1", add("/description/dummy/dummy", "junk"))); |
| | | } |
| | | |
| | | @Test |
| | | public void testRead() throws Exception { |
| | | final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource = newConnection().read(newAuthConnectionContext(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testReadNotFound() throws Exception { |
| | | newConnection().read(ctx(), newReadRequest("/missing")); |
| | | newConnection().read(newAuthConnectionContext(), newReadRequest("/missing")); |
| | | } |
| | | |
| | | @Test |
| | | public void testReadSelectAllFields() throws Exception { |
| | | final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1").addField("/")); |
| | | final ResourceResponse resource = newConnection().read(newAuthConnectionContext(), |
| | | newReadRequest("/test1").addField("/")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | } |
| | | |
| | | @Test |
| | | public void testReadSelectPartial() throws Exception { |
| | | final ResourceResponse resource = newConnection().read( |
| | | ctx(), newReadRequest("/test1").addField("/name/surname")); |
| | | newAuthConnectionContext(), newReadRequest("/test1").addField("/name/surname")); |
| | | assertThat(resource.getId()).isEqualTo("test1"); |
| | | assertThat(resource.getRevision()).isEqualTo("12345"); |
| | | assertThat(resource.getContent().get("_id").asString()).isNull(); |
| | |
| | | @Test(enabled = false) |
| | | public void testReadSelectPartialInsensitive() throws Exception { |
| | | final ResourceResponse resource = newConnection().read( |
| | | ctx(), newReadRequest("/test1").addField("/name/SURNAME")); |
| | | newAuthConnectionContext(), newReadRequest("/test1").addField("/name/SURNAME")); |
| | | assertThat(resource.getId()).isEqualTo("test1"); |
| | | assertThat(resource.getRevision()).isEqualTo("12345"); |
| | | assertThat(resource.getContent().get("_id").asString()).isNull(); |
| | |
| | | @Test |
| | | public void testUpdate() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(); |
| | | final ResourceResponse resource1 = connection.update( |
| | | ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))); |
| | | context, newUpdateRequest("/test1", getTestUser1Updated(12345))); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | |
| | | public void testUpdateNoChange() throws Exception { |
| | | final List<Request> requests = new LinkedList<>(); |
| | | final Connection connection = newConnection(requests); |
| | | final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345))); |
| | | final Context context = newAuthConnectionContext(requests); |
| | | final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", getTestUser1(12345))); |
| | | |
| | | // Check that no modify operation was sent |
| | | // (only a single search should be sent in order to get the current resource). |
| | |
| | | assertThat(requests.get(0)).isInstanceOf(SearchRequest.class); |
| | | |
| | | checkResourcesAreEqual(resource1, getTestUser1(12345)); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1(12345)); |
| | | } |
| | | |
| | | @Test |
| | | public void testUpdateAddOptionalAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(); |
| | | final JsonValue newContent = getTestUser1Updated(12345); |
| | | newContent.put("description", asList("one", "two")); |
| | | final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent)); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1Updated(12345); |
| | | newContent.put("schemas", asList("junk")); |
| | | connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); |
| | | } |
| | | |
| | | @Test |
| | | public void testUpdateDeleteOptionalAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(); |
| | | final JsonValue newContent = getTestUser1Updated(12345); |
| | | newContent.put("description", asList("one", "two")); |
| | | connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); |
| | | newContent.remove("description"); |
| | | final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent)); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1Updated(12345); |
| | | newContent.get("name").remove("surname"); |
| | | connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); |
| | | } |
| | | |
| | | @Test |
| | | public void testUpdateModifyOptionalAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(); |
| | | final JsonValue newContent = getTestUser1Updated(12345); |
| | | newContent.put("description", asList("one", "two")); |
| | | connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); |
| | | newContent.put("description", asList("three")); |
| | | final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent)); |
| | | checkResourcesAreEqual(resource1, newContent); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, newContent); |
| | | } |
| | | |
| | | @Test |
| | | public void testUpdateMVCCMatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | final Context context = newAuthConnectionContext(); |
| | | final ResourceResponse resource1 = |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345")); |
| | | connection.update(context, newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345")); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | | @Test(expectedExceptions = PreconditionFailedException.class) |
| | | public void testUpdateMVCCNoMatch() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)) |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(12345)) |
| | | .setRevision("12346")); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testUpdateNotFound() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | connection.update(ctx(), newUpdateRequest("/missing", getTestUser1Updated(12345))); |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/missing", getTestUser1Updated(12345))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testUpdateReadOnlyAttribute() throws Exception { |
| | | final Connection connection = newConnection(); |
| | | // Etag is read-only. |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(99999))); |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(99999))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1Updated(12345); |
| | | newContent.put("surname", asList("black", "white")); |
| | | connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | |
| | | final Connection connection = newConnection(); |
| | | final JsonValue newContent = getTestUser1Updated(12345); |
| | | newContent.add("dummy", "junk"); |
| | | connection.update(ctx(), newUpdateRequest("/test1", newContent)); |
| | | connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent)); |
| | | } |
| | | |
| | | private Connection newConnection() throws IOException { |
| | |
| | | |
| | | private Builder builder(final List<Request> requests) throws IOException { |
| | | return Rest2LDAP.builder() |
| | | .ldapConnectionFactory(getConnectionFactory(requests)) |
| | | .baseDN("dc=test") |
| | | .useEtagAttribute() |
| | | .useClientDNNaming("uid") |
| | | .readOnUpdatePolicy(ReadOnUpdatePolicy.CONTROLS) |
| | | .authorizationPolicy(AuthorizationPolicy.NONE) |
| | | .additionalLDAPAttribute("objectClass", "top", "person") |
| | | .mapper(object() |
| | | .attribute("schemas", constant(asList("urn:scim:schemas:core:1.0"))) |
| | |
| | | expectedResource.getContent().getObject()); |
| | | } |
| | | |
| | | private AuthenticatedConnectionContext newAuthConnectionContext() throws LdapException, IOException { |
| | | return newAuthConnectionContext(new ArrayList<Request>()); |
| | | } |
| | | |
| | | private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests) |
| | | throws LdapException, IOException { |
| | | return new AuthenticatedConnectionContext(ctx(), getConnectionFactory(requests).getConnection()); |
| | | } |
| | | |
| | | private ConnectionFactory getConnectionFactory(final List<Request> requests) throws IOException { |
| | | // @formatter:off |
| | | final MemoryBackend backend = |
| File was renamed from opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java |
| | |
| | | * |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | package org.forgerock.opendj.rest2ldap.authz; |
| | | |
| | | import static org.fest.assertions.Assertions.assertThat; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | import org.forgerock.json.resource.ForbiddenException; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.testng.annotations.DataProvider; |
| | |
| | | |
| | | } |
| | | |
| | | @Test(dataProvider = "invalidTemplateData", expectedExceptions = ForbiddenException.class) |
| | | @Test(dataProvider = "invalidTemplateData", expectedExceptions = IllegalArgumentException.class) |
| | | public void testInvalidTemplateData(final String template, Map<String, Object> principals) |
| | | throws Exception { |
| | | new AuthzIdTemplate(template).formatAsAuthzId(principals, Schema.getDefaultSchema()); |
| 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.authz; |
| | | |
| | | import static org.fest.assertions.Assertions.*; |
| | | import static org.forgerock.opendj.ldap.LdapException.*; |
| | | import static org.forgerock.opendj.ldap.ResultCode.*; |
| | | import static org.mockito.Matchers.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | import java.util.concurrent.ExecutionException; |
| | | |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.http.protocol.Status; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.RootContext; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.testng.annotations.BeforeMethod; |
| | | import org.testng.annotations.Test; |
| | | |
| | | @Test |
| | | public class DirectConnectionFilterTest extends ForgeRockTestCase { |
| | | |
| | | private ConnectionFactory connectionFactory; |
| | | private Connection connection; |
| | | private DirectConnectionFilter filter; |
| | | |
| | | @BeforeMethod |
| | | public void setUp() { |
| | | connectionFactory = mock(ConnectionFactory.class); |
| | | connection = mock(Connection.class); |
| | | filter = new DirectConnectionFilter(connectionFactory); |
| | | } |
| | | |
| | | @Test |
| | | public void testInjectAuthenticatedConnectionContext() { |
| | | when(connectionFactory.getConnectionAsync()).thenReturn(newResultPromise(connection)); |
| | | final Handler handler = mock(Handler.class); |
| | | final ArgumentCaptor<Context> captureContext = ArgumentCaptor.forClass(Context.class); |
| | | when(handler.handle(captureContext.capture(), any(Request.class))) |
| | | .thenReturn(Response.newResponsePromise(new Response())); |
| | | |
| | | filter.filter(new RootContext(), new Request(), handler); |
| | | |
| | | assertThat(captureContext.getValue().asContext(AuthenticatedConnectionContext.class).getConnection()) |
| | | .isSameAs(connection); |
| | | verify(connection).close(); |
| | | } |
| | | |
| | | @Test |
| | | public void testErrorResponseSentIfCannotGetConnection() throws InterruptedException, ExecutionException { |
| | | when(connectionFactory.getConnectionAsync()).thenReturn(newExceptionPromise(ADMIN_LIMIT_EXCEEDED)); |
| | | |
| | | final Response response = filter.filter(new RootContext(), new Request(), mock(Handler.class)).get(); |
| | | |
| | | assertThat(response.getStatus()).isEqualTo(Status.PAYLOAD_TOO_LARGE); |
| | | } |
| | | |
| | | private static Promise<Connection, LdapException> newResultPromise(Connection connection) { |
| | | return Promises.<Connection, LdapException> newResultPromise(connection); |
| | | } |
| | | |
| | | private static Promise<Connection, LdapException> newExceptionPromise(ResultCode result) { |
| | | return Promises.<Connection, LdapException> newExceptionPromise(newLdapException(result)); |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.fest.assertions.Assertions.*; |
| | | import static org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor.*; |
| | | import static org.mockito.Matchers.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.assertj.core.api.SoftAssertions; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Headers; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.CustomHeaderExtractor; |
| | | import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.forgerock.util.Pair; |
| | | import org.forgerock.util.encode.Base64; |
| | | import org.forgerock.util.promise.Promises; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | @Test |
| | | public class HttpBasicAuthenticationFilterTest extends ForgeRockTestCase { |
| | | |
| | | private static final String USERNAME = "Aladdin"; |
| | | private static final String PASSWORD = "open sesame"; |
| | | private static final String BASE64_USERPASS = Base64.encode((USERNAME + ":" + PASSWORD).getBytes()); |
| | | |
| | | @DataProvider(name = "Invalid HTTP basic auth strings") |
| | | public Object[][] getInvalidHttpBasicAuthStrings() { |
| | | return new Object[][] { { null }, { "bla" }, { "basic " + Base64.encode("la:bli:blu".getBytes()) } }; |
| | | } |
| | | |
| | | @Test(dataProvider = "Invalid HTTP basic auth strings") |
| | | public void parseUsernamePasswordFromInvalidAuthZHeader(String authZHeader) throws Exception { |
| | | final AuthenticationStrategy strategy = mock(AuthenticationStrategy.class); |
| | | final HttpBasicAuthenticationFilter filter = |
| | | new HttpBasicAuthenticationFilter(strategy, HttpBasicExtractor.INSTANCE, false); |
| | | |
| | | final Request req = new Request(); |
| | | req.getHeaders().put(HTTP_BASIC_AUTH_HEADER, authZHeader); |
| | | |
| | | assertThat(filter.canApplyFilter(null, req)).isFalse(); |
| | | } |
| | | |
| | | @DataProvider(name = "Valid HTTP basic auth strings") |
| | | public Object[][] getValidHttpBasicAuthStrings() { |
| | | return new Object[][] { { "basic " + BASE64_USERPASS }, { "Basic " + BASE64_USERPASS } }; |
| | | } |
| | | |
| | | @Test(dataProvider = "Valid HTTP basic auth strings") |
| | | public void parseUsernamePasswordFromValidAuthZHeader(String authZHeader) throws Exception { |
| | | final Headers headers = new Headers(); |
| | | headers.put(HTTP_BASIC_AUTH_HEADER, authZHeader); |
| | | assertThat(HttpBasicExtractor.INSTANCE.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD)); |
| | | } |
| | | |
| | | @Test |
| | | public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent() throws Exception { |
| | | final AuthenticationStrategy failureStrategy = mock(AuthenticationStrategy.class); |
| | | when(failureStrategy |
| | | .authenticate(any(String.class), any(String.class), any(Context.class), any(AtomicReference.class))) |
| | | .thenReturn(Promises.<SecurityContext, LdapException>newResultPromise(null)); |
| | | |
| | | final HttpBasicAuthenticationFilter filter = |
| | | new HttpBasicAuthenticationFilter(failureStrategy, HttpBasicExtractor.INSTANCE, false); |
| | | |
| | | final Response response = filter.filter(null, new Request(), mock(Handler.class)).get(); |
| | | verifyUnauthorizedOutputMessage(response); |
| | | } |
| | | |
| | | private void verifyUnauthorizedOutputMessage(Response response) throws IOException { |
| | | final SoftAssertions softly = new SoftAssertions(); |
| | | softly.assertThat(response.getStatus().getCode()).isEqualTo(401); |
| | | softly.assertThat(response.getStatus().getReasonPhrase()).isEqualTo("Unauthorized"); |
| | | softly.assertThat(response.getEntity().getJson().toString()) |
| | | .isEqualTo("{code=401, reason=Unauthorized, message=Invalid Credentials}"); |
| | | softly.assertAll(); |
| | | } |
| | | |
| | | @Test |
| | | public void extractUsernamePasswordHttpBasicAuthWillAcceptUserAgent() throws Exception { |
| | | final Headers headers = new Headers(); |
| | | headers.add(HTTP_BASIC_AUTH_HEADER, "Basic " + BASE64_USERPASS); |
| | | assertThat(HttpBasicExtractor.INSTANCE.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD)); |
| | | } |
| | | |
| | | @Test |
| | | public void extractUsernamePasswordCustomHeaders() throws Exception { |
| | | final String customHeaderUsername = "X-OpenIDM-Username"; |
| | | final String customHeaderPassword = "X-OpenIDM-Password"; |
| | | CustomHeaderExtractor cha = new CustomHeaderExtractor(customHeaderUsername, customHeaderPassword); |
| | | Headers headers = new Headers(); |
| | | headers.add(customHeaderUsername, USERNAME); |
| | | headers.add(customHeaderPassword, PASSWORD); |
| | | |
| | | assertThat(cha.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD)); |
| | | } |
| | | |
| | | } |
| 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.authz; |
| | | |
| | | import static org.mockito.Matchers.*; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.Condition; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.RootContext; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.testng.annotations.BeforeMethod; |
| | | import org.testng.annotations.Test; |
| | | |
| | | @Test |
| | | public class OptionalFilterTest extends ForgeRockTestCase { |
| | | |
| | | private OptionalFilter optionalFilter; |
| | | private Filter filter; |
| | | private Condition condition; |
| | | |
| | | @BeforeMethod |
| | | public void setUp() { |
| | | filter = mock(Filter.class); |
| | | condition = mock(Condition.class); |
| | | optionalFilter = new OptionalFilter(filter, condition); |
| | | } |
| | | |
| | | @Test |
| | | public void testFilterNotAppliedIfConditionIsFalse() { |
| | | when(condition.canApplyFilter(any(Context.class), any(Request.class))).thenReturn(false); |
| | | |
| | | optionalFilter.filter(new RootContext(), new Request(), mock(Handler.class)); |
| | | |
| | | verify(filter, never()).filter(any(RootContext.class), any(Request.class), any(Handler.class)); |
| | | } |
| | | |
| | | @Test |
| | | public void testFilterAppliedIfConditionIsTrue() { |
| | | when(condition.canApplyFilter(any(Context.class), any(Request.class))).thenReturn(true); |
| | | |
| | | final Context context = new RootContext(); |
| | | final Request request = new Request(); |
| | | final Handler handler = mock(Handler.class); |
| | | optionalFilter.filter(context, request, handler); |
| | | |
| | | verify(filter).filter(same(context), same(request), same(handler)); |
| | | } |
| | | } |
| 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.authz; |
| | | |
| | | import static org.fest.assertions.Assertions.assertThat; |
| | | import static org.mockito.Matchers.any; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.opendj.ldap.AbstractSynchronousConnection; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionEventListener; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.DecodeOptions; |
| | | import org.forgerock.opendj.ldap.IntermediateResponseHandler; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl; |
| | | 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.DeleteRequest; |
| | | 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.Requests; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.requests.UnbindRequest; |
| | | 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.Responses; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.RootContext; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.forgerock.util.promise.Promises; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.testng.annotations.BeforeMethod; |
| | | import org.testng.annotations.Test; |
| | | |
| | | @Test |
| | | public class ProxiedAuthV2FilterTest extends ForgeRockTestCase { |
| | | |
| | | private ProxiedAuthV2Filter filter; |
| | | private ArgumentCaptor<Context> captureContext; |
| | | private Handler handler; |
| | | |
| | | @BeforeMethod |
| | | public void setUp() { |
| | | captureContext = ArgumentCaptor.forClass(Context.class); |
| | | handler = mock(Handler.class); |
| | | } |
| | | |
| | | @Test |
| | | public void testConnectionIsUsingProxiedAuthControlOnRequests() throws LdapException { |
| | | final ConnectionFactory connectionFactory = mock(ConnectionFactory.class); |
| | | when(connectionFactory.getConnectionAsync()) |
| | | .thenReturn(Promises.<Connection, LdapException> newResultPromise(new CheckConnection() { |
| | | @Override |
| | | void verifyRequest(org.forgerock.opendj.ldap.requests.Request request) throws LdapException { |
| | | assertThat(request.getControls()).isNotEmpty(); |
| | | final ProxiedAuthV2RequestControl control; |
| | | try { |
| | | control = request.getControl(ProxiedAuthV2RequestControl.DECODER, new DecodeOptions()); |
| | | } catch (DecodeException e) { |
| | | throw LdapException.newLdapException(ResultCode.OPERATIONS_ERROR); |
| | | } |
| | | assertThat(control.getValue()) |
| | | .isEqualTo(ByteString.valueOfUtf8("dn:uid=whatever,ou=people,dc=com")); |
| | | } |
| | | })); |
| | | filter = new ProxiedAuthV2Filter(connectionFactory, ProxiedAuthV2Filter.IntrospectionAuthzProvider.INSTANCE); |
| | | |
| | | final Map<String, Object> authz = new HashMap<>(); |
| | | authz.put(SecurityContext.AUTHZID_DN, "uid=whatever,ou=people,dc=com"); |
| | | final SecurityContext securityContext = new SecurityContext(new RootContext(), "whatever", authz); |
| | | |
| | | when(handler.handle(captureContext.capture(), any(Request.class))) |
| | | .thenReturn(Response.newResponsePromise(new Response())); |
| | | filter.filter(securityContext, new Request(), handler); |
| | | |
| | | final Connection proxiedConnection = captureContext.getValue().asContext(AuthenticatedConnectionContext.class) |
| | | .getConnection(); |
| | | proxiedConnection.add(Requests.newAddRequest("cn=test")); |
| | | proxiedConnection.applyChange(Requests.newChangeRecord("dn: cn=test", "changetype: delete")); |
| | | proxiedConnection.compare(Requests.newCompareRequest("cn=test", "foo", "bar")); |
| | | proxiedConnection.search(Requests.newSearchRequest("cn=test", SearchScope.BASE_OBJECT, "(cn=test)", "")); |
| | | proxiedConnection.modify(Requests.newModifyRequest("cn=test")); |
| | | proxiedConnection.delete("cn=test"); |
| | | proxiedConnection.deleteSubtree("cn=test"); |
| | | proxiedConnection.extendedRequest("blah", ByteString.empty()); |
| | | proxiedConnection.modifyDN("cn=foo", "cn=bar"); |
| | | } |
| | | |
| | | private abstract static class CheckConnection extends AbstractSynchronousConnection { |
| | | |
| | | abstract void verifyRequest(org.forgerock.opendj.ldap.requests.Request request) throws LdapException; |
| | | |
| | | @Override |
| | | public Result add(AddRequest request) throws LdapException { |
| | | verifyRequest(request); |
| | | return Responses.newResult(ResultCode.SUCCESS); |
| | | } |
| | | |
| | | @Override |
| | | public BindResult bind(BindRequest request) throws LdapException { |
| | | return Responses.newBindResult(ResultCode.SUCCESS); |
| | | } |
| | | |
| | | @Override |
| | | public void close(UnbindRequest request, String reason) { |
| | | } |
| | | |
| | | @Override |
| | | public CompareResult compare(CompareRequest request) throws LdapException { |
| | | verifyRequest(request); |
| | | return Responses.newCompareResult(ResultCode.SUCCESS); |
| | | } |
| | | |
| | | @Override |
| | | public Result delete(DeleteRequest request) throws LdapException { |
| | | verifyRequest(request); |
| | | return Responses.newResult(ResultCode.SUCCESS); |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | @Override |
| | | public <R extends ExtendedResult> R extendedRequest(ExtendedRequest<R> request, |
| | | IntermediateResponseHandler handler) throws LdapException { |
| | | verifyRequest(request); |
| | | return (R) Responses.newGenericExtendedResult(ResultCode.SUCCESS); |
| | | } |
| | | |
| | | @Override |
| | | public Result search(SearchRequest request, SearchResultHandler handler) throws LdapException { |
| | | verifyRequest(request); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Result modify(ModifyRequest request) throws LdapException { |
| | | verifyRequest(request); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Result modifyDN(ModifyDNRequest request) throws LdapException { |
| | | verifyRequest(request); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public void addConnectionEventListener(ConnectionEventListener listener) { |
| | | } |
| | | |
| | | @Override |
| | | public boolean isClosed() { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public boolean isValid() { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public void removeConnectionEventListener(ConnectionEventListener listener) { |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return null; |
| | | } |
| | | |
| | | } |
| | | } |
| | |
| | | { |
| | | // The Rest2LDAP authentication filter configuration. The filter will be |
| | | // disabled if the configuration is not present. Upon successful |
| | | // authentication the filter will create a security context containing the |
| | | // following principals: |
| | | // |
| | | // "dn" - the DN of the user if known (may not be the case for sasl-plain) |
| | | // "id" - the username used for authentication. |
| | | "authenticationFilter" : { |
| | | // Indicates whether the filter should allow HTTP BASIC authentication. |
| | | "supportHTTPBasicAuthentication" : true, |
| | | "authorization": { |
| | | // The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2". |
| | | "policies": [ "basic" ], |
| | | |
| | | // Perform all operations using a pre-authorization connection. |
| | | "anonymous": { |
| | | // Specify the connection factory to use to perform LDAP operations. |
| | | // If missing, "root" factory will be used. |
| | | "ldapConnectionFactory": "root", |
| | | |
| | | // Enable proxied authorization using the specified user DN. |
| | | // If empty, anonymous proxied authorization will be used. |
| | | // If missing, connection from the ldapConnectionFactory will be used as-is. |
| | | "userDN": "" |
| | | }, |
| | | |
| | | // Indicates whether the filter should allow alternative authentication |
| | | // and, if so, which HTTP headers it should obtain the username and |
| | | // password from. |
| | | "supportAltAuthentication" : true, |
| | | "altAuthenticationUsernameHeader" : "X-OpenIDM-Username", |
| | | "altAuthenticationPasswordHeader" : "X-OpenIDM-Password", |
| | | // Use HTTP Basic authentication's information to bind to the LDAP server. |
| | | "basic": { |
| | | // Indicates whether the filter should allow alternative authentication |
| | | // and, if so, which HTTP headers it should obtain the username and |
| | | // password from. |
| | | "supportAltAuthentication" : true, |
| | | "altAuthenticationUsernameHeader" : "X-OpenIDM-Username", |
| | | "altAuthenticationPasswordHeader" : "X-OpenIDM-Password", |
| | | |
| | | // Define which LDAP bind mechanism to use |
| | | // Supported mechanisms are "simple", "sasl-plain", "search" |
| | | "bind": "search", |
| | | |
| | | // Bind to the LDAP server using the DN built from the HTTP Basic's username |
| | | "simple": { |
| | | // Connection factory used to perform the bind operation. |
| | | // If missing, "bind" factory will be used. |
| | | "ldapConnectionFactory": "bind", |
| | | |
| | | // The Bind DN Template containing a single %s which will be replaced by the authenticating |
| | | // user's name. (i.e: uid=%s,ou=People,dc=example,dc=com) |
| | | // If missing, "%s" is used. |
| | | "bindDNTemplate": "uid=%s,ou=People,dc=example,dc=com" |
| | | }, |
| | | |
| | | // Bind to the LDAP server using a SASL Plain request |
| | | "sasl-plain": { |
| | | // Connection factory used to perform the bind operation. |
| | | // If missing, "bind" factory will be used. |
| | | "ldapConnectionFactory": "bind", |
| | | |
| | | // Authentication identity template containing a single %s which will be replaced by the authenticating |
| | | // user's name. (i.e: u:%s) |
| | | "authcIdTemplate": "u:%s" |
| | | }, |
| | | |
| | | // Bind to the LDAP server using the resulting DN of a search request. |
| | | "search": { |
| | | // Connection factory used to perform the search operation. |
| | | // If missing, "root" factory will be used. |
| | | "searchLDAPConnectionFactory": "root", |
| | | |
| | | // Connection factory used to perform the bind operation. |
| | | // If missing, "bind" factory will be used. |
| | | "bindLDAPConnectionFactory": "bind", |
| | | |
| | | // The %s filter format parameters will be substituted with the client-provided username, |
| | | // using LDAP filter string character escaping. |
| | | "baseDN" : "ou=people,dc=example,dc=com", |
| | | "scope" : "sub", // Or "one". |
| | | "filterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))" |
| | | } |
| | | // TODO: support for HTTP sessions? |
| | | } |
| | | }, |
| | | |
| | | // The search parameters to use for "search-simple" authentication. The |
| | | // %s filter format parameters will be substituted with the |
| | | // client-provided username, using LDAP filter string character escaping. |
| | | "searchBaseDN" : "ou=people,dc=example,dc=com", |
| | | "searchScope" : "sub", // Or "one". |
| | | "searchFilterTemplate" : "(&(uid=%s)(objectClass=inetOrgPerson))" |
| | | |
| | | // TODO: support for HTTP sessions? |
| | | }, |
| | | |
| | | // The Rest2LDAP Servlet configuration. |
| | | "servlet" : { |
| | | // 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" } } |
| | | } } |
| | | } |
| | | // 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" |
| | | }, |
| | | "/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" } } |
| | | } } |
| | | "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" } } |
| | | } } |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | import java.net.InetAddress; |
| | | import java.net.UnknownHostException; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.opendj.ldap.AbstractSynchronousConnection; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | |
| | | import org.forgerock.opendj.ldap.responses.Responses; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.PromiseImpl; |
| | | import org.opends.server.core.AddOperation; |
| | | import org.opends.server.core.BindOperation; |
| | | import org.opends.server.core.CompareOperation; |
| | | import org.opends.server.core.DeleteOperation; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.core.ExtendedOperation; |
| | | import org.opends.server.protocols.internal.InternalClientConnection; |
| | | import org.opends.server.protocols.internal.InternalSearchListener; |
| | | import org.opends.server.protocols.internal.InternalSearchOperation; |
| | | import org.opends.server.protocols.internal.Requests; |
| | | import org.opends.server.types.AuthenticationInfo; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.OperationType; |
| | | import org.opends.server.types.SearchFilter; |
| | | import org.opends.server.types.SearchResultEntry; |
| | | import org.opends.server.types.SearchResultReference; |
| | |
| | | import static org.forgerock.opendj.adapter.server3x.Converters.*; |
| | | import static org.forgerock.opendj.ldap.ByteString.*; |
| | | import static org.forgerock.opendj.ldap.LdapException.*; |
| | | import static org.forgerock.util.promise.Promises.*; |
| | | |
| | | /** This class provides a connection factory and an adapter for the OpenDJ 2.x server. */ |
| | | public final class Adapters { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Returns a new anonymous connection factory. |
| | | * |
| | | * @return A new anonymous connection factory. |
| | | */ |
| | | public static ConnectionFactory newAnonymousConnectionFactory() { |
| | | InternalClientConnection icc = new InternalClientConnection(new AuthenticationInfo()); |
| | | return newConnectionFactory(icc); |
| | | } |
| | | |
| | | /** |
| | | * Returns a new connection factory for a specified user. |
| | | * |
| | | * @param userDN |
| | | * The specified user's DN. |
| | | * @return a new connection factory. |
| | | */ |
| | | public static ConnectionFactory newConnectionFactoryForUser(final DN userDN) { |
| | | InternalClientConnection icc = null; |
| | | try { |
| | | icc = new InternalClientConnection(userDN); |
| | | } catch (DirectoryException e) { |
| | | throw new IllegalStateException(e.getMessage(), e); |
| | | } |
| | | return newConnectionFactory(icc); |
| | | } |
| | | |
| | | /** |
| | | * Returns a new connection factory. |
| | | * |
| | | * @param icc |
| | |
| | | */ |
| | | public static ConnectionFactory newConnectionFactory(final InternalClientConnection icc) { |
| | | return new ConnectionFactory() { |
| | | |
| | | @Override |
| | | public void close() { |
| | | // Nothing to do. |
| | |
| | | |
| | | @Override |
| | | public Promise<Connection, LdapException> getConnectionAsync() { |
| | | // TODO change the path... |
| | | return newResultPromise(newConnection(icc)); |
| | | final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create(); |
| | | try |
| | | { |
| | | DirectoryServer.getWorkQueue().submitOperation(new AsyncOperation<>(icc, new Runnable() |
| | | { |
| | | @Override |
| | | public void run() |
| | | { |
| | | try |
| | | { |
| | | promise.handleResult(getConnection()); |
| | | } |
| | | catch (LdapException e) |
| | | { |
| | | promise.handleException(e); |
| | | } |
| | | } |
| | | })); |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | promise.handleException(LdapException.newLdapException(e.getResultCode())); |
| | | } |
| | | return promise; |
| | | } |
| | | |
| | | @Override |
| | |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Returns a new root connection. |
| | | * |
| | | * @return A new root connection. |
| | | */ |
| | | public static Connection newRootConnection() { |
| | | return newConnection(InternalClientConnection.getRootConnection()); |
| | | } |
| | | |
| | | /** |
| | | * Returns a new connection for an anonymous user. |
| | | * |
| | | * @return A new connection. |
| | | */ |
| | | public static Connection newAnonymousConnection() { |
| | | return newConnection(new InternalClientConnection(new AuthenticationInfo())); |
| | | } |
| | | |
| | | /** |
| | | * Returns a new connection for a specified user. |
| | | * |
| | | * @param dn |
| | | * The DN of the user. |
| | | * @return A new connection for a specified user. |
| | | * @throws LdapException |
| | | * If no such object. |
| | | */ |
| | | public static Connection newConnectionForUser(final DN dn) throws LdapException { |
| | | try { |
| | | return newConnection(new InternalClientConnection(dn)); |
| | | } catch (DirectoryException e) { |
| | | throw newLdapException(Responses.newResult(ResultCode.NO_SUCH_OBJECT)); |
| | | } |
| | | } |
| | | |
| | | private static Connection newConnection(final InternalClientConnection icc) { |
| | | return new AbstractSynchronousConnection() { |
| | | return new AbstractSynchronousConnection() { |
| | | |
| | | @Override |
| | | public Result search(final SearchRequest request, final SearchResultHandler handler) |
| | | throws LdapException { |
| | | InternalSearchListener internalSearchListener = new InternalSearchListener() { |
| | | @Override |
| | | public Result search(final SearchRequest request, final SearchResultHandler handler) |
| | | throws LdapException { |
| | | InternalSearchListener internalSearchListener = new InternalSearchListener() { |
| | | |
| | | @Override |
| | | public void handleInternalSearchReference( |
| | | InternalSearchOperation searchOperation, |
| | | SearchResultReference searchReference) throws DirectoryException { |
| | | handler.handleReference(from(searchReference)); |
| | | } |
| | | @Override |
| | | public void handleInternalSearchReference( |
| | | InternalSearchOperation searchOperation, |
| | | SearchResultReference searchReference) throws DirectoryException { |
| | | handler.handleReference(from(searchReference)); |
| | | } |
| | | |
| | | @Override |
| | | public void handleInternalSearchEntry(InternalSearchOperation searchOperation, |
| | | SearchResultEntry searchEntry) throws DirectoryException { |
| | | handler.handleEntry(from(searchEntry)); |
| | | } |
| | | }; |
| | | @Override |
| | | public void handleInternalSearchEntry(InternalSearchOperation searchOperation, |
| | | SearchResultEntry searchEntry) throws DirectoryException { |
| | | handler.handleEntry(from(searchEntry)); |
| | | } |
| | | }; |
| | | |
| | | final SearchFilter filter = toSearchFilter(request.getFilter()); |
| | | final org.opends.server.protocols.internal.SearchRequest sr = |
| | | Requests.newSearchRequest(request.getName(), request.getScope(), filter) |
| | | .setDereferenceAliasesPolicy(request.getDereferenceAliasesPolicy()) |
| | | .setSizeLimit(request.getSizeLimit()) |
| | | .setTimeLimit(request.getTimeLimit()) |
| | | .setTypesOnly(request.isTypesOnly()) |
| | | .addAttribute(request.getAttributes()) |
| | | .addControl(to(request.getControls())); |
| | | return getResponseResult(icc.processSearch(sr, internalSearchListener)); |
| | | } |
| | | final SearchFilter filter = toSearchFilter(request.getFilter()); |
| | | final org.opends.server.protocols.internal.SearchRequest sr = |
| | | Requests.newSearchRequest(request.getName(), request.getScope(), filter) |
| | | .setDereferenceAliasesPolicy(request.getDereferenceAliasesPolicy()) |
| | | .setSizeLimit(request.getSizeLimit()) |
| | | .setTimeLimit(request.getTimeLimit()) |
| | | .setTypesOnly(request.isTypesOnly()) |
| | | .addAttribute(request.getAttributes()) |
| | | .addControl(to(request.getControls())); |
| | | return getResponseResult(icc.processSearch(sr, internalSearchListener)); |
| | | } |
| | | |
| | | @Override |
| | | public void removeConnectionEventListener(ConnectionEventListener listener) { |
| | | // Internal client connection don't have any connection events. |
| | | } |
| | | @Override |
| | | public void removeConnectionEventListener(ConnectionEventListener listener) { |
| | | // Internal client connection don't have any connection events. |
| | | } |
| | | |
| | | @Override |
| | | public Result modifyDN(final ModifyDNRequest request) throws LdapException { |
| | | return getResponseResult(icc.processModifyDN(request)); |
| | | } |
| | | @Override |
| | | public Result modifyDN(final ModifyDNRequest request) throws LdapException { |
| | | return getResponseResult(icc.processModifyDN(request)); |
| | | } |
| | | |
| | | @Override |
| | | public Result modify(final ModifyRequest request) throws LdapException { |
| | | return getResponseResult(icc.processModify(request)); |
| | | } |
| | | @Override |
| | | public Result modify(final ModifyRequest request) throws LdapException { |
| | | return getResponseResult(icc.processModify(request)); |
| | | } |
| | | |
| | | @Override |
| | | public boolean isValid() { |
| | | // Always true. |
| | | return true; |
| | | } |
| | | @Override |
| | | public boolean isValid() { |
| | | // Always true. |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean isClosed() { |
| | | return false; |
| | | } |
| | | @Override |
| | | public boolean isClosed() { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request, |
| | | final IntermediateResponseHandler handler) throws LdapException { |
| | | @Override |
| | | public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request, |
| | | final IntermediateResponseHandler handler) throws LdapException { |
| | | |
| | | final ExtendedOperation extendedOperation = |
| | | icc.processExtendedOperation(request.getOID(), request.getValue(), |
| | | to(request.getControls())); |
| | | final ExtendedOperation extendedOperation = |
| | | icc.processExtendedOperation(request.getOID(), request.getValue(), |
| | | to(request.getControls())); |
| | | |
| | | final Result result = getResponseResult(extendedOperation); |
| | | final GenericExtendedResult genericExtendedResult = |
| | | Responses.newGenericExtendedResult(result.getResultCode()) |
| | | .setDiagnosticMessage(result.getDiagnosticMessage()).setMatchedDN( |
| | | result.getMatchedDN()).setValue( |
| | | extendedOperation.getResponseValue().toByteString()); |
| | | try { |
| | | R extendedResult = |
| | | request.getResultDecoder().decodeExtendedResult(genericExtendedResult, |
| | | new DecodeOptions()); |
| | | for (final Control control : result.getControls()) { |
| | | extendedResult.addControl(control); |
| | | } |
| | | return extendedResult; |
| | | final Result result = getResponseResult(extendedOperation); |
| | | final GenericExtendedResult genericExtendedResult = |
| | | Responses.newGenericExtendedResult(result.getResultCode()) |
| | | .setDiagnosticMessage(result.getDiagnosticMessage()).setMatchedDN( |
| | | result.getMatchedDN()).setValue( |
| | | extendedOperation.getResponseValue().toByteString()); |
| | | try { |
| | | R extendedResult = |
| | | request.getResultDecoder().decodeExtendedResult(genericExtendedResult, |
| | | new DecodeOptions()); |
| | | for (final Control control : result.getControls()) { |
| | | extendedResult.addControl(control); |
| | | } |
| | | return extendedResult; |
| | | |
| | | } catch (DecodeException e) { |
| | | DN matchedDN = extendedOperation.getMatchedDN(); |
| | | return request.getResultDecoder().newExtendedErrorResult( |
| | | extendedOperation.getResultCode(), |
| | | matchedDN != null ? matchedDN.toString() : null, |
| | | extendedOperation.getErrorMessage().toString()); |
| | | } |
| | | } |
| | | } catch (DecodeException e) { |
| | | DN matchedDN = extendedOperation.getMatchedDN(); |
| | | return request.getResultDecoder().newExtendedErrorResult( |
| | | extendedOperation.getResultCode(), |
| | | matchedDN != null ? matchedDN.toString() : null, |
| | | extendedOperation.getErrorMessage().toString()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public Result delete(final DeleteRequest request) throws LdapException { |
| | | final DeleteOperation deleteOperation = |
| | | icc.processDelete(valueOfObject(request.getName()), to(request.getControls())); |
| | | return getResponseResult(deleteOperation); |
| | | } |
| | | @Override |
| | | public Result delete(final DeleteRequest request) throws LdapException { |
| | | final DeleteOperation deleteOperation = |
| | | icc.processDelete(valueOfObject(request.getName()), to(request.getControls())); |
| | | return getResponseResult(deleteOperation); |
| | | } |
| | | |
| | | @Override |
| | | public CompareResult compare(final CompareRequest request) throws LdapException { |
| | | final CompareOperation compareOperation = |
| | | icc.processCompare(valueOfObject(request.getName()), |
| | | request.getAttributeDescription().toString(), |
| | | request.getAssertionValue(), to(request.getControls())); |
| | | @Override |
| | | public CompareResult compare(final CompareRequest request) throws LdapException { |
| | | final CompareOperation compareOperation = |
| | | icc.processCompare(valueOfObject(request.getName()), |
| | | request.getAttributeDescription().toString(), |
| | | request.getAssertionValue(), to(request.getControls())); |
| | | |
| | | CompareResult result = Responses.newCompareResult(compareOperation.getResultCode()); |
| | | return getResponseResult(compareOperation, result); |
| | | } |
| | | CompareResult result = Responses.newCompareResult(compareOperation.getResultCode()); |
| | | return getResponseResult(compareOperation, result); |
| | | } |
| | | |
| | | @Override |
| | | public void close(final UnbindRequest request, final String reason) { |
| | | // no implementation in open-ds. |
| | | } |
| | | @Override |
| | | public void close(final UnbindRequest request, final String reason) { |
| | | // no implementation in open-ds. |
| | | } |
| | | |
| | | @Override |
| | | public BindResult bind(final BindRequest request) throws LdapException { |
| | | BindOperation bindOperation = null; |
| | | if (request instanceof SimpleBindRequest) { |
| | | bindOperation = |
| | | icc.processSimpleBind(valueOfUtf8(request.getName()), |
| | | ByteString.wrap(((SimpleBindRequest) request).getPassword()), |
| | | to(request.getControls())); |
| | | } else if (request instanceof SASLBindRequest) { |
| | | String serverName = null; |
| | | try { |
| | | serverName = InetAddress.getByName(null).getCanonicalHostName(); |
| | | } catch (UnknownHostException e) { |
| | | // nothing to do. |
| | | } |
| | | BindClient bindClient = request.createBindClient(serverName); |
| | | do { |
| | | final GenericBindRequest genericBindRequest = bindClient.nextBindRequest(); |
| | | bindOperation = |
| | | icc.processSASLBind( |
| | | valueOfUtf8(request.getName()), |
| | | ((SASLBindRequest) request).getSASLMechanism(), |
| | | getCredentials(genericBindRequest.getAuthenticationValue()), |
| | | to(request.getControls())); |
| | | } while (bindOperation.getResultCode() == ResultCode.SASL_BIND_IN_PROGRESS); |
| | | @Override |
| | | public BindResult bind(final BindRequest request) throws LdapException { |
| | | BindOperation bindOperation = null; |
| | | if (request instanceof SimpleBindRequest) { |
| | | bindOperation = |
| | | icc.processSimpleBind(valueOfUtf8(request.getName()), |
| | | ByteString.wrap(((SimpleBindRequest) request).getPassword()), |
| | | to(request.getControls())); |
| | | } else if (request instanceof SASLBindRequest) { |
| | | String serverName = null; |
| | | try { |
| | | serverName = InetAddress.getByName(null).getCanonicalHostName(); |
| | | } catch (UnknownHostException e) { |
| | | // nothing to do. |
| | | } |
| | | BindClient bindClient = request.createBindClient(serverName); |
| | | do { |
| | | final GenericBindRequest genericBindRequest = bindClient.nextBindRequest(); |
| | | bindOperation = |
| | | icc.processSASLBind( |
| | | valueOfUtf8(request.getName()), |
| | | ((SASLBindRequest) request).getSASLMechanism(), |
| | | getCredentials(genericBindRequest.getAuthenticationValue()), |
| | | to(request.getControls())); |
| | | } while (bindOperation.getResultCode() == ResultCode.SASL_BIND_IN_PROGRESS); |
| | | |
| | | bindClient.dispose(); |
| | | bindClient.dispose(); |
| | | |
| | | } else { // not supported |
| | | throw newLdapException(Responses.newResult(ResultCode.AUTH_METHOD_NOT_SUPPORTED)); |
| | | } |
| | | BindResult result = Responses.newBindResult(bindOperation.getResultCode()); |
| | | result.setServerSASLCredentials(bindOperation.getSASLCredentials()); |
| | | } else { // not supported |
| | | throw newLdapException(Responses.newResult(ResultCode.AUTH_METHOD_NOT_SUPPORTED)); |
| | | } |
| | | BindResult result = Responses.newBindResult(bindOperation.getResultCode()); |
| | | result.setServerSASLCredentials(bindOperation.getSASLCredentials()); |
| | | |
| | | if (result.isSuccess()) { |
| | | return result; |
| | | } else { |
| | | throw newLdapException(result); |
| | | } |
| | | } |
| | | if (result.isSuccess()) { |
| | | return result; |
| | | } else { |
| | | throw newLdapException(result); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void addConnectionEventListener(ConnectionEventListener listener) { |
| | | // Internal client connection don't have any connection events. |
| | | } |
| | | @Override |
| | | public void addConnectionEventListener(ConnectionEventListener listener) { |
| | | // Internal client connection don't have any connection events. |
| | | } |
| | | |
| | | @Override |
| | | public Result add(final AddRequest request) throws LdapException { |
| | | final AddOperation addOperation = |
| | | icc.processAdd(valueOfObject(request.getName()), to(request |
| | | .getAllAttributes()), to(request.getControls())); |
| | | return getResponseResult(addOperation); |
| | | } |
| | | @Override |
| | | public Result add(final AddRequest request) throws LdapException { |
| | | final AddOperation addOperation = |
| | | icc.processAdd(valueOfObject(request.getName()), to(request |
| | | .getAllAttributes()), to(request.getControls())); |
| | | return getResponseResult(addOperation); |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return icc.toString(); |
| | | } |
| | | }; |
| | | @Override |
| | | public String toString() { |
| | | return icc.toString(); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * This operation is hack to be able to execute a {@link Runnable} in a |
| | | * Directory Server's worker thread. |
| | | */ |
| | | private static final class AsyncOperation<V> extends org.opends.server.types.AbstractOperation |
| | | { |
| | | private final Runnable runnable; |
| | | |
| | | AsyncOperation(InternalClientConnection icc, Runnable runnable) |
| | | { |
| | | super(icc, icc.nextOperationID(), icc.nextMessageID(), Collections.<org.opends.server.types.Control> emptyList()); |
| | | this.setInternalOperation(true); |
| | | this.runnable = runnable; |
| | | } |
| | | |
| | | @Override |
| | | public void run() |
| | | { |
| | | runnable.run(); |
| | | } |
| | | |
| | | @Override |
| | | public OperationType getOperationType() |
| | | { |
| | | return null; |
| | | } |
| | | @Override |
| | | public List<org.opends.server.types.Control> getResponseControls() |
| | | { |
| | | return Collections.emptyList(); |
| | | } |
| | | @Override |
| | | public void addResponseControl(org.opends.server.types.Control control) |
| | | { |
| | | } |
| | | @Override |
| | | public void removeResponseControl(org.opends.server.types.Control control) |
| | | { |
| | | } |
| | | @Override |
| | | public DN getProxiedAuthorizationDN() |
| | | { |
| | | return null; |
| | | } |
| | | @Override |
| | | public void setProxiedAuthorizationDN(DN proxiedAuthorizationDN) |
| | | { |
| | | } |
| | | @Override |
| | | public void toString(StringBuilder buffer) |
| | | { |
| | | buffer.append(AsyncOperation.class.getSimpleName()); |
| | | } |
| | | } |
| | | } |
| | |
| | | import org.forgerock.http.HttpApplicationException; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.opendj.server.config.server.HTTPEndpointCfg; |
| | | import org.opends.server.core.ServerContext; |
| | | import org.opends.server.types.InitializationException; |
| | | |
| | | /** |
| | |
| | | /** Configuration of this endpoint. */ |
| | | protected final C configuration; |
| | | |
| | | /** Context of this LDAP server. */ |
| | | protected final ServerContext serverContext; |
| | | |
| | | /** |
| | | * Create a new {@link HttpEndpoint} with the given configuration. |
| | | * |
| | | * @param configuration |
| | | * Configuration of this {@link HttpEndpoint}. |
| | | * Configuration of this {@link HttpEndpoint} |
| | | * @param serverContext |
| | | * Context of this LDAP server |
| | | */ |
| | | public HttpEndpoint(C configuration) |
| | | public HttpEndpoint(C configuration, ServerContext serverContext) |
| | | { |
| | | this.configuration = configuration; |
| | | this.serverContext = serverContext; |
| | | } |
| | | |
| | | /** |
| | |
| | | /** The configuration handler for the Directory Server. */ |
| | | private ConfigurationHandler configurationHandler; |
| | | |
| | | /** The configuration manager that will handle HTTP endpoints. */ |
| | | private HttpEndpointConfigManager httpEndpointConfigManager; |
| | | |
| | | /** The set of account status notification handlers defined in the server. */ |
| | | private ConcurrentMap<DN, AccountStatusNotificationHandler<?>> |
| | | accountStatusNotificationHandlers; |
| | |
| | | identityMapperConfigManager = new IdentityMapperConfigManager(serverContext); |
| | | identityMapperConfigManager.initializeIdentityMappers(); |
| | | |
| | | new HttpEndpointConfigManager(httpRouter) |
| | | .registerTo(serverContext.getServerManagementContext().getRootConfiguration()); |
| | | |
| | | initializeRootDNConfigManager(); |
| | | |
| | | initializeAuthenticatedUsers(); |
| | |
| | | admDataSync.synchronize(); |
| | | } |
| | | |
| | | httpEndpointConfigManager = new HttpEndpointConfigManager(serverContext); |
| | | httpEndpointConfigManager.registerTo(serverContext.getServerManagementContext().getRootConfiguration()); |
| | | |
| | | deleteUnnecessaryFiles(); |
| | | } |
| | | } |
| | |
| | | * removals, or modifications to any HTTP endpoints while the server is running. |
| | | */ |
| | | public class HttpEndpointConfigManager implements ConfigurationChangeListener<HTTPEndpointCfg>, |
| | | ConfigurationAddListener<HTTPEndpointCfg>, ConfigurationDeleteListener<HTTPEndpointCfg> |
| | | ConfigurationAddListener<HTTPEndpointCfg>, |
| | | ConfigurationDeleteListener<HTTPEndpointCfg> |
| | | { |
| | | private static final LocalizedLogger LOGGER = LocalizedLogger.getLoggerForThisClass(); |
| | | |
| | | private final ServerContext serverContext; |
| | | private final Router router; |
| | | private final Map<DN, HttpApplication> applications; |
| | | |
| | | /** |
| | | * Creates a new instance of this HTTP endpoint config manager. |
| | | * |
| | | * @param router |
| | | * The {@link Router} where to register configured {@link HttpEndpoint} |
| | | * @param serverContext |
| | | * The server context. |
| | | */ |
| | | public HttpEndpointConfigManager(Router router) |
| | | public HttpEndpointConfigManager(ServerContext serverContext) |
| | | { |
| | | this.router = router; |
| | | this.serverContext = serverContext; |
| | | this.router = serverContext.getHTTPRouter(); |
| | | this.applications = new HashMap<>(); |
| | | } |
| | | |
| | |
| | | final Class<? extends HttpEndpoint<?>> endpointClass = |
| | | (Class<? extends HttpEndpoint<?>>) HTTPEndpointCfgDefn.getInstance().getJavaClassPropertyDefinition() |
| | | .loadClass(configuration.getJavaClass(), HttpEndpoint.class); |
| | | return endpointClass.getDeclaredConstructor(configuration.configurationClass()).newInstance(configuration); |
| | | return endpointClass.getDeclaredConstructor(configuration.configurationClass(), ServerContext.class) |
| | | .newInstance(configuration, serverContext); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | |
| | | * |
| | | * Copyright 2016 ForgeRock AS. |
| | | */ |
| | | |
| | | package org.opends.server.protocols.http; |
| | | |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.services.context.AbstractContext; |
| | | import org.forgerock.services.context.Context; |
| | | |
| | | /** Context provided by this LDAP server to the embedded {@link org.forgerock.http.HttpApplication}s. */ |
| | | /** |
| | | * Context provided by a Directory Server. It contains a reference to a |
| | | * {@link InternalConnectionFactory} which can be used to perform direct LDAP |
| | | * operation on this Directory Server without the network overhead. |
| | | */ |
| | | public final class LDAPContext extends AbstractContext |
| | | { |
| | | private final ConnectionFactory ldapConnectionFactory; |
| | | private final InternalConnectionFactory internalConnectionFactory; |
| | | |
| | | LDAPContext(final Context parent, ConnectionFactory ldapConnectionFactory) |
| | | /** |
| | | * Create a new LDAPContext. |
| | | * |
| | | * @param parent |
| | | * The parent context. |
| | | * @param internalConnectionFactory |
| | | * Internal connection factory of this LDAP server. |
| | | */ |
| | | public LDAPContext(final Context parent, InternalConnectionFactory internalConnectionFactory) |
| | | { |
| | | super(parent, "LDAP context"); |
| | | this.ldapConnectionFactory = ldapConnectionFactory; |
| | | this.internalConnectionFactory = internalConnectionFactory; |
| | | } |
| | | |
| | | /** |
| | | * Get the {@link org.forgerock.opendj.ldap.LDAPConnectionFactory} attached to this context. |
| | | * Get the {@link InternalConnectionFactory} attached to this context. |
| | | * |
| | | * @return The {@link org.forgerock.opendj.ldap.LDAPConnectionFactory} attached to this context. |
| | | * @return The {@link InternalConnectionFactory} attached to this context. |
| | | */ |
| | | public ConnectionFactory getLdapConnectionFactory() |
| | | public InternalConnectionFactory getInternalConnectionFactory() |
| | | { |
| | | return ldapConnectionFactory; |
| | | return internalConnectionFactory; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * An internal connection factory providing direct connection to this Directory |
| | | * Server without the network overhead. |
| | | */ |
| | | public interface InternalConnectionFactory |
| | | { |
| | | /** |
| | | * Get a direct {@link Connection} to this Directory Server. |
| | | * |
| | | * @param userDN |
| | | * DN of the user's used to validate authorization. |
| | | * @return A direct {@link Connection} to this Directory Server. |
| | | * @throws LdapException |
| | | * If a connection cannot be create (i.e: because the userDN |
| | | * doesn't exists). |
| | | */ |
| | | Connection getConnection(DN userDN) throws LdapException; |
| | | } |
| | | } |
| | |
| | | */ |
| | | package org.opends.server.protocols.http; |
| | | |
| | | import static org.opends.messages.ProtocolMessages.*; |
| | | |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.http.protocol.Status; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.core.ServerContext; |
| | | import org.opends.server.types.DisconnectReason; |
| | | import org.opends.server.protocols.http.LDAPContext.InternalConnectionFactory; |
| | | import org.opends.server.types.AuthenticationInfo; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | |
| | | /** |
| | | * Filter injecting the {@link LDAPContext} giving access to |
| | |
| | | private final ServerContext serverContext; |
| | | private final HTTPConnectionHandler httpConnectionHandler; |
| | | |
| | | LDAPContextInjectionFilter(ServerContext serverContext, HTTPConnectionHandler httpConnectionHandler) { |
| | | LDAPContextInjectionFilter(ServerContext serverContext, HTTPConnectionHandler httpConnectionHandler) |
| | | { |
| | | this.serverContext = serverContext; |
| | | this.httpConnectionHandler= httpConnectionHandler; |
| | | this.httpConnectionHandler = httpConnectionHandler; |
| | | } |
| | | |
| | | @Override |
| | | public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) |
| | | public Promise<Response, NeverThrowsException> filter(final Context context, final Request request, |
| | | final Handler next) |
| | | { |
| | | final HTTPClientConnection clientConnection = |
| | | new HTTPClientConnection(serverContext, httpConnectionHandler, context, request); |
| | | if (clientConnection.getConnectionID() < 0) |
| | | final LDAPContext djContext = new LDAPContext(context, new InternalConnectionFactory() |
| | | { |
| | | clientConnection.disconnect(DisconnectReason.ADMIN_LIMIT_EXCEEDED, true, |
| | | ERR_CONNHANDLER_REJECTED_BY_SERVER.get()); |
| | | return Promises.newResultPromise(new Response(Status.SERVICE_UNAVAILABLE)); |
| | | } |
| | | |
| | | final LDAPContext djContext = new LDAPContext(context, new ConnectionFactory() |
| | | { |
| | | private final Connection connection = new SdkConnectionAdapter(clientConnection); |
| | | |
| | | @Override |
| | | public Promise<Connection, LdapException> getConnectionAsync() |
| | | public Connection getConnection(DN userDN) throws LdapException |
| | | { |
| | | return Promises.newResultPromise(connection); |
| | | final HTTPClientConnection clientConnection = |
| | | new HTTPClientConnection(serverContext, httpConnectionHandler, context, request); |
| | | clientConnection.setAuthenticationInfo(getAuthInfoForDN(userDN)); |
| | | if (clientConnection.getConnectionID() < 0) |
| | | { |
| | | throw LdapException.newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED); |
| | | } |
| | | httpConnectionHandler.addClientConnection(clientConnection); |
| | | return new SdkConnectionAdapter(clientConnection); |
| | | } |
| | | |
| | | @Override |
| | | public Connection getConnection() throws LdapException |
| | | private AuthenticationInfo getAuthInfoForDN(DN userDN) throws LdapException |
| | | { |
| | | return connection; |
| | | } |
| | | |
| | | @Override |
| | | public void close() |
| | | { |
| | | if (userDN == null || userDN.isRootDN()) |
| | | { |
| | | return new AuthenticationInfo(); |
| | | } |
| | | final DN rootUserDN = DirectoryServer.getActualRootBindDN(userDN); |
| | | if (rootUserDN != null) |
| | | { |
| | | userDN = rootUserDN; |
| | | } |
| | | Entry userEntry; |
| | | try |
| | | { |
| | | userEntry = DirectoryServer.getEntry(userDN); |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | throw LdapException.newLdapException(e.getResultCode()); |
| | | } |
| | | if (userEntry == null) |
| | | { |
| | | throw LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS); |
| | | } |
| | | return new AuthenticationInfo(userEntry, DirectoryServer.isRootDN(userDN)); |
| | | } |
| | | }); |
| | | return next.handle(djContext, request); |
| | | } |
| | | |
| | | } |
| | | } |
| 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.opends.server.protocols.http.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | import static org.forgerock.util.Reject.checkNotNull; |
| | | import static org.forgerock.util.Utils.closeSilently; |
| | | |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.Handler; |
| | | import org.forgerock.http.protocol.Request; |
| | | import org.forgerock.http.protocol.Response; |
| | | import org.forgerock.http.protocol.Status; |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext; |
| | | import org.forgerock.services.context.Context; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.Function; |
| | | import org.forgerock.util.promise.NeverThrowsException; |
| | | import org.forgerock.util.promise.Promise; |
| | | import org.forgerock.util.promise.Promises; |
| | | import org.opends.server.api.IdentityMapper; |
| | | import org.opends.server.protocols.http.LDAPContext; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.types.Entry; |
| | | |
| | | /** |
| | | * Authorization proxy using internal connection's capabilities as an optimized |
| | | * alternative to {@link ProxiedAuthV2Control}. This proxy creates an |
| | | * {@link AuthenticatedConnectionContext} for the current request using its |
| | | * {@link SecurityContext}. |
| | | */ |
| | | final class InternalProxyAuthzFilter implements Filter |
| | | { |
| | | private final IdentityMapper<?> identityMapper; |
| | | private final Schema schema; |
| | | private final Function<SecurityContext, String, LdapException> authzIdProvider; |
| | | |
| | | InternalProxyAuthzFilter(IdentityMapper<?> identityMapper, Schema schema, |
| | | Function<SecurityContext, String, LdapException> authzIdProvider) |
| | | { |
| | | this.identityMapper = checkNotNull(identityMapper, "identityMapper cannot be null"); |
| | | this.schema = checkNotNull(schema, "schema cannot be null"); |
| | | this.authzIdProvider = checkNotNull(authzIdProvider, "authzIdProvider cannot be null"); |
| | | } |
| | | |
| | | @Override |
| | | public final Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) |
| | | { |
| | | final SecurityContext securityContext = context.asContext(SecurityContext.class); |
| | | final LDAPContext ldapContext = context.asContext(LDAPContext.class); |
| | | Connection tmp = null; |
| | | try |
| | | { |
| | | tmp = ldapContext.getInternalConnectionFactory().getConnection(getUserDN(securityContext)); |
| | | } |
| | | catch (LdapException | DirectoryException e) |
| | | { |
| | | closeSilently(tmp); |
| | | return asErrorResponse(e); |
| | | } |
| | | final Connection authConnection = tmp; |
| | | return next.handle(new AuthenticatedConnectionContext(context, authConnection), request) |
| | | .thenFinally(new Runnable() |
| | | { |
| | | @Override |
| | | public void run() |
| | | { |
| | | closeSilently(authConnection); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private DN getUserDN(final SecurityContext securityContext) throws LdapException, DirectoryException |
| | | { |
| | | final String authzId = authzIdProvider.apply(securityContext); |
| | | if (authzId.startsWith("u:")) |
| | | { |
| | | final Entry entry = identityMapper.getEntryForID(authzId); |
| | | if (entry == null) |
| | | { |
| | | throw LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS); |
| | | } |
| | | return entry.getName(); |
| | | } |
| | | else if (authzId.startsWith("dn:")) |
| | | { |
| | | try |
| | | { |
| | | return DN.valueOf(authzId.substring(3), schema); |
| | | } |
| | | catch (LocalizedIllegalArgumentException e) |
| | | { |
| | | throw LdapException.newLdapException(ResultCode.INVALID_DN_SYNTAX, e); |
| | | } |
| | | } |
| | | throw LdapException.newLdapException(ResultCode.AUTHORIZATION_DENIED); |
| | | } |
| | | |
| | | 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()); |
| | | return Promises.newResultPromise(response); |
| | | } |
| | | } |
| | |
| | | import java.net.URISyntaxException; |
| | | import java.net.URL; |
| | | |
| | | import org.forgerock.http.Filter; |
| | | import org.forgerock.http.HttpApplication; |
| | | import org.forgerock.opendj.adapter.server3x.Adapters; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.LdapException; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.rest2ldap.Rest2LDAPHttpApplication; |
| | | import org.forgerock.opendj.server.config.server.Rest2ldapEndpointCfg; |
| | | import org.forgerock.services.context.SecurityContext; |
| | | import org.forgerock.util.Function; |
| | | import org.opends.server.api.HttpEndpoint; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.core.ServerContext; |
| | | import org.opends.server.types.InitializationException; |
| | | |
| | | /** |
| | | * Encapsulates configuration required to start a rest2ldap application in an |
| | | * OpenDJ context. Acts as a factory for {@link Rest2LdapEmbeddedHttpApplication}. |
| | | * Encapsulates configuration required to start a REST2LDAP application embedded |
| | | * in this LDAP server. Acts as a factory for {@link Rest2LDAPHttpApplication}. |
| | | */ |
| | | public final class Rest2LdapEndpoint extends HttpEndpoint<Rest2ldapEndpointCfg> |
| | | { |
| | | |
| | | /** |
| | | * Create a new Rest2LdapEnpoint with the supplied configuration. |
| | | * |
| | | * @param configuration |
| | | * Configuration to use for the {@link HttpApplication} |
| | | * @param serverContext |
| | | * Server of this LDAP server |
| | | */ |
| | | public Rest2LdapEndpoint(Rest2ldapEndpointCfg configuration) |
| | | public Rest2LdapEndpoint(Rest2ldapEndpointCfg configuration, ServerContext serverContext) |
| | | { |
| | | super(configuration); |
| | | super(configuration, serverContext); |
| | | } |
| | | |
| | | @Override |
| | |
| | | final URI configURI = new URI(configuration.getConfigUrl()); |
| | | final URL absoluteConfigUrl = |
| | | configURI.isAbsolute() ? configURI.toURL() : getFileForPath(configuration.getConfigUrl()).toURI().toURL(); |
| | | return new Rest2LdapEmbeddedHttpApplication(absoluteConfigUrl, configuration.isAuthenticationRequired()); |
| | | return new InternalRest2LDAPHttpApplication(absoluteConfigUrl, serverContext.getSchemaNG()); |
| | | } |
| | | catch (MalformedURLException | URISyntaxException e) |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Specialized {@link Rest2LDAPHttpApplication} using internal connections to |
| | | * this local LDAP server. |
| | | */ |
| | | private final class InternalRest2LDAPHttpApplication extends Rest2LDAPHttpApplication |
| | | { |
| | | private final ConnectionFactory rootInternalConnectionFactory = Adapters.newRootConnectionFactory(); |
| | | |
| | | InternalRest2LDAPHttpApplication(URL configURL, Schema schema) |
| | | { |
| | | super(configURL, schema); |
| | | } |
| | | |
| | | @Override |
| | | protected Filter newProxyAuthzFilter(final ConnectionFactory connectionFactory, |
| | | final Function<SecurityContext, String, LdapException> authzIdProvider) |
| | | { |
| | | return new InternalProxyAuthzFilter(DirectoryServer.getProxiedAuthorizationIdentityMapper(), schema, |
| | | authzIdProvider); |
| | | } |
| | | |
| | | @Override |
| | | protected ConnectionFactory getConnectionFactory(String name) |
| | | { |
| | | return rootInternalConnectionFactory; |
| | | } |
| | | } |
| | | } |
| | |
| | | import org.forgerock.util.Options; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.protocols.internal.InternalClientConnection; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.testng.annotations.AfterClass; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.DataProvider; |
| | |
| | | public class AdaptersTestCase extends DirectoryServerTestCase { |
| | | private static final String USER_0_DN_STRING = "uid=user.0,o=test"; |
| | | |
| | | /** |
| | | * Provides an anonymous connection factories. |
| | | * |
| | | * @return Anonymous connection factories. |
| | | */ |
| | | @DataProvider |
| | | public Object[][] anonymousConnectionFactories() { |
| | | return new Object[][] { |
| | | { new LDAPConnectionFactory("localhost", getServerLdapPort()) }, |
| | | { Adapters.newAnonymousConnectionFactory() } }; |
| | | } |
| | | |
| | | private Integer getServerLdapPort() { |
| | | return TestCaseUtils.getServerLdapPort(); |
| | | } |
| | |
| | | * Provides root connection factories. |
| | | * |
| | | * @return Root connection factories. |
| | | * @throws DirectoryException |
| | | */ |
| | | @DataProvider |
| | | public Object[][] rootConnectionFactories() { |
| | | public Object[][] rootConnectionFactories() throws DirectoryException { |
| | | return new Object[][] { |
| | | { new LDAPConnectionFactory("localhost", |
| | | getServerLdapPort(), |
| | |
| | | .set(AUTHN_BIND_REQUEST, |
| | | newSimpleBindRequest("cn=directory manager", |
| | | "password".toCharArray()))) }, |
| | | { Adapters.newConnectionFactoryForUser(DN.valueOf("cn=directory manager")) } }; |
| | | { Adapters.newConnectionFactory(new InternalClientConnection(DN.valueOf("cn=directory manager"))) }, |
| | | { Adapters.newRootConnectionFactory() } }; |
| | | } |
| | | |
| | | /** |
| | |
| | | TestCaseUtils.startServer(); |
| | | |
| | | // Creates a root connection to add data |
| | | final Connection connection = Adapters.newRootConnection(); |
| | | final Connection connection = Adapters.newRootConnectionFactory().getConnection(); |
| | | // @formatter:off |
| | | connection.add( |
| | | "dn: uid=user.0, o=test", |
| | |
| | | */ |
| | | @Test |
| | | public void testSimpleLDAPConnectionFactorySimpleBind() throws LdapException { |
| | | final LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", getServerLdapPort()); |
| | | try (Connection connection = factory.getConnection()) { |
| | | try (final LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", getServerLdapPort()); |
| | | final Connection connection = factory.getConnection()) { |
| | | connection.bind("cn=Directory Manager", "password".toCharArray()); |
| | | assertThat(connection.isValid()).isTrue(); |
| | | assertThat(connection.isClosed()).isFalse(); |
| | |
| | | */ |
| | | @Test |
| | | public void testLDAPSASLBind() throws NumberFormatException, GeneralSecurityException, LdapException { |
| | | LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", getServerLdapPort()); |
| | | |
| | | PlainSASLBindRequest request = |
| | | Requests.newPlainSASLBindRequest("u:user.0", "password".toCharArray()); |
| | | |
| | | try (Connection connection = factory.getConnection()) { |
| | | final PlainSASLBindRequest request = Requests.newPlainSASLBindRequest("u:user.0", "password".toCharArray()); |
| | | try (final LDAPConnectionFactory factory = new LDAPConnectionFactory("localhost", getServerLdapPort()); |
| | | final Connection connection = factory.getConnection()) { |
| | | connection.bind(request); |
| | | } |
| | | } |
| | |
| | | * @throws LdapException |
| | | */ |
| | | @Test |
| | | public void testAdapterConnectionSASLBindRequest() throws LdapException, |
| | | GeneralSecurityException { |
| | | public void testAdapterConnectionSASLBindRequest() throws LdapException { |
| | | PlainSASLBindRequest request = |
| | | Requests.newPlainSASLBindRequest("u:user.0", "password".toCharArray()); |
| | | try (final Connection connection = Adapters.newRootConnection()) { |
| | | try (final Connection connection = Adapters.newRootConnectionFactory().getConnection()) { |
| | | connection.bind(request); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * This type of connection is not supported. Anonymous SASL Mechanisms is |
| | | * disabled in the config.ldif file. |
| | | * |
| | | * @throws LdapException |
| | | */ |
| | | @Test(dataProvider = "anonymousConnectionFactories", expectedExceptions = LdapException.class) |
| | | public void testConnectionAnonymousSASLBindRequest(final ConnectionFactory factory) throws LdapException { |
| | | try (final Connection connection = factory.getConnection()) { |
| | | connection.bind(Requests.newAnonymousSASLBindRequest("anonymousSASLBindRequest")); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Binds as a root. |
| | | * |
| | | * @throws Exception |
| | | */ |
| | | @Test |
| | | public void testAdapterConnectionSimpleBindAsRoot() throws Exception { |
| | | try (final Connection connection = Adapters.newRootConnection()) { |
| | | try (final Connection connection = Adapters.newRootConnectionFactory().getConnection()) { |
| | | final BindResult result = connection.bind("cn=Directory Manager", "password".toCharArray()); |
| | | assertThat(connection.isValid()).isTrue(); |
| | | assertThat(result.getResultCode()).isEqualTo(ResultCode.SUCCESS); |
| | |
| | | */ |
| | | @Test |
| | | public void testAdapterConnectionSimpleBindAsAUser() throws Exception { |
| | | try (final Connection connection = Adapters.newConnectionForUser(DN.valueOf(USER_0_DN_STRING))) { |
| | | try (final Connection connection = Adapters.newConnectionFactory( |
| | | new InternalClientConnection(DN.valueOf(USER_0_DN_STRING))).getConnection()) { |
| | | final BindResult result = connection.bind(USER_0_DN_STRING, "password".toCharArray()); |
| | | assertThat(result.getResultCode()).isEqualTo(ResultCode.SUCCESS); |
| | | } |
| | |
| | | */ |
| | | @Test(expectedExceptions = AuthenticationException.class) |
| | | public void testAdapterConnectionSimpleBindAsAUserWrongPassword() throws Exception { |
| | | try (final Connection connection = Adapters.newConnectionForUser(DN.valueOf(USER_0_DN_STRING))) { |
| | | try (final Connection connection = Adapters.newConnectionFactory( |
| | | new InternalClientConnection(DN.valueOf(USER_0_DN_STRING))).getConnection()) { |
| | | // Invalid credentials |
| | | connection.bind(USER_0_DN_STRING, "pass".toCharArray()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Tries to bind as anonymous. |
| | | * |
| | | * @throws Exception |
| | | */ |
| | | @Test |
| | | public void testAdapterConnectionSimpleBind() throws Exception { |
| | | // Anonymous |
| | | try (final Connection connection = Adapters.newAnonymousConnection()) { |
| | | final BindResult result = connection.bind("", "".toCharArray()); |
| | | assertThat(result.getDiagnosticMessage()).isEmpty(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Testing the adapters with a simple add request. |
| | | * |
| | | * @throws Exception |
| | | */ |
| | | @Test |
| | | public void testAdapterAddRequest() throws Exception { |
| | | final Connection connection = Adapters.newRootConnection(); |
| | | final Connection connection = Adapters.newRootConnectionFactory().getConnection(); |
| | | // @formatter:off |
| | | final AddRequest addRequest = Requests.newAddRequest( |
| | | "dn: sn=carter,o=test", |
| | |
| | | */ |
| | | @Test |
| | | public void testAdapterSearchRequest() throws Exception { |
| | | final Connection connection = Adapters.newRootConnection(); |
| | | final Connection connection = Adapters.newRootConnectionFactory().getConnection(); |
| | | |
| | | final SearchRequest request = |
| | | Requests.newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE, |
| | |
| | | */ |
| | | @Test |
| | | public void testAdapterDeleteRequest() throws LdapException { |
| | | try (final Connection connection = Adapters.newRootConnection()) { |
| | | try (final Connection connection = Adapters.newRootConnectionFactory().getConnection()) { |
| | | // Checks if the entry exists. |
| | | SearchResultEntry sre = |
| | | connection.searchSingleEntry(Requests.newSearchRequest( |
| | |
| | | PreReadRequestControl.newControl(true, "mail")).addModification( |
| | | ModificationType.ADD, "mail", "modified@example.com"); |
| | | |
| | | final Connection connection = Adapters.newRootConnection(); |
| | | final Connection connection = Adapters.newRootConnectionFactory().getConnection(); |
| | | final Result result = connection.modify(changeRequest); |
| | | assertThat(result.getDiagnosticMessage()).isEmpty(); |
| | | assertThat(result.getControls()).isNotEmpty(); |
| | |
| | | } |
| | | |
| | | /** |
| | | * If an anonymous tries to delete, sends a result code : insufficient |
| | | * access rights. |
| | | * |
| | | * @throws LdapException |
| | | */ |
| | | @Test(dataProvider = "anonymousConnectionFactories", |
| | | expectedExceptions = AuthorizationException.class) |
| | | public void testAdapterAsAnonymousCannotPerformDeleteRequest(final ConnectionFactory factory) |
| | | throws LdapException { |
| | | final DeleteRequest deleteRequest = |
| | | Requests.newDeleteRequest("uid=user.2,o=test"); |
| | | |
| | | try (final Connection connection = factory.getConnection()) { |
| | | connection.delete(deleteRequest); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * If an anonymous tries to do an add request, sends a result code : |
| | | * insufficient access rights. |
| | | * |
| | | * @throws LdapException |
| | | */ |
| | | @Test(dataProvider = "anonymousConnectionFactories", |
| | | expectedExceptions = AuthorizationException.class) |
| | | public void testAdapterAsAnonymousCannotPerformAddRequest(final ConnectionFactory factory) |
| | | throws LdapException { |
| | | // @formatter:off |
| | | final AddRequest addRequest = Requests.newAddRequest( |
| | | "dn: sn=scarter,o=test", |
| | | "objectClass: top", |
| | | "objectClass: person", |
| | | "cn: scarter"); |
| | | // @formatter:on |
| | | |
| | | try (final Connection connection = factory.getConnection()) { |
| | | connection.add(addRequest); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * If an anonymous tries to do a modify DN request, sends a result code : |
| | | * insufficient access rights. |
| | | * |
| | | * @throws LdapException |
| | | */ |
| | | @Test(dataProvider = "anonymousConnectionFactories", |
| | | expectedExceptions = AuthorizationException.class) |
| | | public void testAdapterAsAnonymousCannotPerformModifyDNRequest(final ConnectionFactory factory) |
| | | throws LdapException { |
| | | final ModifyDNRequest changeRequest = |
| | | Requests.newModifyDNRequest("uid=user.2,o=test", "uid=user.test") |
| | | .setDeleteOldRDN(true); |
| | | try (final Connection connection = factory.getConnection()) { |
| | | connection.modifyDN(changeRequest); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * If an anonymous tries to do a modify request, sends a result code : |
| | | * insufficient access rights. |
| | | * |
| | | * @throws LdapException |
| | | */ |
| | | @Test(dataProvider = "anonymousConnectionFactories", |
| | | expectedExceptions = LdapException.class) |
| | | public void testAdapterAsAnonymousCannotPerformModifyRequest(final ConnectionFactory factory) |
| | | throws LdapException { |
| | | final ModifyRequest changeRequest = |
| | | Requests.newModifyRequest("uid=user.2,o=test").addControl( |
| | | PreReadRequestControl.newControl(true, "mail")).addModification( |
| | | ModificationType.REPLACE, "mail", "modified@example.com"); |
| | | |
| | | try (final Connection connection = factory.getConnection()) { |
| | | connection.modify(changeRequest); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The anonymous connection is allowed to perform compare request. |
| | | * |
| | | * @throws LdapException |
| | | */ |
| | | @Test(dataProvider = "anonymousConnectionFactories") |
| | | public void testAdapterAsAnonymousPerformsCompareRequest(final ConnectionFactory factory) |
| | | throws LdapException { |
| | | final CompareRequest compareRequest = |
| | | Requests.newCompareRequest(USER_0_DN_STRING, "uid", "user.0"); |
| | | |
| | | try (final Connection connection = factory.getConnection()) { |
| | | final CompareResult result = connection.compare(compareRequest); |
| | | assertThat(result.getResultCode()).isEqualTo(ResultCode.COMPARE_TRUE); |
| | | |
| | | assertThat(result.getDiagnosticMessage()).isEmpty(); |
| | | assertThat(result.getControls()).isEmpty(); |
| | | assertThat(result.getMatchedDN()).isEmpty(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The anonymous connection is allowed to perform search request. |
| | | * |
| | | * @throws Exception |
| | | */ |
| | | @Test(dataProvider = "anonymousConnectionFactories") |
| | | public void testAdapterAsAnonymousPerformsSearchRequest(final ConnectionFactory factory) |
| | | throws Exception { |
| | | final SearchRequest request = |
| | | Requests.newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE, |
| | | "(uid=user.1)"); |
| | | |
| | | final Connection connection = factory.getConnection(); |
| | | final ConnectionEntryReader reader = connection.search(request); |
| | | |
| | | assertThat(reader.isEntry()).isTrue(); |
| | | final SearchResultEntry entry = reader.readEntry(); |
| | | assertThat(entry).isNotNull(); |
| | | assertThat(entry.getName().toString()).isEqualTo("uid=user.1,o=test"); |
| | | assertThat(reader.hasNext()).isFalse(); |
| | | } |
| | | |
| | | /** |
| | | * The anonymous connection is not allowed to perform search request |
| | | * associated with a control. |
| | | * <p> |
| | | * Unavailable Critical Extension: The request control with Object |
| | | * Identifier (OID) "x.x.x" cannot be used due to insufficient access |
| | | * rights. |
| | | * |
| | | * @throws Exception |
| | | */ |
| | | @Test(dataProvider = "anonymousConnectionFactories", expectedExceptions = LdapException.class) |
| | | public void testAdapterAsAnonymousCannotPerformSearchRequestWithControl( |
| | | final ConnectionFactory factory) throws Exception { |
| | | final Connection connection = factory.getConnection(); |
| | | final SearchRequest request = |
| | | Requests.newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE, |
| | | "(uid=user.1)").addControl(ADNotificationRequestControl.newControl(true)); |
| | | |
| | | final ConnectionEntryReader reader = connection.search(request); |
| | | reader.readEntry(); |
| | | } |
| | | |
| | | /** |
| | | * Creates an LDAP Connection and performs some basic calls like |
| | | * add/delete/search and compare results with an SDK adapter connection |
| | | * doing the same. |
| | |
| | | assertThat(deleteResult.getResultCode()).isEqualTo(ResultCode.SUCCESS); |
| | | |
| | | // SDK Adapter connection |
| | | final Connection adapterConnection = Adapters.newRootConnection(); |
| | | final Connection adapterConnection = Adapters.newRootConnectionFactory().getConnection(); |
| | | final Result sdkAddResult = adapterConnection.add(addRequest); |
| | | final ConnectionEntryReader sdkReader = adapterConnection.search(searchRequest); |
| | | final Result sdkDeleteResult = adapterConnection.delete(deleteRequest); |