mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
21.04.2013 f590ef177e05cff287db176160ca7ef26a7f8543
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}"
5 files modified
443 ■■■■■ changed files
opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java 55 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json 281 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java 83 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java 12 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java 12 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
@@ -21,6 +21,7 @@
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;
@@ -36,54 +37,14 @@
 */
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.
@@ -117,9 +78,9 @@
            // 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 =
@@ -131,7 +92,7 @@
            // 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 =
opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
@@ -1,17 +1,33 @@
{
    // 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" : {
            "primaryLDAPServers" : [
            "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",
@@ -20,104 +36,179 @@
            }
        }
    },
    "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?
    },
    "mappings" : {
        "/users" : {
            "baseDN" : "ou=people,dc=example,dc=com",
            "readOnUpdatePolicy" : "controls",
            "additionalLDAPAttributes" : [
                {
                    "type" : "objectClass",
                    "values" : [
                        "top",
                        "person",
                        "organizationalPerson",
                        "inetOrgPerson"
                    ]
    // 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",
                "readOnUpdatePolicy" : "controls",
                "additionalLDAPAttributes" : [
                    {
                        "type" : "objectClass",
                        "values" : [
                            "top",
                            "person",
                            "organizationalPerson",
                            "inetOrgPerson"
                        ]
                    }
                ],
                "namingStrategy" : {
                    "strategy" : "clientDNNaming",
                    "dnAttribute" : "uid"
                },
                "etagAttribute" : "etag",
                "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" } }
                    } }
                }
            ],
            "namingStrategy" : {
                "strategy" : "clientDNNaming",
                "dnAttribute" : "uid"
            },
            "etagAttribute" : "etag",
            "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" : {
                "baseDN" : "ou=groups,dc=example,dc=com",
                "readOnUpdatePolicy" : "controls",
                "additionalLDAPAttributes" : [
                    {
                        "type" : "objectClass",
                        "values" : [
                            "top",
                            "groupOfUniqueNames"
                        ]
                    }
                ],
                "namingStrategy" : {
                    "strategy" : "clientDNNaming",
                    "dnAttribute" : "cn"
                },
                "etagAttribute" : "etag",
                "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" } }
                    } }
                } },
                "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",
            "additionalLDAPAttributes" : [
                {
                    "type" : "objectClass",
                    "values" : [
                        "top",
                        "groupOfUniqueNames"
                    ]
                }
            ],
            "namingStrategy" : {
                "strategy" : "clientDNNaming",
                "dnAttribute" : "cn"
            },
            "etagAttribute" : "etag",
            "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" } }
                } }
            }
        }
    }
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
@@ -37,11 +37,57 @@
 * <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) {
@@ -57,10 +103,16 @@
            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
@@ -70,29 +122,8 @@
    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)
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -660,10 +660,14 @@
        // Parse secondary data center(s).
        final JsonValue secondaryLDAPServers = configuration.get("secondaryLDAPServers");
        final ConnectionFactory secondary;
        if (secondaryLDAPServers.isList() && secondaryLDAPServers.size() != 0) {
            secondary =
                    parseLDAPServers(secondaryLDAPServers, bindRequest, connectionPoolSize,
                            heartBeatIntervalSeconds);
        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 {
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
@@ -49,6 +49,12 @@
                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")
@@ -82,6 +88,11 @@
                map("uid", "test.user")
            },
            {
                // Malformed DN.
                "dn:{dn}",
                map("dn", "uid")
            },
            {
                "u:{uid}@{realm}.example.com",
                map("uid", "test.user")
            },
@@ -105,7 +116,6 @@
            },
        };
        // @formatter:on
    }
    @Test(dataProvider = "invalidTemplates", expectedExceptions = IllegalArgumentException.class)