[#1155] Make the rest2ldap bind templates and HTTP Basic credentials work as documented (#1165)
## Problem
rest2ldap could not build an LDAP name from HTTP Basic credentials in
several configurations the reference documentation recommends. See
#1155.
- `sasl-plain` with an `authzIdTemplate` starting with `dn:` failed
every bind with `INVALID_DN_SYNTAX`, because the whole `dn:...` string
went to `DN.format()`.
- The `simple` bind with its default `bindDnTemplate`, `{username}`,
never worked. `DN.format()` escaped the whole DN user name as one
attribute value. The parse error was thrown from `authenticate()`
instead of failing the returned promise.
- An HTTP Basic password that contains `:` was dropped: the credentials
were split with `split(":")` and anything but two parts meant "no
credentials". A bare `Basic` header threw
`StringIndexOutOfBoundsException`.
- A `%` in the fixed part of a template was read by `String.format()` as
a format specifier (`o=100%,dc=example` →
`MissingFormatArgumentException: '%,d'`). `DnTemplate` also stripped a
trailing `,..` after an escaped comma.
## Change
- **Bind DN templates** (`authz/Utils.formatBindDn`, used by
`SimpleBindStrategy` and the `dn:` branch of `SaslPlainStrategy`): a
template which is just `{username}` takes the user name as the DN
(`DN.valueOf`); any other template keeps `DN.format()`, which escapes
the user name as one attribute value. A user name which does not give a
DN fails the returned promise with `INVALID_CREDENTIALS` (HTTP 401)
before a connection is taken.
- **`sasl-plain`**: only the part after `dn:` is formatted, and the
authentication ID sent is `dn:<DN>`. Spaces after `dn:` are ignored, as
for the OAuth2 template, so `dn: {username}` reads as `dn:{username}`.
- **OAuth2 `authzIdTemplate`** (`AuthzIdTemplate`): a `dn:` template
which is just one placeholder, such as `dn:{sub}`, takes the field value
as the DN as well. Without this, it failed in the same way as the
default `simple` template.
- **HTTP Basic** (`CredentialExtractors`): the credentials are split at
the first `:`, as [RFC 7617
§2](https://www.rfc-editor.org/rfc/rfc7617#section-2) requires, and the
scheme must be followed by a space. The case-insensitive match no longer
depends on the default locale. Credentials which do not decode as
base64, such as `Basic abc` or unpadded base64, are "no credentials"
instead of a `NullPointerException`.
- **Empty password** (`HttpBasicAuthenticationFilter`): refused with 401
before any bind. A simple bind with a DN and an empty password is an
unauthenticated bind ([RFC 4513
§5.1.2](https://www.rfc-editor.org/rfc/rfc4513#section-5.1.2)). A server
which accepts it would let the request run as the named user through
proxied authorization without checking any password. Before this change,
`user:` from the Basic header was "no credentials" and fell through to
the next authorization mechanism; an empty alternative password header
already reached the bind strategy. The server's own HTTP Basic mechanism
(`HttpBasicAuthorizationMechanism`) uses the same filter and extractor,
so it gets both changes.
- **`%` in templates**: `DnTemplate`, `AuthzIdTemplate` and the
`{username}` templates of the `basic` configuration (`bindDnTemplate`,
`authzIdTemplate`, `filterTemplate`) escape `%` in their fixed parts.
The configuration defaults are now `{username}` and `u:{username}`
instead of the raw format strings `%s` and `u:%s`. A literal `%s`
written in `config.json`, which was never documented, is now kept as
text.
- **`DnTemplate`**: a trailing `,..` is stripped only when its comma is
not escaped (an even number of backslashes before it).
- The reference (`appendix-rest2ldap.adoc`) says how a template which is
just `{username}` or one field is read, and gives the `sasl-plain`
default.
Departures from the issue text: an empty password is parsed but refused
(see above), and a user name which does not give a DN is reported as
`INVALID_CREDENTIALS` (401) rather than `INVALID_DN_SYNTAX`, which maps
to 400.
## Test
- `SimpleBindStrategyTest` (new, in-memory backend): the default
template binds with a DN user name. A template escapes the user name, so
`bjensen,ou=People` cannot add RDNs. A user name which is not a DN fails
the promise with `INVALID_CREDENTIALS` and takes no connection.
- `SaslPlainStrategyTest` (new): the authentication ID sent for `dn:%s`,
`dn: %s`, `dn:uid=%s,...` (escaped) and `u:%s`. A user name which is not
a DN fails without taking a connection or sending a bind.
- `BasicJsonConfigurationTestCase` (new): the `basic` configuration
defaults for `simple` and `sasl-plain`, `dn:{username}`, and `%` in all
three templates, read from the request sent to the server.
- `CredentialExtractorsTest`: `bjensen:se:cret`, `bjensen::`, a DN user
name and `bjensen:`. `testBasicReturnNullOnInvalidCredentials` used to
fill `headers` and then pass `new Headers()`; it now checks the headers
it builds, including a bare `Basic`, `Basic abc` and unpadded base64.
- `HttpBasicAuthenticationFilterTest`: an empty password gives 401 and
the strategy is never called.
- `AuthzIdTemplateTest`, `DnTemplateTest`: `%` in the fixed part,
`dn:{dn}` and `dn: {dn}` with a DN value, `\,..` and `\\,..`.
Results:
- Against master, 27 of the new checks fail, each for the reason the
issue describes.
- With the fix, the whole `opendj-rest2ldap` suite passes (571 tests).
Javadoc doclint passes, and a broken `{@link}` added as a negative
control fails it.
- Nine mutants each turn at least one test red:
`DN.valueOf(String.format(...))` for every template, the empty password
let through, any backslash escaping the comma, the old `%s` default, a
split at the last colon, no `null` check after `Base64.decode`, no trim
after `dn:`, and `getConnectionAsync()` called before the user name is
checked, in `SimpleBindStrategy` and in `SaslPlainStrategy`.
Fixes #1155