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