Merge pull request #799 from sjcorbett/as7-security-0.5-backport
AS7 security 0.5 backport
diff --git a/core/pom.xml b/core/pom.xml
index 111b8ac..ff047fe 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -105,7 +105,12 @@
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
-
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-security</artifactId>
+ <version>${jetty.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<!-- add maxmind geo-ip library; exact copy of source included as required by LGPL2 -->
diff --git a/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java b/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java
index 06081a6..0ac2150 100644
--- a/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java
+++ b/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java
@@ -13,8 +13,12 @@
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
+import com.google.common.base.Optional;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
@@ -23,6 +27,7 @@
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
@@ -108,8 +113,9 @@
private Map<String, String> baseUriVars = Maps.newLinkedHashMap();
private Map<String, String> headers = Maps.newLinkedHashMap();
private boolean suspended = false;
+ private Credentials credentials;
private volatile boolean built;
-
+
public Builder entity(EntityLocal val) {
this.entity = val;
return this;
@@ -167,6 +173,10 @@
this.suspended = startsSuspended;
return this;
}
+ public Builder credentials(String username, String password) {
+ this.credentials = new UsernamePasswordCredentials(username, password);
+ return this;
+ }
public HttpFeed build() {
built = true;
HttpFeed result = new HttpFeed(this);
@@ -185,15 +195,20 @@
final Supplier<URI> uriProvider;
final Map<String,String> headers;
final byte[] body;
+ final Optional<Credentials> credentials;
- private HttpPollIdentifier(String method, URI uri, Map<String,String> headers, byte[] body) {
- this(method, Suppliers2.ofInstance(uri), headers, body);
+ private HttpPollIdentifier(String method, URI uri, Map<String,String> headers, byte[] body,
+ Optional<Credentials> credentials) {
+ this(method, Suppliers2.ofInstance(uri), headers, body, credentials);
}
- private HttpPollIdentifier(String method, Supplier<URI> uriProvider, Map<String,String> headers, byte[] body) {
+
+ private HttpPollIdentifier(String method, Supplier<URI> uriProvider, Map<String, String> headers, byte[] body,
+ Optional<Credentials> credentials) {
this.method = checkNotNull(method, "method").toLowerCase();
this.uriProvider = checkNotNull(uriProvider, "uriProvider");
this.headers = checkNotNull(headers, "headers");
this.body = body;
+ this.credentials = checkNotNull(credentials, "credentials");
if (!(this.method.equals("get") || this.method.equals("post"))) {
throw new IllegalArgumentException("Unsupported HTTP method (only supports GET and POST): "+method);
@@ -205,7 +220,7 @@
@Override
public int hashCode() {
- return Objects.hashCode(method, uriProvider, headers, body);
+ return Objects.hashCode(method, uriProvider, headers, body, credentials);
}
@Override
@@ -217,7 +232,8 @@
return Objects.equal(method, o.method) &&
Objects.equal(uriProvider, o.uriProvider) &&
Objects.equal(headers, o.headers) &&
- Objects.equal(body, o.body);
+ Objects.equal(body, o.body) &&
+ Objects.equal(credentials, o.credentials);
}
}
@@ -235,6 +251,8 @@
String method = config.getMethod();
Map<String,String> headers = config.buildHeaders(baseHeaders);
byte[] body = config.getBody();
+
+ Optional<Credentials> credentials = Optional.fromNullable(builder.credentials);
Supplier<URI> baseUriProvider = builder.baseUriProvider;
if (builder.baseUri!=null) {
@@ -248,13 +266,21 @@
}
checkNotNull(baseUriProvider);
- polls.put(new HttpPollIdentifier(method, baseUriProvider, headers, body), configCopy);
+ polls.put(new HttpPollIdentifier(method, baseUriProvider, headers, body, credentials), configCopy);
}
}
@Override
protected void preStart() {
for (final HttpPollIdentifier pollInfo : polls.keySet()) {
+ // Though HttpClients are thread safe and can take advantage of connection pooling
+ // and authentication caching, the httpcomponents documentation says:
+ // "While HttpClient instances are thread safe and can be shared between multiple
+ // threads of execution, it is highly recommended that each thread maintains its
+ // own dedicated instance of HttpContext.
+ // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html
+ final HttpClient httpClient = createHttpClient(pollInfo);
+
Set<HttpPollConfig<?>> configs = polls.get(pollInfo);
long minPeriod = Integer.MAX_VALUE;
Set<AttributePollHandler<? super HttpPollValue>> handlers = Sets.newLinkedHashSet();
@@ -263,21 +289,6 @@
handlers.add(new AttributePollHandler<HttpPollValue>(config, entity, this));
if (config.getPeriod() > 0) minPeriod = Math.min(minPeriod, config.getPeriod());
}
-
- final DefaultHttpClient httpClient = new DefaultHttpClient();
- URI uri = pollInfo.uriProvider.get();
- // TODO if supplier returns null, we may wish to defer initialization until url available?
- if (uri!=null && "https".equalsIgnoreCase(uri.getScheme())) {
- try {
- int port = (uri.getPort() >= 0) ? uri.getPort() : 443;
- SSLSocketFactory socketFactory = new SSLSocketFactory(new TrustAllStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
- Scheme sch = new Scheme("https", port, socketFactory);
- httpClient.getConnectionManager().getSchemeRegistry().register(sch);
- } catch (Exception e) {
- log.warn("Error in HTTP Feed of {}, setting trust for uri {}", entity, uri);
- throw Exceptions.propagate(e);
- }
- }
Callable<HttpPollValue> pollJob;
@@ -301,6 +312,35 @@
}
}
+ private HttpClient createHttpClient(HttpPollIdentifier pollIdentifier) {
+ final DefaultHttpClient httpClient = new DefaultHttpClient();
+
+ URI uri = pollIdentifier.uriProvider.get();
+ // TODO if supplier returns null, we may wish to defer initialization until url available?
+ if (uri != null && "https".equalsIgnoreCase(uri.getScheme())) {
+ try {
+ int port = (uri.getPort() >= 0) ? uri.getPort() : 443;
+ SSLSocketFactory socketFactory = new SSLSocketFactory(
+ new TrustAllStrategy(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ Scheme sch = new Scheme("https", port, socketFactory);
+ httpClient.getConnectionManager().getSchemeRegistry().register(sch);
+ } catch (Exception e) {
+ log.warn("Error in HTTP Feed of {}, setting trust for uri {}", entity, uri);
+ throw Exceptions.propagate(e);
+ }
+ }
+
+ // Set credentials
+ if (uri != null && pollIdentifier.credentials.isPresent()) {
+ String hostname = uri.getHost();
+ int port = uri.getPort();
+ httpClient.getCredentialsProvider().setCredentials(
+ new AuthScope(hostname, port), pollIdentifier.credentials.get());
+ }
+
+ return httpClient;
+ }
+
@SuppressWarnings("unchecked")
private Poller<HttpPollValue> getPoller() {
return (Poller<HttpPollValue>) poller;
@@ -311,7 +351,7 @@
for (Map.Entry<String,String> entry : headers.entrySet()) {
httpGet.addHeader(entry.getKey(), entry.getValue());
}
-
+
long startTime = System.currentTimeMillis();
HttpResponse httpResponse = httpClient.execute(httpGet);
try {
diff --git a/core/src/test/java/brooklyn/event/feed/http/HttpFeedIntegrationTest.java b/core/src/test/java/brooklyn/event/feed/http/HttpFeedIntegrationTest.java
new file mode 100644
index 0000000..6f8911d
--- /dev/null
+++ b/core/src/test/java/brooklyn/event/feed/http/HttpFeedIntegrationTest.java
@@ -0,0 +1,137 @@
+package brooklyn.event.feed.http;
+
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityLocal;
+import brooklyn.entity.proxying.EntitySpecs;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.location.Location;
+import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import brooklyn.location.basic.PortRanges;
+import brooklyn.test.Asserts;
+import brooklyn.test.EntityTestUtils;
+import brooklyn.test.HttpService;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.test.entity.TestEntity;
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableList;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.net.URI;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+public class HttpFeedIntegrationTest {
+
+ final static AttributeSensor<String> SENSOR_STRING = new BasicAttributeSensor<String>(String.class, "aString", "");
+ final static AttributeSensor<Integer> SENSOR_INT = new BasicAttributeSensor<Integer>(Integer.class, "aLong", "");
+
+ private HttpService httpService;
+
+ private Location loc;
+ private TestApplication app;
+ private EntityLocal entity;
+ private HttpFeed feed;
+
+ @BeforeMethod(alwaysRun=true)
+ public void setUp() throws Exception {
+ loc = new LocalhostMachineProvisioningLocation();
+ app = ApplicationBuilder.newManagedApp(TestApplication.class);
+ entity = app.createAndManageChild(EntitySpecs.spec(TestEntity.class));
+ app.start(ImmutableList.of(loc));
+ }
+
+ @AfterMethod(alwaysRun=true)
+ public void tearDown() throws Exception {
+ if (feed != null) feed.stop();
+ if (httpService != null) httpService.shutdown();
+ if (app != null) Entities.destroyAll(app);
+ }
+
+ @Test(groups = {"Integration"})
+ public void testPollsAndParsesHttpGetResponseWithSsl() throws Exception {
+ httpService = new HttpService(PortRanges.fromString("9000+"), true).start();
+ URI baseUrl = new URI(httpService.getUrl());
+
+ assertEquals(baseUrl.getScheme(), "https", "baseUrl="+baseUrl);
+
+ feed = HttpFeed.builder()
+ .entity(entity)
+ .baseUri(baseUrl)
+ .poll(new HttpPollConfig<Integer>(SENSOR_INT)
+ .period(100)
+ .onSuccess(HttpValueFunctions.responseCode()))
+ .poll(new HttpPollConfig<String>(SENSOR_STRING)
+ .period(100)
+ .onSuccess(HttpValueFunctions.stringContentsFunction()))
+ .build();
+
+ EntityTestUtils.assertAttributeEqualsEventually(entity, SENSOR_INT, 200);
+ Asserts.succeedsEventually(new Runnable() {
+ public void run() {
+ String val = entity.getAttribute(SENSOR_STRING);
+ assertTrue(val != null && val.contains("Hello, World"), "val="+val);
+ }});
+ }
+
+ @Test(groups = {"Integration"})
+ public void testPollsAndParsesHttpGetResponseWithBasicAuthentication() throws Exception {
+ final String username = "brooklyn";
+ final String password = "hunter2";
+ httpService = new HttpService(PortRanges.fromString("9000+"))
+ .basicAuthentication(username, password)
+ .start();
+ URI baseUrl = new URI(httpService.getUrl());
+ assertEquals(baseUrl.getScheme(), "http", "baseUrl="+baseUrl);
+
+ feed = HttpFeed.builder()
+ .entity(entity)
+ .baseUri(baseUrl)
+ .credentials(username, password)
+ .poll(new HttpPollConfig<Integer>(SENSOR_INT)
+ .period(100)
+ .onSuccess(HttpValueFunctions.responseCode()))
+ .poll(new HttpPollConfig<String>(SENSOR_STRING)
+ .period(100)
+ .onSuccess(HttpValueFunctions.stringContentsFunction()))
+ .build();
+
+ EntityTestUtils.assertAttributeEqualsEventually(entity, SENSOR_INT, 200);
+ Asserts.succeedsEventually(new Runnable() {
+ public void run() {
+ String val = entity.getAttribute(SENSOR_STRING);
+ assertTrue(val != null && val.contains("Hello, World"), "val="+val);
+ }});
+ }
+
+ @Test(groups = {"Integration"})
+ public void testPollWithInvalidCredentialsFails() throws Exception {
+ httpService = new HttpService(PortRanges.fromString("9000+"))
+ .basicAuthentication("brooklyn", "hunter2")
+ .start();
+
+ feed = HttpFeed.builder()
+ .entity(entity)
+ .baseUri(httpService.getUrl())
+ .credentials("brooklyn", "9876543210")
+ .poll(new HttpPollConfig<Integer>(SENSOR_INT)
+ .period(100)
+ .onSuccess(HttpValueFunctions.responseCode()))
+ .poll(new HttpPollConfig<String>(SENSOR_STRING)
+ .period(100)
+ .onSuccess(HttpValueFunctions.stringContentsFunction()))
+ .build();
+
+ EntityTestUtils.assertAttributeEqualsEventually(entity, SENSOR_INT, 401);
+ Asserts.succeedsEventually(new Runnable() {
+ public void run() {
+ String val = entity.getAttribute(SENSOR_STRING);
+ assertTrue(val != null && val.contains("Error 401 Unauthorized"), "val=" + val);
+ }
+ });
+ }
+}
diff --git a/core/src/test/java/brooklyn/event/feed/http/HttpFeedSslIntegrationTest.java b/core/src/test/java/brooklyn/event/feed/http/HttpFeedSslIntegrationTest.java
deleted file mode 100644
index 7b367cc..0000000
--- a/core/src/test/java/brooklyn/event/feed/http/HttpFeedSslIntegrationTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package brooklyn.event.feed.http;
-
-import static brooklyn.test.TestUtils.executeUntilSucceeds;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertTrue;
-
-import java.net.URI;
-import java.util.concurrent.Callable;
-
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.basic.EntityLocal;
-import brooklyn.entity.proxying.EntitySpecs;
-import brooklyn.event.basic.BasicAttributeSensor;
-import brooklyn.location.Location;
-import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
-import brooklyn.location.basic.PortRanges;
-import brooklyn.test.EntityTestUtils;
-import brooklyn.test.HttpService;
-import brooklyn.test.entity.TestApplication;
-import brooklyn.test.entity.TestEntity;
-
-import com.google.common.collect.ImmutableList;
-
-public class HttpFeedSslIntegrationTest {
-
- final static BasicAttributeSensor<String> SENSOR_STRING = new BasicAttributeSensor<String>(String.class, "aString", "");
- final static BasicAttributeSensor<Integer> SENSOR_INT = new BasicAttributeSensor<Integer>(Integer.class, "aLong", "");
-
- private static final long TIMEOUT_MS = 10*1000;
-
- private HttpService httpService;
- private URI baseUrl;
-
- private Location loc;
- private TestApplication app;
- private EntityLocal entity;
- private HttpFeed feed;
-
- @BeforeMethod(alwaysRun=true)
- public void setUp() throws Exception {
- httpService = new HttpService(PortRanges.fromString("9000+"), true);
- baseUrl = new URI(httpService.getUrl());
-
- loc = new LocalhostMachineProvisioningLocation();
- app = ApplicationBuilder.newManagedApp(TestApplication.class);
- entity = app.createAndManageChild(EntitySpecs.spec(TestEntity.class));
- app.start(ImmutableList.of(loc));
- }
-
- @AfterMethod(alwaysRun=true)
- public void tearDown() throws Exception {
- if (feed != null) feed.stop();
- if (httpService != null) httpService.shutdown();
- if (app != null) Entities.destroyAll(app);
- }
-
- @Test
- public void testPollsAndParsesHttpGetResponse() throws Exception {
- assertEquals(baseUrl.getScheme(), "https", "baseUrl="+baseUrl);
-
- feed = HttpFeed.builder()
- .entity(entity)
- .baseUri(baseUrl)
- .poll(new HttpPollConfig<Integer>(SENSOR_INT)
- .period(100)
- .onSuccess(HttpValueFunctions.responseCode()))
- .poll(new HttpPollConfig<String>(SENSOR_STRING)
- .period(100)
- .onSuccess(HttpValueFunctions.stringContentsFunction()))
- .build();
-
- EntityTestUtils.assertAttributeEqualsEventually(entity, SENSOR_INT, 200);
- executeUntilSucceeds(new Callable<Void>() {
- public Void call() {
- String val = entity.getAttribute(SENSOR_STRING);
- assertTrue(val != null && val.contains("Hello, World"), "val="+val);
- return null;
- }});
- }
-}
diff --git a/core/src/test/java/brooklyn/test/HttpService.java b/core/src/test/java/brooklyn/test/HttpService.java
index 266cf07..bd665e5 100644
--- a/core/src/test/java/brooklyn/test/HttpService.java
+++ b/core/src/test/java/brooklyn/test/HttpService.java
@@ -5,10 +5,19 @@
import java.net.InetAddress;
import java.security.KeyStore;
+import com.google.common.base.Optional;
+import org.eclipse.jetty.security.HashLoginService;
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import org.eclipse.jetty.security.authentication.DigestAuthenticator;
+import org.eclipse.jetty.security.ConstraintMapping;
+import org.eclipse.jetty.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ssl.SslSocketConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.security.Credential;
+import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.webapp.WebAppContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,17 +49,51 @@
private final Server server;
private volatile Thread shutdownHook;
- public HttpService(PortRange portRange, boolean httpsEnabled) throws Exception {
+ private Optional<? extends SecurityHandler> securityHandler = Optional.absent();
+
+ public HttpService(PortRange portRange) {
+ this(portRange, false);
+ }
+
+ public HttpService(PortRange portRange, boolean httpsEnabled) {
this.httpsEnabled = httpsEnabled;
addr = LocalhostMachineProvisioningLocation.getLocalhostInetAddress();
actualPort = LocalhostMachineProvisioningLocation.obtainPort(addr, portRange);
server = new Server(actualPort);
-
+ }
+
+ /**
+ * Enables basic HTTP authentication on the server.
+ */
+ public HttpService basicAuthentication(String username, String password) {
+ HashLoginService l = new HashLoginService();
+ l.putUser(username, Credential.getCredential(password), new String[]{"user"});
+ l.setName("test-realm");
+
+ Constraint constraint = new Constraint(Constraint.__BASIC_AUTH, "user");
+ constraint.setAuthenticate(true);
+
+ ConstraintMapping constraintMapping = new ConstraintMapping();
+ constraintMapping.setConstraint(constraint);
+ constraintMapping.setPathSpec("/*");
+
+ ConstraintSecurityHandler csh = new ConstraintSecurityHandler();
+ csh.setAuthenticator(new BasicAuthenticator());
+ csh.setRealmName("test-realm");
+ csh.addConstraintMapping(constraintMapping);
+ csh.setLoginService(l);
+
+ this.securityHandler = Optional.of(csh);
+
+ return this;
+ }
+
+ public HttpService start() throws Exception {
try {
- if(httpsEnabled){
+ if (httpsEnabled) {
//by default the server is configured with a http connector, this needs to be removed since we are going
//to provide https
- for(Connector c: server.getConnectors()) {
+ for (Connector c: server.getConnectors()) {
server.removeConnector(c);
}
@@ -84,6 +127,10 @@
context.setContextPath("/");
context.setParentLoaderPriority(true);
+ if (securityHandler.isPresent()) {
+ context.setSecurityHandler(securityHandler.get());
+ }
+
server.setHandler(context);
server.start();
@@ -97,6 +144,8 @@
throw e;
}
}
+
+ return this;
}
public void shutdown() throws Exception {
@@ -139,4 +188,5 @@
}
});
}
+
}
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7Server.java b/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7Server.java
index 8dd8c1d..b1b427e 100644
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7Server.java
+++ b/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7Server.java
@@ -1,6 +1,7 @@
package brooklyn.entity.webapp.jboss;
import brooklyn.catalog.Catalog;
+import brooklyn.config.ConfigKey;
import brooklyn.entity.basic.SoftwareProcess;
import brooklyn.entity.proxying.ImplementedBy;
import brooklyn.entity.trait.HasShortName;
@@ -64,4 +65,15 @@
BasicAttributeSensor<Integer> MANAGEMENT_STATUS =
new BasicAttributeSensor<Integer>(Integer.class, "webapp.jboss.managementStatus", "HTTP response code for the management server");
+
+ @SetFromFlag("managementUser")
+ ConfigKey<String> MANAGEMENT_USER = new BasicConfigKey<String>(String.class,
+ "webapp.jboss.managementUser",
+ "A user to be placed in the management realm. Brooklyn will use this user to poll sensors",
+ "brooklyn");
+
+ @SetFromFlag("managementPassword")
+ ConfigKey<String> MANAGEMENT_PASSWORD =
+ new BasicConfigKey<String>(String.class, "webapp.jboss.managementPassword", "Password for MANAGEMENT_USER.");
+
}
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java b/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java
index 661bc0d..6b6bf24 100644
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java
+++ b/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java
@@ -64,6 +64,7 @@
.entity(this)
.period(200)
.baseUri(managementUri)
+ .credentials(getConfig(MANAGEMENT_USER), getConfig(MANAGEMENT_PASSWORD))
.poll(new HttpPollConfig<Integer>(MANAGEMENT_STATUS)
.onSuccess(HttpValueFunctions.responseCode()))
.poll(new HttpPollConfig<Boolean>(SERVICE_UP)
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7SshDriver.java b/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7SshDriver.java
index 8e615fb..b32695b 100644
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7SshDriver.java
+++ b/software/webapp/src/main/java/brooklyn/entity/webapp/jboss/JBoss7SshDriver.java
@@ -1,29 +1,38 @@
package brooklyn.entity.webapp.jboss;
-import static java.lang.String.format;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
import brooklyn.entity.basic.SoftwareProcess;
-import brooklyn.entity.basic.lifecycle.CommonCommands;
import brooklyn.entity.drivers.downloads.DownloadResolver;
import brooklyn.entity.webapp.JavaWebAppSshDriver;
import brooklyn.location.basic.SshMachineLocation;
import brooklyn.util.MutableMap;
import brooklyn.util.NetworkUtils;
import brooklyn.util.ResourceUtils;
+import brooklyn.util.ssh.CommonCommands;
+import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.hash.Hashing;
+
+import org.apache.commons.codec.binary.Hex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static java.lang.String.format;
public class JBoss7SshDriver extends JavaWebAppSshDriver implements JBoss7Driver {
+ public static final Logger LOG = LoggerFactory.getLogger(JBoss7SshDriver.class);
+
/*
* TODO
- * - security for stats access (see below)
* - expose log file location, or even support accessing them dynamically
* - more configurability of config files, java memory, etc
*/
@@ -31,7 +40,9 @@
public static final String SERVER_TYPE = "standalone";
private static final String CONFIG_FILE = "standalone-brooklyn.xml";
private static final String KEYSTORE_FILE = ".keystore";
-
+
+ private static final String MANAGEMENT_REALM = "ManagementRealm";
+
private String expandedInstallDir;
public JBoss7SshDriver(JBoss7ServerImpl entity, SshMachineLocation machine) {
@@ -79,6 +90,14 @@
return entity.getAttribute(JBoss7Server.MANAGEMENT_NATIVE_PORT);
}
+ private String getManagementUsername() {
+ return entity.getConfig(JBoss7Server.MANAGEMENT_USER);
+ }
+
+ private String getManagementPassword() {
+ return entity.getConfig(JBoss7Server.MANAGEMENT_PASSWORD);
+ }
+
private Integer getPortIncrement() {
return entity.getAttribute(JBoss7Server.PORT_INCREMENT);
}
@@ -123,6 +142,15 @@
*/
@Override
public void customize() {
+ // Check that a password was set for the management user
+ Preconditions.checkState(!Strings.isNullOrEmpty(getManagementUsername()), "User for management realm required");
+ String managementPassword = getManagementPassword();
+ if (Strings.isNullOrEmpty(managementPassword)) {
+ LOG.warn("No password given for "+JBoss7Server.MANAGEMENT_PASSWORD.getName()+". Using a random string instead.");
+ entity.setConfig(JBoss7Server.MANAGEMENT_PASSWORD, UUID.randomUUID().toString());
+ }
+ String hashedPassword = hashPassword(getManagementUsername(), getManagementPassword(), MANAGEMENT_REALM);
+
// Check that ports are all configured
Map<String,Integer> ports = MutableMap.<String,Integer>builder()
.put("managementHttpPort", getManagementHttpPort())
@@ -141,10 +169,13 @@
String hostname = entity.getAttribute(SoftwareProcess.HOSTNAME);
Preconditions.checkNotNull(hostname, "AS 7 entity must set hostname otherwise server will only be visible on localhost");
- // Copy the install files to the run-dir
+ // Copy the install files to the run-dir and add the management user
newScript(CUSTOMIZING)
- .body.append(format("cp -r %s/%s . || exit $!", getExpandedInstallDir(), SERVER_TYPE))
- .execute();
+ .body.append(
+ format("cp -r %s/%s . || exit $!", getExpandedInstallDir(), SERVER_TYPE),
+ format("echo -e '\n%s=%s' >> %s/%s/configuration/mgmt-users.properties",
+ getManagementUsername(), hashedPassword, getRunDir(), SERVER_TYPE)
+ ).execute();
// Copy the keystore across, if there is one
if (isProtocolEnabled("HTTPS")) {
@@ -169,8 +200,8 @@
@Override
public void launch() {
Map flags = MutableMap.of("usePidFile", false);
-
- // We wait for evidence of tomcat running because, using
+
+ // We wait for evidence of tomcat running because, using
// brooklyn.ssh.config.tool.class=brooklyn.util.internal.ssh.cli.SshCliTool,
// we saw the ssh session return before the tomcat process was fully running
// so the process failed to start.
@@ -223,4 +254,27 @@
return options;
}
+ /**
+ * Creates a hash of a username, password and security realm that is suitable for use
+ * with AS7 and Wildfire.
+ * <p/>
+ * Although AS7 has an <code>add-user.sh</code> script it is unsuitable for use in
+ * non-interactive modes. (See AS7-5061 for details.) Versions 7.1.2+ (EAP) accept
+ * a <code>--silent</code> flag. When this entity is updated past 7.1.1 we should
+ * probably use that instead.
+ * <p/>
+ * This method mirrors AS7 and Wildfire's method of hashing user's passwords. Refer
+ * to its class <code>UsernamePasswordHashUtil.generateHashedURP</code> for their
+ * implementation.
+ *
+ * @see <a href="https://issues.jboss.org/browse/AS7-5061">AS7-5061</a>
+ * @see <a href="https://github.com/jboss-remoting/jboss-sasl/blob/master/src/main/java/org/jboss/sasl/util/UsernamePasswordHashUtil.java">
+ * UsernamePasswordHashUtil.generateHashedURP</a>
+ * @return <code>HEX(MD5(username ':' realm ':' password))</code>
+ */
+ public static String hashPassword(String username, String password, String realm) {
+ String concat = username + ":" + realm + ":" + password;
+ byte[] hashed = Hashing.md5().hashString(concat, Charsets.UTF_8).asBytes();
+ return Hex.encodeHexString(hashed);
+ }
}
diff --git a/software/webapp/src/main/resources/brooklyn/entity/webapp/jboss/jboss7-standalone.xml b/software/webapp/src/main/resources/brooklyn/entity/webapp/jboss/jboss7-standalone.xml
index 4763a2a..714de2c 100644
--- a/software/webapp/src/main/resources/brooklyn/entity/webapp/jboss/jboss7-standalone.xml
+++ b/software/webapp/src/main/resources/brooklyn/entity/webapp/jboss/jboss7-standalone.xml
@@ -44,15 +44,9 @@
<native-interface security-realm="ManagementRealm">
<socket-binding native="management-native"/>
</native-interface>
- [#if entity.httpManagementInterfaceSecurityRealm != ""]
- <http-interface security-realm="${entity.httpManagementInterfaceSecurityRealm}">
+ <http-interface security-realm="ManagementRealm">
<socket-binding http="management-http"/>
</http-interface>
- [#else]
- <http-interface>
- <socket-binding http="management-http"/>
- </http-interface>
- [/#if]
</management-interfaces>
</management>
<profile>
diff --git a/software/webapp/src/test/java/brooklyn/entity/webapp/jboss/JBoss7PasswordHashingTest.java b/software/webapp/src/test/java/brooklyn/entity/webapp/jboss/JBoss7PasswordHashingTest.java
new file mode 100644
index 0000000..aabc6a6
--- /dev/null
+++ b/software/webapp/src/test/java/brooklyn/entity/webapp/jboss/JBoss7PasswordHashingTest.java
@@ -0,0 +1,42 @@
+package brooklyn.entity.webapp.jboss;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+/**
+ * Expected values in tests were generated by AS7's add-user.sh script and copied here.
+ */
+public class JBoss7PasswordHashingTest {
+
+ @Test
+ public void testPasswordForManagementRealm() {
+ assertEquals(
+ JBoss7SshDriver.hashPassword("username", "password", "ManagementRealm"),
+ "8959126dd54df47f694cd762a51a1a6f");
+ assertEquals(
+ JBoss7SshDriver.hashPassword("test", "123", "ManagementRealm"),
+ "090d846d31185e54a5e8811a2ccb43ee");
+ }
+
+ @Test
+ public void testPasswordForApplicationRealm() {
+ assertEquals(
+ JBoss7SshDriver.hashPassword("username", "password", "ApplicationRealm"),
+ "888a0504c559a34b1c3e919dcec6d941");
+ assertEquals(
+ JBoss7SshDriver.hashPassword("test", "321", "ApplicationRealm"),
+ "a0fdaa45e2d509ac2d390ff6820e2a10");
+ }
+
+ @Test
+ public void testPasswordForCustomRealm() {
+ assertEquals(
+ JBoss7SshDriver.hashPassword("abcdef", "ghijkl", "BrooklynRealm"),
+ "a65be1ba2eb88b9b9edc6a2a7105af72");
+ assertEquals(
+ JBoss7SshDriver.hashPassword("username", "password", "BrooklynRealm"),
+ "161124b73591a1483330f496311b0692");
+ }
+
+}
diff --git a/software/webapp/src/test/java/brooklyn/entity/webapp/jboss/Jboss7ServerIntegrationTest.java b/software/webapp/src/test/java/brooklyn/entity/webapp/jboss/Jboss7ServerIntegrationTest.java
index 6634198..ffa5c25 100644
--- a/software/webapp/src/test/java/brooklyn/entity/webapp/jboss/Jboss7ServerIntegrationTest.java
+++ b/software/webapp/src/test/java/brooklyn/entity/webapp/jboss/Jboss7ServerIntegrationTest.java
@@ -1,7 +1,22 @@
package brooklyn.entity.webapp.jboss;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertNotNull;
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.proxying.EntitySpecs;
+import brooklyn.entity.webapp.HttpsSslConfig;
+import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
+import brooklyn.test.Asserts;
+import brooklyn.test.HttpTestUtils;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.util.crypto.FluentKeySigner;
+import brooklyn.util.crypto.SecureKeys;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Closeables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
import java.io.File;
import java.io.FileOutputStream;
@@ -9,26 +24,8 @@
import java.security.KeyStore;
import java.security.cert.Certificate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import brooklyn.entity.basic.ApplicationBuilder;
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.proxying.EntitySpecs;
-import brooklyn.entity.webapp.HttpsSslConfig;
-import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
-import brooklyn.test.HttpTestUtils;
-import brooklyn.test.TestUtils;
-import brooklyn.test.entity.TestApplication;
-import brooklyn.util.crypto.FluentKeySigner;
-import brooklyn.util.crypto.SecureKeys;
-import brooklyn.util.internal.TimeExtras;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.io.Closeables;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
/**
* TODO re-write this like WebAppIntegrationTest, rather than being jboss7 specific.
@@ -36,8 +33,6 @@
public class Jboss7ServerIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(Jboss7ServerIntegrationTest.class);
- static { TimeExtras.init(); }
-
private URL warUrl;
private LocalhostMachineProvisioningLocation localhostProvisioningLocation;
private TestApplication app;
@@ -96,7 +91,7 @@
HttpTestUtils.assertUrlUnreachable(httpsUrl);
- TestUtils.executeUntilSucceeds(new Runnable() {
+ Asserts.succeedsEventually(new Runnable() {
public void run() {
assertNotNull(server.getAttribute(JBoss7Server.REQUEST_COUNT));
assertNotNull(server.getAttribute(JBoss7Server.ERROR_COUNT));
@@ -168,7 +163,7 @@
//HttpTestUtils.assertHttpStatusCodeEventuallyEquals(httpsUrl, 200);
//HttpTestUtils.assertContentContainsText(httpsUrl, "Hello");
- TestUtils.executeUntilSucceeds(new Runnable() {
+ Asserts.succeedsEventually(new Runnable() {
public void run() {
assertNotNull(server.getAttribute(JBoss7Server.REQUEST_COUNT));
assertNotNull(server.getAttribute(JBoss7Server.ERROR_COUNT));
@@ -178,4 +173,20 @@
assertNotNull(server.getAttribute(JBoss7Server.BYTES_SENT));
}});
}
+
+ @Test(groups = {"Integration"})
+ public void testUsingPortOffsets() throws Exception {
+ final JBoss7Server serverA = app.createAndManageChild(EntitySpecs.spec(JBoss7Server.class)
+ .configure("portIncrement", 100));
+ final JBoss7Server serverB = app.createAndManageChild(EntitySpecs.spec(JBoss7Server.class)
+ .configure("portIncrement", 200));
+ app.start(ImmutableList.of(localhostProvisioningLocation));
+
+ Asserts.succeedsEventually(new Runnable() {
+ public void run() {
+ assertNotNull(serverA.getAttribute(JBoss7Server.BYTES_SENT));
+ assertNotNull(serverB.getAttribute(JBoss7Server.BYTES_SENT));
+ }});
+ }
+
}
diff --git a/usage/rest/src/main/java/brooklyn/rest/security/BrooklynPropertiesSecurityFilter.java b/usage/rest/src/main/java/brooklyn/rest/security/BrooklynPropertiesSecurityFilter.java
index 6458504..2a41d84 100644
--- a/usage/rest/src/main/java/brooklyn/rest/security/BrooklynPropertiesSecurityFilter.java
+++ b/usage/rest/src/main/java/brooklyn/rest/security/BrooklynPropertiesSecurityFilter.java
@@ -20,6 +20,9 @@
import com.sun.jersey.core.util.Base64;
+/**
+ * Provides basic HTTP authentication.
+ */
public class BrooklynPropertiesSecurityFilter implements Filter {
/** session attribute set for authenticated users; for reference