Skip to content

Commit

Permalink
Refactored the authentication features into the core of the configura…
Browse files Browse the repository at this point in the history
…tion - features have been removed. This is a breaking change, but since this is the 1.0 release, its ok.
  • Loading branch information
cjstehno committed Mar 3, 2017
1 parent 42aaef5 commit 1fe759c
Show file tree
Hide file tree
Showing 18 changed files with 182 additions and 141 deletions.
31 changes: 17 additions & 14 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ A similar test could be written in JUnit with Java 8, as follows (using the prov
----
public class HelloTest {
@Rule public ErsatzServerRule ersatzServer = new ErsatzServerRule(ServerConfig::enableAutoStart);
@Rule
public ErsatzServerRule ersatzServer = new ErsatzServerRule(ServerConfig::enableAutoStart);
private OkHttpClient client;
Expand Down Expand Up @@ -483,7 +484,7 @@ is compressed, a `Content-Encoding` header will be added to the response with th

== HTTPS Request Support

The `ErsatzServer` supports HTTPS requests when the `enableHttps()` configuration is set (either as `enableHttps()` or as `enableHttps true`). This
The `ErsatzServer` supports HTTPS requests when the `https()` configuration is set (either as `https()` or as `https true`). This
will setup both an HTTP and HTTPS listener both of which will have access to all configured expectations. In order to limit a specific request
expectation to HTTP or HTTPS, apply the `procotol(String)` matcher method with the desired protocol, for example:

Expand Down Expand Up @@ -517,44 +518,46 @@ The keystore should then be provided during server configuration as:
[source,groovy]
----
ErsatzServer server = new ErsatzServer({
enableHttps()
https()
keystore KEYSTORE_URL, KEYSTORE_PASS
})
----

where `KEYSTORE_URL` is the URL to your custom keystore file, and `KEYSTORE_PASS` is the password (maybe omitted if you used `ersatz` as the password).

== Feature Extensions
== Authentication

Additional server functionality may be added/configured on the server before startup. The `ServerFeature` interface provides this extension point;
however, the extension feature mechanism is experimental and may change in the future (efforts will be made to maintain a simple migration path if/when
this happens).
Ersatz support two forms of built-in server authentication, BASIC and DIGEST. Both authentication methods are exclusive and global meaning that they
cannot be configured together on the same server and that when configured, they apply to all end points configured on the server.

At this point there are two feature extension, the `BasicAuthFeature` and `DigestAuthFeature`, both discussed below.

TIP: Features are additive, so be careful about how features are applied.
If more fine-grained control of which URLs are authenticated is desired, you will need to configured multiple Ersatz Servers for the different
configuration sets.

=== BASIC Authentication

https://en.wikipedia.org/wiki/Basic_access_authentication[HTTP BASIC Authentication] is supported by applying the `BasicAuthFeature` to the server.
https://en.wikipedia.org/wiki/Basic_access_authentication[HTTP BASIC Authentication] is supported by applying the `basic` `authentication` configuration to the server.

[source,groovy]
----
def ersatz = new ErsatzServer({
feature new BasicAuthFeature()
authentication {
basic 'admin', 'my-password'
}
})
----

This configuration causes the configured request expectations to require BASIC authentication (username and password) as part of their matching.

=== DIGEST Authentication

https://en.wikipedia.org/wiki/Digest_access_authentication[HTTP DIGEST Authentication] is supported by applying the `DigestAuthFeature` to the server.
https://en.wikipedia.org/wiki/Digest_access_authentication[HTTP DIGEST Authentication] is supported by applying the `digest` `authentication` to the server.

[source,groovy]
----
def ersatz = new ErsatzServer({
feature new BasicAuthFeature()
authentication {
digest 'guest', 'other-password'
}
})
----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,11 @@
package com.stehno.ersatz

import groovy.transform.CompileStatic
import io.undertow.server.HttpHandler

/**
* A <code>ServerFeature</code> provides support for additional functionality on the Ersatz server.
* Enumerates the supported authentication types.
*/
@CompileStatic
interface ServerFeature {

/**
* Applies the extended server configuration.
*
* @param handler the extension handler
* @return the wrapped handler
*/
HttpHandler apply(HttpHandler handler)
}
enum Authentication {
BASIC, DIGEST
}
72 changes: 72 additions & 0 deletions src/main/groovy/com/stehno/ersatz/AuthenticationConfig.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2017 Christopher J. Stehno
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.stehno.ersatz

/**
* Configuration object for BASIC and DIGEST authentication support. If the username or password are unspecified or null, they will be "admin" and
* "$3cr3t" respectively.
*
* Only one of BASIC or DIGEST may be specified (last one called wins).
*
* Enabling authentication causes ALL server endpoints to require the configured authentication.
*/
class AuthenticationConfig {

/**
* The configured username. Defaults to "admin".
*/
String username = 'admin'

/**
* The configured password. Defaults to "$3cr3t".
*/
String password = '$3cr3t'

/**
* The configured authentication type.
*/
Authentication type

/**
* Configures BASIC authentication support.
*
* @param username the username or null to use the default
* @param password the password or null to use the default
*/
void basic(final String username = null, final String password = null) {
spec Authentication.BASIC, username, password
}

/**
* Configures DIGEST authentication support.
*
* @param username the username or null to use the default
* @param password the password or null to use the default
*/
void digest(final String username = null, final String password = null) {
spec Authentication.DIGEST, username, password
}

private void spec(final Authentication type, final String username, final String password) {
this.type = type
if (username != null) {
this.username = username
}
if (password != null) {
this.password = password
}
}
}
82 changes: 37 additions & 45 deletions src/main/groovy/com/stehno/ersatz/ErsatzServer.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package com.stehno.ersatz

import com.stehno.ersatz.auth.BasicAuthHandler
import com.stehno.ersatz.auth.DigestAuthHandler
import com.stehno.ersatz.auth.SimpleIdentityManager
import com.stehno.ersatz.impl.ErsatzRequest
import com.stehno.ersatz.impl.ExpectationsImpl
import com.stehno.ersatz.impl.UndertowClientRequest
Expand Down Expand Up @@ -62,8 +65,6 @@ import java.util.function.Function
@CompileStatic @Slf4j
class ErsatzServer implements ServerConfig {

// FIXME: BASIC/DIGEST should just be config options, not features (?)

/**
* The response body returned when no matching expectation could be found.
*/
Expand All @@ -75,7 +76,6 @@ class ErsatzServer implements ServerConfig {
private final RequestDecoders globalDecoders = new RequestDecoders()
private final ResponseEncoders globalEncoders = new ResponseEncoders()
private final ExpectationsImpl expectations = new ExpectationsImpl(globalDecoders, globalEncoders)
private final List<ServerFeature> features = []
private Undertow server
private boolean httpsEnabled
private boolean autoStartEnabled
Expand All @@ -84,6 +84,7 @@ class ErsatzServer implements ServerConfig {
private String keystorePass = 'ersatz'
private int actualHttpPort = UNSPECIFIED_PORT
private int actualHttpsPort = UNSPECIFIED_PORT
private AuthenticationConfig authenticationConfig

/**
* Creates a new Ersatz server instance with either the default configuration or a configuration provided by the Groovy DSL closure.
Expand All @@ -107,24 +108,13 @@ class ErsatzServer implements ServerConfig {
consumer.accept(this)
}

/**
* Used to enable support for a feature extension.
*
* @param feature the <code>ServerFeature</code> to be added
* @return a reference to this server instance
*/
ErsatzServer feature(final ServerFeature feature) {
features << feature
this
}

/**
* Used to control the enabled/disabled state of HTTPS on the server. By default HTTPS is disabled.
*
* @param enabled whether or not HTTPS is enabled (defaults to true if omitted)
* @param enabled optional toggle value (true if not specified)
* @return a reference to the server being configured
*/
ErsatzServer enableHttps(boolean enabled = true) {
ErsatzServer https(boolean enabled = true) {
httpsEnabled = enabled
this
}
Expand All @@ -136,11 +126,10 @@ class ErsatzServer implements ServerConfig {
*
* Auto-start is disabled by default.
*
* @param autoStart whether or not auto-start is enabled
* @param autoStart whether or not auto-start is enabled (true if not specified)
* @return a reference to the server being configured
*/
@Override
ServerConfig enableAutoStart(boolean autoStart = true) {
ServerConfig autoStart(boolean autoStart = true) {
autoStartEnabled = autoStart
this
}
Expand All @@ -159,17 +148,6 @@ class ErsatzServer implements ServerConfig {
this
}

/**
* Used to retrieve the port where the HTTP server is running.
*
* @deprecated Use getHttpPort() instead
* @return the HTTP server port
*/
@Deprecated
int getPort() {
actualHttpPort
}

/**
* Used to retrieve the port where the HTTP server is running.
*
Expand All @@ -188,17 +166,6 @@ class ErsatzServer implements ServerConfig {
actualHttpsPort
}

/**
* Used to retrieve the full URL of the HTTP server.
*
* @deprecated Use getHttpUrl() instead
* @return the full URL of the HTTP server
*/
@Deprecated
String getServerUrl() {
"http://localhost:$actualHttpPort"
}

/**
* Used to retrieve the full URL of the HTTP server.
*
Expand Down Expand Up @@ -294,6 +261,21 @@ class ErsatzServer implements ServerConfig {
this
}

@Override
ServerConfig authentication(@DelegatesTo(AuthenticationConfig) final Closure closure) {
authenticationConfig = new AuthenticationConfig()
closure.delegate = authenticationConfig
closure.call()
this
}

@Override
ServerConfig authentication(final Consumer<AuthenticationConfig> config) {
authenticationConfig = new AuthenticationConfig()
config.accept(authenticationConfig)
return this
}

/**
* Used to start the HTTP server for test interactions. This method should be called after configuration of expectations and before the test
* interactions are executed against the server.
Expand All @@ -307,7 +289,7 @@ class ErsatzServer implements ServerConfig {
}

BlockingHandler blockingHandler = new BlockingHandler(new EncodingHandler(
applyFeatures(new HttpHandler() {
applyAuthentication(new HttpHandler() {
@Override void handleRequest(final HttpServerExchange exchange) throws Exception {
ClientRequest clientRequest = new UndertowClientRequest(exchange)

Expand Down Expand Up @@ -370,11 +352,21 @@ class ErsatzServer implements ServerConfig {
expectations.verify()
}

private HttpHandler applyFeatures(final HttpHandler handler) {
private HttpHandler applyAuthentication(final HttpHandler handler) {
HttpHandler result = handler

features?.each { feat ->
result = feat.apply(result)
if (authenticationConfig) {
SimpleIdentityManager identityManager = new SimpleIdentityManager(authenticationConfig.username, authenticationConfig.password)
switch (authenticationConfig.type) {
case Authentication.BASIC:
result = new BasicAuthHandler(identityManager).apply(result)
break
case Authentication.DIGEST:
result = new DigestAuthHandler(identityManager).apply(result)
break
default:
throw new IllegalArgumentException('Invalid authentication configuration.')
}
}

result
Expand Down

0 comments on commit 1fe759c

Please sign in to comment.