Partial fix for OPENDJ-694: Implement HTTP BASIC authentication
* allow and add comments in the sample JSON configuration
* add sample authentication configuration to JSON configuration
* put all servlet related configuration in a separate element
* add support for authzId templates where the template is of the form "dn:{xxx}"
| | |
| | | import javax.servlet.ServletConfig; |
| | | import javax.servlet.ServletException; |
| | | |
| | | import org.codehaus.jackson.JsonParser; |
| | | import org.codehaus.jackson.map.ObjectMapper; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | |
| | | */ |
| | | public final class Rest2LDAPConnectionFactoryProvider { |
| | | private static final String INIT_PARAM_CONFIG_FILE = "config-file"; |
| | | private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); |
| | | private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure( |
| | | JsonParser.Feature.ALLOW_COMMENTS, true); |
| | | |
| | | /** |
| | | * Returns a JSON resource connection factory configured using the |
| | | * configuration file named in the {@code config-file} Servlet |
| | | * initialization parameter. The configuration file should have the |
| | | * following JSON structure excluding the C-like comments: |
| | | * |
| | | * <pre> |
| | | * { |
| | | * // LDAP connection factory configurations. |
| | | * "ldapConnectionFactories" : { |
| | | * "default" : { |
| | | * // See Rest2LDAP.configureConnectionFactory(JsonValue, String) |
| | | * }, |
| | | * "root" : { |
| | | * ... |
| | | * } |
| | | * }, |
| | | * |
| | | * // This is optional. |
| | | * "authorization" : { |
| | | * // The LDAP connection factory which should be used for LDAP operations, or |
| | | * // re-use cached connection from authentication filter if not present. |
| | | * "ldapConnectionFactory" : "root", |
| | | * |
| | | * // The optional authorization ID template to use if proxied authorization is |
| | | * // to be performed. |
| | | * "proxyAuthzIdTemplate" : "dn:uid={uid},ou=people,dc=example,dc=com" |
| | | * }, |
| | | * |
| | | * // The LDAP mappings |
| | | * "mappings" : { |
| | | * "/users" : { |
| | | * // The LDAP mapping for /users - Rest2LDAP.Builder.configureMapping(JsonValue). |
| | | * "baseDN" : "ou=people,dc=example,dc=com", |
| | | * |
| | | * ... |
| | | * }, |
| | | * "/groups" : { |
| | | * // The LDAP mapping for /groups - Rest2LDAP.Builder.configureMapping(JsonValue). |
| | | * "baseDN" : "ou=groups,dc=example,dc=com", |
| | | * |
| | | * ... |
| | | * } |
| | | * } |
| | | * } |
| | | * </pre> |
| | | * initialization parameter. See the sample configuration file for a |
| | | * detailed description of its content. |
| | | * |
| | | * @param config |
| | | * The Servlet configuration. |
| | |
| | | |
| | | // Parse the authorization configuration. |
| | | final String proxyAuthzTemplate = |
| | | configuration.get("authorization").get("proxyAuthzIdTemplate").asString(); |
| | | configuration.get("servlet").get("proxyAuthzIdTemplate").asString(); |
| | | final String ldapFactoryName = |
| | | configuration.get("authorization").get("ldapConnectionFactory").asString(); |
| | | configuration.get("servlet").get("ldapConnectionFactory").asString(); |
| | | final org.forgerock.opendj.ldap.ConnectionFactory ldapFactory; |
| | | if (ldapFactoryName != null) { |
| | | ldapFactory = |
| | |
| | | |
| | | // Create the router. |
| | | final Router router = new Router(); |
| | | final JsonValue mappings = configuration.get("mappings").required(); |
| | | final JsonValue mappings = configuration.get("servlet").get("mappings").required(); |
| | | for (final String mappingUrl : mappings.keys()) { |
| | | final JsonValue mapping = mappings.get(mappingUrl); |
| | | final CollectionResourceProvider provider = |
| | |
| | | { |
| | | // The array of connection factories which will be used by the Rest2LDAP |
| | | // Servlet and authentication filter. |
| | | "ldapConnectionFactories" : { |
| | | // Unauthenticated connections used for performing bind requests. |
| | | "default" : { |
| | | "connectionPoolSize" : 10, |
| | | "heartBeatIntervalSeconds" : 30, |
| | | |
| | | // The preferred load-balancing pool. |
| | | "primaryLDAPServers" : [ |
| | | { |
| | | "hostname" : "localhost", |
| | | "port" : 1389 |
| | | } |
| | | ], |
| | | "connectionPoolSize" : 10, |
| | | "heartBeatIntervalSeconds" : 30 |
| | | // The fail-over load-balancing pool (optional). |
| | | "secondaryLDAPServers" : [ |
| | | // Empty. |
| | | ] |
| | | }, |
| | | |
| | | // Authenticated connections which will be used for searches during |
| | | // authentication and proxied operations (if enabled). This factory |
| | | // will re-use the server "default" configuration. |
| | | "root" : { |
| | | "inheritFrom" : "default", |
| | | |
| | | // Defines how authentication should be performed. Only "simple" |
| | | // authentication is supported at the moment. |
| | | "authentication" : { |
| | | "simple" : { |
| | | "bindDN" : "cn=directory manager", |
| | |
| | | } |
| | | }, |
| | | |
| | | "authorization" : { |
| | | "ldapConnectionFactory" : "root" |
| | | // 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, |
| | | |
| | | // 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", |
| | | |
| | | // 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, |
| | | |
| | | // 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" : "simple", |
| | | |
| | | // The connection factory which will be exclusively used for |
| | | // authenticating users using LDAP bind operations. |
| | | "bindLDAPConnectionFactory" : "default", |
| | | |
| | | // The SASL AuthzID template which will be used for "sasl-plain" |
| | | // authentication. |
| | | "saslAuthzIdTemplate" : "dn:uid=%s,ou=people,dc=example,dc=com", |
| | | |
| | | // The connection factory which will be used for performing LDAP |
| | | // searches to locate users when "search+simple" authentication is |
| | | // enabled. |
| | | "searchLDAPConnectionFactory" : "root", |
| | | |
| | | // The search parameters to use for "search+simple" authentication. |
| | | "searchBaseDN" : "ou=people,dc=example,dc=com", |
| | | "searchScope" : "sub", // Or "one". |
| | | "searchFilterTemplate" : "(&(objectClass=inetOrgPerson)(uid=%s))" |
| | | |
| | | // 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", |
| | | |
| | | // The AuthzID template which will be used for proxied authorization. If |
| | | // no template is specified then proxied authorization will be disabled. |
| | | // 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", |
| | |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | * <code>u:{uid}@{realm}.example.com</code>. |
| | | */ |
| | | final class AuthzIdTemplate { |
| | | private static interface Impl { |
| | | String formatAsAuthzId(AuthzIdTemplate t, Object[] templateVariables, Schema schema) |
| | | throws ResourceException; |
| | | } |
| | | |
| | | private static final Impl DN_IMPL = new Impl() { |
| | | |
| | | @Override |
| | | public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables, |
| | | final Schema schema) throws ResourceException { |
| | | 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( |
| | | "The request could not be authorized because the required security principal " |
| | | + " was not a valid LDAP DN"); |
| | | } |
| | | return authzId; |
| | | } |
| | | }; |
| | | |
| | | private static final Pattern DN_PATTERN = Pattern.compile("^dn:\\{[^}]+\\}$"); |
| | | |
| | | private static final Impl DN_TEMPLATE_IMPL = new Impl() { |
| | | |
| | | @Override |
| | | public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables, |
| | | final Schema schema) throws ResourceException { |
| | | return "dn:" + DN.format(t.dnFormatString, schema, templateVariables).toString(); |
| | | } |
| | | |
| | | }; |
| | | |
| | | private static final Pattern KEY_RE = Pattern.compile("\\{([^}]+)\\}"); |
| | | |
| | | private static final Impl UID_TEMPLATE_IMPL = new Impl() { |
| | | |
| | | @Override |
| | | public String formatAsAuthzId(final AuthzIdTemplate t, final Object[] templateVariables, |
| | | final Schema schema) throws ResourceException { |
| | | return String.format(Locale.ENGLISH, t.formatString, templateVariables); |
| | | } |
| | | |
| | | }; |
| | | |
| | | private final String dnFormatString; |
| | | private final String formatString; |
| | | private final List<String> keys = new ArrayList<String>(); |
| | | private final Impl pimpl; |
| | | private final String template; |
| | | |
| | | AuthzIdTemplate(final String template) { |
| | |
| | | keys.add(matcher.group(1)); |
| | | } |
| | | matcher.appendTail(buffer); |
| | | |
| | | this.template = template; |
| | | this.formatString = buffer.toString(); |
| | | this.dnFormatString = template.startsWith("dn:") ? formatString.substring(3) : null; |
| | | this.template = template; |
| | | |
| | | if (template.startsWith("dn:")) { |
| | | this.pimpl = DN_PATTERN.matcher(template).matches() ? DN_IMPL : DN_TEMPLATE_IMPL; |
| | | this.dnFormatString = formatString.substring(3); |
| | | } else { |
| | | this.pimpl = UID_TEMPLATE_IMPL; |
| | | this.dnFormatString = null; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | String formatAsAuthzId(final Map<String, Object> principals, final Schema schema) |
| | | throws ResourceException { |
| | | if (isDNTemplate()) { |
| | | final String dn = formatAsDN(principals, schema).toString(); |
| | | final StringBuilder builder = new StringBuilder(dn.length() + 3); |
| | | builder.append("dn:"); |
| | | builder.append(dn); |
| | | return builder.toString(); |
| | | } else { |
| | | final String[] values = getPrincipalsForFormatting(principals); |
| | | return String.format(Locale.ENGLISH, formatString, (Object[]) values); |
| | | } |
| | | } |
| | | |
| | | DN formatAsDN(final Map<String, Object> principals, final Schema schema) |
| | | throws ResourceException { |
| | | if (!isDNTemplate()) { |
| | | throw new IllegalStateException(); |
| | | } |
| | | final String[] values = getPrincipalsForFormatting(principals); |
| | | return DN.format(dnFormatString, schema, (Object[]) values); |
| | | } |
| | | |
| | | boolean isDNTemplate() { |
| | | return dnFormatString != null; |
| | | final String[] templateVariables = getPrincipalsForFormatting(principals); |
| | | return pimpl.formatAsAuthzId(this, templateVariables, schema); |
| | | } |
| | | |
| | | private String[] getPrincipalsForFormatting(final Map<String, Object> principals) |
| | |
| | | // Parse secondary data center(s). |
| | | final JsonValue secondaryLDAPServers = configuration.get("secondaryLDAPServers"); |
| | | final ConnectionFactory secondary; |
| | | if (secondaryLDAPServers.isList() && secondaryLDAPServers.size() != 0) { |
| | | if (secondaryLDAPServers.isList()) { |
| | | if (secondaryLDAPServers.size() > 0) { |
| | | secondary = |
| | | parseLDAPServers(secondaryLDAPServers, bindRequest, connectionPoolSize, |
| | | heartBeatIntervalSeconds); |
| | | } else { |
| | | secondary = null; |
| | | } |
| | | } else if (!secondaryLDAPServers.isNull()) { |
| | | throw new IllegalArgumentException("Invalid secondaryLDAPServers configuration"); |
| | | } else { |
| | |
| | | map("uid", "test.user", "realm", "test+cn=quoting") |
| | | }, |
| | | { |
| | | // Should not perform DN quoting. |
| | | "dn:{dn}", |
| | | "dn:uid=test.user,ou=acme,dc=example,dc=com", |
| | | map("dn", "uid=test.user,ou=acme,dc=example,dc=com") |
| | | }, |
| | | { |
| | | "u:{uid}@{realm}.example.com", |
| | | "u:test.user@acme.example.com", |
| | | map("uid", "test.user", "realm", "acme") |
| | |
| | | map("uid", "test.user") |
| | | }, |
| | | { |
| | | // Malformed DN. |
| | | "dn:{dn}", |
| | | map("dn", "uid") |
| | | }, |
| | | { |
| | | "u:{uid}@{realm}.example.com", |
| | | map("uid", "test.user") |
| | | }, |
| | |
| | | }, |
| | | }; |
| | | // @formatter:on |
| | | |
| | | } |
| | | |
| | | @Test(dataProvider = "invalidTemplates", expectedExceptions = IllegalArgumentException.class) |