You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by ha...@apache.org on 2015/08/09 04:55:20 UTC

[13/28] incubator-brooklyn git commit: brooklyn-rest-server: add org.apache package prefix

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/brooklyn/rest/util/json/PossiblyStrictPreferringFieldsVisibilityChecker.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/brooklyn/rest/util/json/PossiblyStrictPreferringFieldsVisibilityChecker.java b/usage/rest-server/src/main/java/brooklyn/rest/util/json/PossiblyStrictPreferringFieldsVisibilityChecker.java
deleted file mode 100644
index dc033bc..0000000
--- a/usage/rest-server/src/main/java/brooklyn/rest/util/json/PossiblyStrictPreferringFieldsVisibilityChecker.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.rest.util.json;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.Member;
-import java.lang.reflect.Method;
-
-import org.codehaus.jackson.annotate.JsonAutoDetect;
-import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility;
-import org.codehaus.jackson.annotate.JsonMethod;
-import org.codehaus.jackson.map.introspect.AnnotatedField;
-import org.codehaus.jackson.map.introspect.AnnotatedMember;
-import org.codehaus.jackson.map.introspect.AnnotatedMethod;
-import org.codehaus.jackson.map.introspect.VisibilityChecker;
-
-/** a visibility checker which disables getters, but allows private access,
- * unless {@link BidiSerialization#isStrictSerialization()} is enabled in which case public fields or annotations must be used.
- * <p>
- * the reason for this change to visibility
- * is that getters might generate a copy, resulting in infinite loops, whereas field access should never do so.
- * (see e.g. test in {@link BrooklynJacksonSerializerTest} which uses a sensor+config object whose getTypeToken
- * causes infinite recursion)
- **/
-public class PossiblyStrictPreferringFieldsVisibilityChecker implements VisibilityChecker<PossiblyStrictPreferringFieldsVisibilityChecker> {
-    VisibilityChecker<?> 
-        vizDefault = new VisibilityChecker.Std(Visibility.NONE, Visibility.NONE, Visibility.NONE, Visibility.ANY, Visibility.ANY),
-        vizStrict = new VisibilityChecker.Std(Visibility.NONE, Visibility.NONE, Visibility.NONE, Visibility.PUBLIC_ONLY, Visibility.PUBLIC_ONLY);
-    
-    @Override public PossiblyStrictPreferringFieldsVisibilityChecker with(JsonAutoDetect ann) { throw new UnsupportedOperationException(); }
-    @Override public PossiblyStrictPreferringFieldsVisibilityChecker with(Visibility v) { throw new UnsupportedOperationException(); }
-    @Override public PossiblyStrictPreferringFieldsVisibilityChecker withVisibility(JsonMethod method, Visibility v) { throw new UnsupportedOperationException(); }
-    @Override public PossiblyStrictPreferringFieldsVisibilityChecker withGetterVisibility(Visibility v) { throw new UnsupportedOperationException(); }
-    @Override public PossiblyStrictPreferringFieldsVisibilityChecker withIsGetterVisibility(Visibility v) { throw new UnsupportedOperationException(); }
-    @Override public PossiblyStrictPreferringFieldsVisibilityChecker withSetterVisibility(Visibility v) { throw new UnsupportedOperationException(); }
-    @Override public PossiblyStrictPreferringFieldsVisibilityChecker withCreatorVisibility(Visibility v) { throw new UnsupportedOperationException(); }
-    @Override public PossiblyStrictPreferringFieldsVisibilityChecker withFieldVisibility(Visibility v) { throw new UnsupportedOperationException(); }
-    
-    protected VisibilityChecker<?> viz() {
-        return BidiSerialization.isStrictSerialization() ? vizStrict : vizDefault;
-    }
-    
-    @Override public boolean isGetterVisible(Method m) { 
-        return viz().isGetterVisible(m);
-    }
-
-    @Override
-    public boolean isGetterVisible(AnnotatedMethod m) {
-        return isGetterVisible(m.getAnnotated());
-    }
-
-    @Override
-    public boolean isIsGetterVisible(Method m) {
-        return viz().isIsGetterVisible(m);
-    }
-
-    @Override
-    public boolean isIsGetterVisible(AnnotatedMethod m) {
-        return isIsGetterVisible(m.getAnnotated());
-    }
-
-    @Override
-    public boolean isSetterVisible(Method m) {
-        return viz().isSetterVisible(m);
-    }
-
-    @Override
-    public boolean isSetterVisible(AnnotatedMethod m) {
-        return isSetterVisible(m.getAnnotated());
-    }
-
-    @Override
-    public boolean isCreatorVisible(Member m) {
-        return viz().isCreatorVisible(m);
-    }
-
-    @Override
-    public boolean isCreatorVisible(AnnotatedMember m) {
-        return isCreatorVisible(m.getMember());
-    }
-
-    @Override
-    public boolean isFieldVisible(Field f) {
-        return viz().isFieldVisible(f);
-    }
-
-    @Override
-    public boolean isFieldVisible(AnnotatedField f) {
-        return isFieldVisible(f.getAnnotated());
-    }
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java
new file mode 100644
index 0000000..2e38666
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import brooklyn.rest.apidoc.ApidocHelpMessageBodyWriter;
+import org.apache.brooklyn.rest.resources.AbstractBrooklynRestResource;
+import org.apache.brooklyn.rest.resources.AccessResource;
+import org.apache.brooklyn.rest.resources.ActivityResource;
+import org.apache.brooklyn.rest.resources.ApidocResource;
+import org.apache.brooklyn.rest.resources.ApplicationResource;
+import org.apache.brooklyn.rest.resources.CatalogResource;
+import org.apache.brooklyn.rest.resources.EffectorResource;
+import org.apache.brooklyn.rest.resources.EntityConfigResource;
+import org.apache.brooklyn.rest.resources.EntityResource;
+import org.apache.brooklyn.rest.resources.LocationResource;
+import org.apache.brooklyn.rest.resources.PolicyConfigResource;
+import org.apache.brooklyn.rest.resources.PolicyResource;
+import org.apache.brooklyn.rest.resources.ScriptResource;
+import org.apache.brooklyn.rest.resources.SensorResource;
+import org.apache.brooklyn.rest.resources.ServerResource;
+import org.apache.brooklyn.rest.resources.UsageResource;
+import org.apache.brooklyn.rest.resources.VersionResource;
+import org.apache.brooklyn.rest.util.DefaultExceptionMapper;
+import org.apache.brooklyn.rest.util.FormMapProvider;
+import org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider;
+
+import com.google.common.collect.Iterables;
+
+@SuppressWarnings("deprecation")
+public class BrooklynRestApi {
+
+    public static Iterable<AbstractBrooklynRestResource> getBrooklynRestResources() {
+        List<AbstractBrooklynRestResource> resources = new ArrayList<AbstractBrooklynRestResource>();
+        resources.add(new LocationResource());
+        resources.add(new CatalogResource());
+        resources.add(new ApplicationResource());
+        resources.add(new EntityResource());
+        resources.add(new EntityConfigResource());
+        resources.add(new SensorResource());
+        resources.add(new EffectorResource());
+        resources.add(new PolicyResource());
+        resources.add(new PolicyConfigResource());
+        resources.add(new ActivityResource());
+        resources.add(new AccessResource());
+        resources.add(new ScriptResource());
+        resources.add(new ServerResource());
+        resources.add(new UsageResource());
+        resources.add(new VersionResource());
+        return resources;
+    }
+
+    public static Iterable<Object> getApidocResources() {
+        List<Object> resources = new ArrayList<Object>();
+        resources.add(new ApidocHelpMessageBodyWriter());
+        resources.add(new ApidocResource());
+        return resources;
+    }
+
+    public static Iterable<Object> getMiscResources() {
+        List<Object> resources = new ArrayList<Object>();
+        resources.add(new DefaultExceptionMapper());
+        resources.add(new BrooklynJacksonJsonProvider());
+        resources.add(new FormMapProvider());
+        return resources;
+    }
+
+    public static Iterable<Object> getAllResources() {
+        return Iterables.concat(getBrooklynRestResources(), getApidocResources(), getMiscResources());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynWebConfig.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynWebConfig.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynWebConfig.java
new file mode 100644
index 0000000..aaabaac
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/BrooklynWebConfig.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.config.ConfigMap;
+import brooklyn.config.ConfigPredicates;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.location.PortRange;
+import org.apache.brooklyn.rest.security.provider.DelegatingSecurityProvider;
+import org.apache.brooklyn.rest.security.provider.ExplicitUsersSecurityProvider;
+
+public class BrooklynWebConfig {
+
+    public final static String BASE_NAME = "brooklyn.webconsole";
+    public final static String BASE_NAME_SECURITY = BASE_NAME+".security";
+
+    /**
+     * The security provider to be loaded by {@link DelegatingSecurityProvider}.
+     * e.g. <code>brooklyn.webconsole.security.provider=org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider</code>
+     * will allow anyone to log in.
+     */
+    public final static ConfigKey<String> SECURITY_PROVIDER_CLASSNAME = ConfigKeys.newStringConfigKey(
+            BASE_NAME_SECURITY+".provider", "class name of a Brooklyn SecurityProvider",
+            ExplicitUsersSecurityProvider.class.getCanonicalName());
+    
+    /**
+     * Explicitly set the users/passwords, e.g. in brooklyn.properties:
+     * brooklyn.webconsole.security.users=admin,bob
+     * brooklyn.webconsole.security.user.admin.password=password
+     * brooklyn.webconsole.security.user.bob.password=bobspass
+     */
+    public final static ConfigKey<String> USERS = ConfigKeys.newStringConfigKey(BASE_NAME_SECURITY+".users");
+
+    public final static ConfigKey<String> PASSWORD_FOR_USER(String user) {
+        return ConfigKeys.newStringConfigKey(BASE_NAME_SECURITY + ".user." + user + ".password");
+    }
+    
+    public final static ConfigKey<String> SALT_FOR_USER(String user) {
+        return ConfigKeys.newStringConfigKey(BASE_NAME_SECURITY + ".user." + user + ".salt");
+    }
+    
+    public final static ConfigKey<String> SHA256_FOR_USER(String user) {
+        return ConfigKeys.newStringConfigKey(BASE_NAME_SECURITY + ".user." + user + ".sha256");
+    }
+    
+    public final static ConfigKey<String> LDAP_URL = ConfigKeys.newStringConfigKey(
+            BASE_NAME_SECURITY+".ldap.url");
+
+    public final static ConfigKey<String> LDAP_REALM = ConfigKeys.newStringConfigKey(
+            BASE_NAME_SECURITY+".ldap.realm");
+
+    public final static ConfigKey<String> LDAP_OU = ConfigKeys.newStringConfigKey(
+            BASE_NAME_SECURITY+"ldap.ou");
+
+    public final static ConfigKey<Boolean> HTTPS_REQUIRED = ConfigKeys.newBooleanConfigKey(
+            BASE_NAME+".security.https.required",
+            "Whether HTTPS is required; false here can be overridden by CLI option", false); 
+
+    public final static ConfigKey<PortRange> WEB_CONSOLE_PORT = ConfigKeys.newConfigKey(PortRange.class,
+        BASE_NAME+".port",
+        "Port/range for the web console to listen on; can be overridden by CLI option");
+
+    public final static ConfigKey<String> KEYSTORE_URL = ConfigKeys.newStringConfigKey(
+            BASE_NAME+".security.keystore.url",
+            "Keystore from which to take the certificate to present when running HTTPS; "
+            + "note that normally the password is also required, and an alias for the certificate if the keystore has more than one");
+
+    public final static ConfigKey<String> KEYSTORE_PASSWORD = ConfigKeys.newStringConfigKey(
+            BASE_NAME+".security.keystore.password",
+            "Password for the "+KEYSTORE_URL);
+
+    public final static ConfigKey<String> KEYSTORE_CERTIFICATE_ALIAS = ConfigKeys.newStringConfigKey(
+            BASE_NAME+".security.keystore.certificate.alias",
+            "Alias in "+KEYSTORE_URL+" for the certificate to use; defaults to the first if not supplied");
+
+    public final static ConfigKey<String> TRANSPORT_PROTOCOLS = ConfigKeys.newStringConfigKey(
+            BASE_NAME+".security.transport.protocols",
+            "SSL/TLS protocol versions to use for web console connections",
+            "TLSv1, TLSv1.1, TLSv1.2");
+
+    // https://wiki.mozilla.org/Security/Server_Side_TLS (v3.4)
+    // http://stackoverflow.com/questions/19846020/how-to-map-a-openssls-cipher-list-to-java-jsse
+    // list created on 05.05.2015, Intermediate config from first link
+    public final static ConfigKey<String> TRANSPORT_CIPHERS = ConfigKeys.newStringConfigKey(
+            BASE_NAME+".security.transport.ciphers",
+            "SSL/TLS cipher suites to use for web console connections",
+            "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256," +
+            "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384," +
+            "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_DSS_WITH_AES_128_GCM_SHA256," +
+            "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384," +
+            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256," +
+            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA," +
+            "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384," +
+            "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA," +
+            "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA," +
+            "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA256," +
+            "TLS_DHE_DSS_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA," +
+            "TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_GCM_SHA384," +
+            "TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA256," +
+            "TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA," +
+            "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA,TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA," +
+            "TLS_SRP_SHA_WITH_AES_256_CBC_SHA,TLS_DHE_DSS_WITH_AES_256_CBC_SHA256," +
+            "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA,TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA," +
+            "TLS_SRP_SHA_WITH_AES_128_CBC_SHA,TLS_DHE_DSS_WITH_AES_128_CBC_SHA," +
+            "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA," +
+            "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA," +
+            "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA,TLS_RSA_WITH_CAMELLIA_128_CBC_SHA," +
+            "TLS_RSA_WITH_3DES_EDE_CBC_SHA," +
+            // Same as above but with SSL_ prefix, IBM Java compatibility (cipher is independent of protocol)
+            // https://www-01.ibm.com/support/knowledgecenter/SSYKE2_7.0.0/com.ibm.java.security.component.70.doc/security-component/jsse2Docs/ciphersuites.html
+            "SSL_ECDHE_RSA_WITH_AES_128_GCM_SHA256,SSL_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256," +
+            "SSL_ECDHE_RSA_WITH_AES_256_GCM_SHA384,SSL_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384," +
+            "SSL_DHE_RSA_WITH_AES_128_GCM_SHA256,SSL_DHE_DSS_WITH_AES_128_GCM_SHA256," +
+            "SSL_DHE_DSS_WITH_AES_256_GCM_SHA384,SSL_DHE_RSA_WITH_AES_256_GCM_SHA384," +
+            "SSL_ECDHE_RSA_WITH_AES_128_CBC_SHA256,SSL_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256," +
+            "SSL_ECDHE_RSA_WITH_AES_128_CBC_SHA,SSL_ECDHE_ECDSA_WITH_AES_128_CBC_SHA," +
+            "SSL_ECDHE_RSA_WITH_AES_256_CBC_SHA384,SSL_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384," +
+            "SSL_ECDHE_RSA_WITH_AES_256_CBC_SHA,SSL_ECDHE_ECDSA_WITH_AES_256_CBC_SHA," +
+            "SSL_DHE_RSA_WITH_AES_128_CBC_SHA256,SSL_DHE_RSA_WITH_AES_128_CBC_SHA," +
+            "SSL_DHE_DSS_WITH_AES_128_CBC_SHA256,SSL_DHE_RSA_WITH_AES_256_CBC_SHA256," +
+            "SSL_DHE_DSS_WITH_AES_256_CBC_SHA,SSL_DHE_RSA_WITH_AES_256_CBC_SHA," +
+            "SSL_RSA_WITH_AES_128_GCM_SHA256,SSL_RSA_WITH_AES_256_GCM_SHA384," +
+            "SSL_RSA_WITH_AES_128_CBC_SHA256,SSL_RSA_WITH_AES_256_CBC_SHA256," +
+            "SSL_RSA_WITH_AES_128_CBC_SHA,SSL_RSA_WITH_AES_256_CBC_SHA," +
+            "SSL_SRP_SHA_DSS_WITH_AES_256_CBC_SHA,SSL_SRP_SHA_RSA_WITH_AES_256_CBC_SHA," +
+            "SSL_SRP_SHA_WITH_AES_256_CBC_SHA,SSL_DHE_DSS_WITH_AES_256_CBC_SHA256," +
+            "SSL_SRP_SHA_DSS_WITH_AES_128_CBC_SHA,SSL_SRP_SHA_RSA_WITH_AES_128_CBC_SHA," +
+            "SSL_SRP_SHA_WITH_AES_128_CBC_SHA,SSL_DHE_DSS_WITH_AES_128_CBC_SHA," +
+            "SSL_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA,SSL_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA," +
+            "SSL_RSA_WITH_CAMELLIA_256_CBC_SHA,SSL_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA," +
+            "SSL_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA,SSL_RSA_WITH_CAMELLIA_128_CBC_SHA," +
+            "SSL_RSA_WITH_3DES_EDE_CBC_SHA");
+
+    public final static boolean hasNoSecurityOptions(ConfigMap config) {
+        return config.submap(ConfigPredicates.startingWith(BASE_NAME_SECURITY)).isEmpty();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java
new file mode 100644
index 0000000..368e887
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.filter;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.BrooklynServiceAttributes;
+import brooklyn.management.ManagementContext;
+import brooklyn.management.entitlement.Entitlements;
+import brooklyn.management.entitlement.WebEntitlementContext;
+import org.apache.brooklyn.rest.security.provider.DelegatingSecurityProvider;
+import brooklyn.util.text.Strings;
+
+import com.sun.jersey.core.util.Base64;
+
+/**
+ * Provides basic HTTP authentication.
+ */
+public class BrooklynPropertiesSecurityFilter implements Filter {
+
+    /**
+     * The session attribute set for authenticated users; for reference
+     * (but should not be relied up to confirm authentication, as
+     * the providers may impose additional criteria such as timeouts,
+     * or a null user (no login) may be permitted)
+     */
+    public static final String AUTHENTICATED_USER_SESSION_ATTRIBUTE = "brooklyn.user";
+
+    /**
+     * The session attribute set to indicate the remote address of the HTTP request.
+     * Corresponds to {@link javax.servlet.http.HttpServletRequest#getRemoteAddr()}.
+     */
+    public static final String REMOTE_ADDRESS_SESSION_ATTRIBUTE = "request.remoteAddress";
+
+    private static final Logger log = LoggerFactory.getLogger(BrooklynPropertiesSecurityFilter.class);
+
+    protected DelegatingSecurityProvider provider;
+
+    private static ThreadLocal<String> originalRequest = new ThreadLocal<String>();
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        String uri = httpRequest.getRequestURI();
+
+        if (provider == null) {
+            log.warn("No security provider available: disallowing web access to brooklyn");
+            httpResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+            return;
+        }
+
+        if (originalRequest.get() != null) {
+            // clear the entitlement context before setting to avoid warnings
+            Entitlements.clearEntitlementContext();
+        } else {
+            originalRequest.set(uri);
+        }
+
+        boolean authenticated = provider.isAuthenticated(httpRequest.getSession());
+        if ("/logout".equals(uri) || "/v1/logout".equals(uri)) {
+            httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"brooklyn\"");
+            if (authenticated && httpRequest.getSession().getAttributeNames().hasMoreElements()) {
+                logout(httpRequest);
+                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            } else {
+                RequestDispatcher dispatcher = httpRequest.getRequestDispatcher("/");
+                log.debug("Not authenticated, forwarding request for {} to {}", uri, dispatcher);
+                dispatcher.forward(httpRequest, httpResponse);
+            }
+            return;
+        }
+
+        if (!(httpRequest.getSession().getAttributeNames().hasMoreElements() && provider.isAuthenticated(httpRequest.getSession())) ||
+                "/logout".equals(originalRequest.get())) {
+            authenticated = authenticate(httpRequest);
+        }
+
+        if (!authenticated) {
+            httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"brooklyn\"");
+            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+
+        // Note that the attribute AUTHENTICATED_USER_SESSION_ATTRIBUTE is only set in the call to authenticate(httpRequest),
+        // so must not try to get the user until that is done.
+        String uid = RequestTaggingFilter.getTag();
+        String user = Strings.toString(httpRequest.getSession().getAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE));
+        try {
+            WebEntitlementContext entitlementContext = new WebEntitlementContext(user, httpRequest.getRemoteAddr(), uri, uid);
+            Entitlements.setEntitlementContext(entitlementContext);
+
+            chain.doFilter(request, response);
+        } catch (Throwable e) {
+            if (!response.isCommitted()) {
+                httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            }
+        } finally {
+            originalRequest.remove();
+            Entitlements.clearEntitlementContext();
+        }
+    }
+
+    protected boolean authenticate(HttpServletRequest request) {
+        HttpSession session = request.getSession();
+        if (provider.isAuthenticated(session)) {
+            return true;
+        }
+        session.setAttribute(REMOTE_ADDRESS_SESSION_ATTRIBUTE, request.getRemoteAddr());
+        String user = null, pass = null;
+        String authorization = request.getHeader("Authorization");
+        if (authorization != null) {
+            String userpass = Base64.base64Decode(authorization.substring(6));
+            user = userpass.substring(0, userpass.indexOf(":"));
+            pass = userpass.substring(userpass.indexOf(":") + 1);
+        }
+        if (provider.authenticate(session, user, pass)) {
+            if (user != null) {
+                session.setAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE, user);
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public void init(FilterConfig config) throws ServletException {
+        ManagementContext mgmt = (ManagementContext) config.getServletContext().getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT);
+        provider = new DelegatingSecurityProvider(mgmt);
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    protected void logout(HttpServletRequest request) {
+        log.info("REST logging {} out of session {}",
+                request.getSession().getAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE), request.getSession().getId());
+        provider.logout(request.getSession());
+        request.getSession().removeAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE);
+        request.getSession().removeAttribute(REMOTE_ADDRESS_SESSION_ATTRIBUTE);
+        request.getSession().invalidate();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java
new file mode 100644
index 0000000..592a354
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.filter;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.management.ManagementContext;
+import brooklyn.management.ha.ManagementNodeState;
+import brooklyn.rest.domain.ApiError;
+import brooklyn.util.text.Strings;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.sun.jersey.api.model.AbstractMethod;
+import com.sun.jersey.spi.container.ContainerRequest;
+import com.sun.jersey.spi.container.ContainerRequestFilter;
+import com.sun.jersey.spi.container.ContainerResponseFilter;
+import com.sun.jersey.spi.container.ResourceFilter;
+import com.sun.jersey.spi.container.ResourceFilterFactory;
+
+/** 
+ * Checks that if the method or resource class corresponding to a request
+ * has a {@link HaHotStateRequired} annotation,
+ * that the server is in that state (and up). 
+ * Requests with {@link #SKIP_CHECK_HEADER} set as a header skip this check.
+ * <p>
+ * This follows a different pattern to {@link HaMasterCheckFilter} 
+ * as this needs to know the method being invoked. 
+ */
+public class HaHotCheckResourceFilter implements ResourceFilterFactory {
+    
+    private static final Logger log = LoggerFactory.getLogger(HaHotCheckResourceFilter.class);
+    
+    private static final Set<ManagementNodeState> HOT_STATES = ImmutableSet.of(
+            ManagementNodeState.MASTER, ManagementNodeState.HOT_STANDBY, ManagementNodeState.HOT_BACKUP);
+
+    @Context
+    private ManagementContext mgmt;
+
+    public HaHotCheckResourceFilter() {}
+    
+    @VisibleForTesting
+    public HaHotCheckResourceFilter(ManagementContext mgmt) {
+        this.mgmt = mgmt;
+    }
+    
+    private static class MethodFilter implements ResourceFilter, ContainerRequestFilter {
+
+        private AbstractMethod am;
+        private ManagementContext mgmt;
+
+        public MethodFilter(AbstractMethod am, ManagementContext mgmt) {
+            this.am = am;
+            this.mgmt = mgmt;
+        }
+
+        @Override
+        public ContainerRequestFilter getRequestFilter() {
+            return this;
+        }
+
+        @Override
+        public ContainerResponseFilter getResponseFilter() {
+            return null;
+        }
+
+        private String lookForProblem(ContainerRequest request) {
+            if (isSkipCheckHeaderSet(request)) 
+                return null;
+            
+            if (!isHaHotStateRequired(request))
+                return null;
+            
+            String problem = HaMasterCheckFilter.lookForProblemIfServerNotRunning(mgmt);
+            if (Strings.isNonBlank(problem)) 
+                return problem;
+            
+            if (!isHaHotStatus())
+                return "server not in required HA hot state";
+            if (isStateNotYetValid())
+                return "server not yet completed loading data for required HA hot state";
+            
+            return null;
+        }
+        
+        @Override
+        public ContainerRequest filter(ContainerRequest request) {
+            String problem = lookForProblem(request);
+            if (Strings.isNonBlank(problem)) {
+                log.warn("Disallowing web request as "+problem+": "+request+"/"+am+" (caller should set '"+HaMasterCheckFilter.SKIP_CHECK_HEADER+"' to force)");
+                throw new WebApplicationException(ApiError.builder()
+                    .message("This request is only permitted against an active hot Brooklyn server")
+                    .errorCode(Response.Status.FORBIDDEN).build().asJsonResponse());
+            }
+            return request;
+        }
+
+        // Maybe there should be a separate state to indicate that we have switched state
+        // but still haven't finished rebinding. (Previously there was a time delay and an
+        // isRebinding check, but introducing RebindManager#isAwaitingInitialRebind() seems cleaner.)
+        private boolean isStateNotYetValid() {
+            return mgmt.getRebindManager().isAwaitingInitialRebind();
+        }
+
+        private boolean isHaHotStateRequired(ContainerRequest request) {
+            return (am.getAnnotation(HaHotStateRequired.class) != null ||
+                    am.getResource().getAnnotation(HaHotStateRequired.class) != null);
+        }
+
+        private boolean isSkipCheckHeaderSet(ContainerRequest request) {
+            return "true".equalsIgnoreCase(request.getHeaderValue(HaMasterCheckFilter.SKIP_CHECK_HEADER));
+        }
+
+        private boolean isHaHotStatus() {
+            ManagementNodeState state = mgmt.getHighAvailabilityManager().getNodeState();
+            return HOT_STATES.contains(state);
+        }
+
+    }
+
+    @Override
+    public List<ResourceFilter> create(AbstractMethod am) {
+        return Collections.<ResourceFilter>singletonList(new MethodFilter(am, mgmt));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaHotStateRequired.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaHotStateRequired.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaHotStateRequired.java
new file mode 100644
index 0000000..f64abdd
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaHotStateRequired.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.filter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * When a REST method (or its containing class) is marked with this annotation
+ * requests to it will fail with a 403 response if the instance is not in MASTER
+ * mode (or has recently switched or is still rebinding). Guards the method so
+ * that when it returns the caller can be certain of the response. For example
+ * if the response is 404, then the resource doesn't exist as opposed to
+ * not being loaded from persistence store yet.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface HaHotStateRequired {}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaMasterCheckFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaMasterCheckFilter.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaMasterCheckFilter.java
new file mode 100644
index 0000000..861a7e6
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaMasterCheckFilter.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.filter;
+
+import java.io.IOException;
+import java.util.Set;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.BrooklynServiceAttributes;
+import brooklyn.management.ManagementContext;
+import brooklyn.management.ha.ManagementNodeState;
+import brooklyn.rest.domain.ApiError;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+import brooklyn.util.text.Strings;
+
+import com.google.common.collect.Sets;
+
+/**
+ * Checks that for requests that want HA master state, the server is up and in that state.
+ * <p>
+ * Post POSTs and PUTs are assumed to need master state, with the exception of shutdown.
+ * Requests with {@link #SKIP_CHECK_HEADER} set as a header skip this check.
+ */
+public class HaMasterCheckFilter implements Filter {
+
+    private static final Logger log = LoggerFactory.getLogger(HaMasterCheckFilter.class);
+    
+    public static final String SKIP_CHECK_HEADER = "Brooklyn-Allow-Non-Master-Access";
+    private static final Set<String> SAFE_STANDBY_METHODS = Sets.newHashSet("GET", "HEAD");
+
+    protected ServletContext servletContext;
+    protected ManagementContext mgmt;
+
+    @Override
+    public void init(FilterConfig config) throws ServletException {
+        servletContext = config.getServletContext();
+        mgmt = (ManagementContext) servletContext.getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT);
+    }
+
+    static String lookForProblemIfServerNotRunning(ManagementContext mgmt) {
+        if (mgmt==null) return "no management context available";
+        if (!mgmt.isRunning()) return "server no longer running";
+        if (!mgmt.isStartupComplete()) return "server not in required startup-completed state";
+        return null;
+    }
+    
+    private String lookForProblem(ServletRequest request) {
+        if (isSkipCheckHeaderSet(request)) 
+            return null;
+        
+        if (!isMasterRequiredForRequest(request))
+            return null;
+        
+        String problem = lookForProblemIfServerNotRunning(mgmt);
+        if (Strings.isNonBlank(problem)) 
+            return problem;
+        
+        if (!isMaster()) 
+            return "server not in required HA master state";
+        
+        return null;
+    }
+    
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        String problem = lookForProblem(request);
+        if (problem!=null) {
+            log.warn("Disallowing web request as "+problem+": "+request.getParameterMap()+" (caller should set '"+SKIP_CHECK_HEADER+"' to force)");
+            WebResourceUtils.applyJsonResponse(servletContext, ApiError.builder()
+                .message("This request is only permitted against an active master Brooklyn server")
+                .errorCode(Response.Status.FORBIDDEN).build().asJsonResponse(), (HttpServletResponse)response);
+        } else {
+            chain.doFilter(request, response);
+        }
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    private boolean isMaster() {
+        return ManagementNodeState.MASTER.equals(mgmt.getHighAvailabilityManager().getNodeState());
+    }
+
+    private boolean isMasterRequiredForRequest(ServletRequest request) {
+        if (request instanceof HttpServletRequest) {
+            HttpServletRequest httpRequest = (HttpServletRequest) request;
+            
+            String method = httpRequest.getMethod().toUpperCase();
+            // gets usually okay
+            if (SAFE_STANDBY_METHODS.contains(method)) return false;
+            
+            // explicitly allow calls to shutdown
+            // (if stopAllApps is specified, the method itself will fail; but we do not want to consume parameters here, that breaks things!)
+            // TODO combine with HaHotCheckResourceFilter and use an annotation HaAnyStateAllowed or similar
+            if ("/v1/server/shutdown".equals(httpRequest.getRequestURI())) return false;
+            
+            // master required for everything else
+            return true;
+        }
+        // previously non-HttpServletRequests were allowed but I don't think they should be
+        return true;
+    }
+
+    private boolean isSkipCheckHeaderSet(ServletRequest httpRequest) {
+        if (httpRequest instanceof HttpServletRequest)
+            return "true".equalsIgnoreCase(((HttpServletRequest)httpRequest).getHeader(SKIP_CHECK_HEADER));
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/LoggingFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/LoggingFilter.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/LoggingFilter.java
new file mode 100644
index 0000000..96eaecb
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/LoggingFilter.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.filter;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.Sets;
+
+import brooklyn.config.BrooklynLogging;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.time.Duration;
+
+/**
+ * Handles logging of request information.
+ */
+public class LoggingFilter implements Filter {
+
+    private static final Logger LOG = LoggerFactory.getLogger(BrooklynLogging.REST);
+
+    /** Methods logged at trace. */
+    private static final Set<String> UNINTERESTING_METHODS = Sets.newHashSet("GET", "HEAD");
+
+    /** Headers whose values will not be logged. */
+    private static final Set<String> CENSORED_HEADERS = Sets.newHashSet("Authorization");
+
+    /** Log all requests that take this time or longer to complete. */
+    private static final Duration REQUEST_DURATION_LOG_POINT = Duration.FIVE_SECONDS;
+
+    public void init(FilterConfig config) throws ServletException {
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        String rid = RequestTaggingFilter.getTag();
+        boolean isInteresting = !UNINTERESTING_METHODS.contains(httpRequest.getMethod().toUpperCase());
+        boolean shouldLog = (isInteresting && LOG.isDebugEnabled()) || LOG.isTraceEnabled();
+        boolean requestErrored = false;
+        if (shouldLog) {
+            String message = "Request {} starting: {} {} from {}";
+            Object[] args = new Object[]{rid, httpRequest.getMethod(), httpRequest.getRequestURI(), httpRequest.getRemoteAddr()};
+            if (isInteresting) {
+                LOG.debug(message, args);
+            } else {
+                LOG.trace(message, args);
+            }
+        }
+
+        Stopwatch timer = Stopwatch.createStarted();
+        try {
+            chain.doFilter(request, response);
+        } catch (Throwable e) {
+            requestErrored = true;
+            isInteresting = true;
+            LOG.warn("Request " + rid + " ("+httpRequest.getMethod()+" "+httpRequest.getRequestURI()+" from "+httpRequest.getRemoteAddr()+") failed: " + e, e);
+            // Propagate for handling by other filter
+            throw Exceptions.propagate(e);
+        } finally {
+            timer.stop();
+            // This logging must not happen before chain.doFilter, or FormMapProvider will not work as expected.
+            // Getting the parameter map consumes the request body and only resource methods using @FormParam
+            // will work as expected.
+            isInteresting |= (timer.elapsed(TimeUnit.SECONDS) - REQUEST_DURATION_LOG_POINT.toSeconds()) > 0;
+            if (shouldLog) {
+                boolean includeHeaders = requestErrored || httpResponse.getStatus() / 100 == 5 || LOG.isTraceEnabled();
+                String message = getRequestCompletedMessage(includeHeaders, Duration.of(timer), rid, httpRequest, httpResponse);
+                if (requestErrored || isInteresting) {
+                    LOG.debug(message);
+                } else {
+                    LOG.trace(message);
+                }
+            }
+        }
+    }
+
+    private String getRequestCompletedMessage(boolean includeHeaders, Duration elapsed,
+            String id, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+        StringBuilder message = new StringBuilder("Request ")
+                .append(id)
+                .append(" completed in ")
+                .append(elapsed)
+                .append(": response ")
+                .append(httpResponse.getStatus())
+                .append(" for ")
+                .append(httpRequest.getMethod())
+                .append(" ")
+                .append(httpRequest.getRequestURI())
+                .append(" from ")
+                .append(httpRequest.getRemoteAddr());
+
+        if (!httpRequest.getParameterMap().isEmpty()) {
+            message.append(", parameters: ")
+                    .append(Joiner.on(", ").withKeyValueSeparator("=").join(httpRequest.getParameterMap()));
+        }
+        if (httpRequest.getContentLength() > 0) {
+            int len = httpRequest.getContentLength();
+            message.append(" contentType=").append(httpRequest.getContentType())
+                    .append(" (length=").append(len).append(")");
+        }
+        if (includeHeaders) {
+            Enumeration<String> headerNames = httpRequest.getHeaderNames();
+            if (headerNames.hasMoreElements()) {
+                message.append(", headers: ");
+                while (headerNames.hasMoreElements()) {
+                    String headerName = headerNames.nextElement();
+                    message.append(headerName).append(": ");
+                    if (CENSORED_HEADERS.contains(headerName)) {
+                        message.append("******");
+                    } else {
+                        message.append(httpRequest.getHeader(headerName));
+                    }
+                    if (headerNames.hasMoreElements()) {
+                        message.append(", ");
+                    }
+                }
+            }
+        }
+
+        return message.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java
new file mode 100644
index 0000000..8a3c1c6
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.filter;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+
+import com.sun.jersey.spi.container.ContainerRequest;
+import com.sun.jersey.spi.container.ContainerResponse;
+import com.sun.jersey.spi.container.ContainerResponseFilter;
+
+public class NoCacheFilter implements ContainerResponseFilter {
+
+    @Override
+    public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
+        //https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching_FAQ
+        MultivaluedMap<String, Object> headers = response.getHttpHeaders();
+        headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache, no-store");
+        headers.putSingle("Pragma", "no-cache");
+        headers.putSingle(HttpHeaders.EXPIRES, "0");
+        return response;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingFilter.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingFilter.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingFilter.java
new file mode 100644
index 0000000..5a21b8f
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingFilter.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.filter;
+
+import java.io.IOException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import brooklyn.util.text.Identifiers;
+
+/**
+ * Tags each request with a probabilistically unique id. Should be included before other
+ * filters to make sense.
+ */
+public class RequestTaggingFilter implements Filter {
+
+    private static ThreadLocal<String> tag = new ThreadLocal<String>();
+
+    protected static String getTag() {
+        return tag.get();
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        String requestId = Identifiers.makeRandomId(6);
+        tag.set(requestId);
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            tag.remove();
+        }
+    }
+
+    @Override
+    public void init(FilterConfig config) throws ServletException {
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/AbstractBrooklynRestResource.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/AbstractBrooklynRestResource.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/AbstractBrooklynRestResource.java
new file mode 100644
index 0000000..37a48e6
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/AbstractBrooklynRestResource.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.resources;
+
+import io.brooklyn.camp.CampPlatform;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.Context;
+
+import org.codehaus.jackson.map.ObjectMapper;
+
+import brooklyn.config.BrooklynServerConfig;
+import brooklyn.config.BrooklynServiceAttributes;
+import brooklyn.config.render.RendererHints;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.EntityLocal;
+import brooklyn.management.ManagementContext;
+import brooklyn.management.ManagementContextInjectable;
+import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+import org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider;
+import brooklyn.util.guava.Maybe;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.time.Duration;
+
+public abstract class AbstractBrooklynRestResource implements ManagementContextInjectable {
+
+    // can be injected by jersey when ManagementContext in not injected manually
+    // (seems there is no way to make this optional so note it _must_ be injected;
+    // most of the time that happens for free, but with test framework it doesn't,
+    // so we have set up a NullServletContextProvider in our tests) 
+    @Context ServletContext servletContext;
+    
+    private ManagementContext managementContext;
+    private BrooklynRestResourceUtils brooklynRestResourceUtils;
+    private ObjectMapper mapper;
+
+    public ManagementContext mgmt() {
+        return mgmtMaybe().get();
+    }
+    
+    protected synchronized Maybe<ManagementContext> mgmtMaybe() {
+        if (managementContext!=null) return Maybe.of(managementContext);
+        managementContext = (ManagementContext) servletContext.getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT);
+        if (managementContext!=null) return Maybe.of(managementContext);
+        
+        return Maybe.absent("ManagementContext not available for Brooklyn Jersey Resource "+this);
+    }
+    
+    public void injectManagementContext(ManagementContext managementContext) {
+        if (this.managementContext!=null) {
+            if (this.managementContext.equals(managementContext)) return;
+            throw new IllegalStateException("ManagementContext cannot be changed: specified twice for Brooklyn Jersey Resource "+this);
+        }
+        this.managementContext = managementContext;
+    }
+
+    public synchronized BrooklynRestResourceUtils brooklyn() {
+        if (brooklynRestResourceUtils!=null) return brooklynRestResourceUtils;
+        brooklynRestResourceUtils = new BrooklynRestResourceUtils(mgmt());
+        return brooklynRestResourceUtils;
+    }
+    
+    protected ObjectMapper mapper() {
+        if (mapper==null)
+            mapper = BrooklynJacksonJsonProvider.findAnyObjectMapper(servletContext, managementContext);
+        return mapper;
+    }
+
+    /** @deprecated since 0.7.0 use {@link #getValueForDisplay(Object, boolean, boolean, Boolean, EntityLocal, Duration)} */ @Deprecated
+    protected Object getValueForDisplay(Object value, boolean preferJson, boolean isJerseyReturnValue) {
+        return resolving(value).preferJson(preferJson).asJerseyOutermostReturnValue(isJerseyReturnValue).resolve();
+    }
+
+    protected RestValueResolver resolving(Object v) {
+        return new RestValueResolver(v).mapper(mapper());
+    }
+
+    public static class RestValueResolver {
+        final private Object valueToResolve;
+        private @Nullable ObjectMapper mapper;
+        private boolean preferJson;
+        private boolean isJerseyReturnValue;
+        private @Nullable Boolean raw; 
+        private @Nullable Entity entity;
+        private @Nullable Duration timeout;
+        private @Nullable Object rendererHintSource;
+        
+        public static RestValueResolver resolving(Object v) { return new RestValueResolver(v); }
+        
+        private RestValueResolver(Object v) { valueToResolve = v; }
+        
+        public RestValueResolver mapper(ObjectMapper mapper) { this.mapper = mapper; return this; }
+        
+        /** whether JSON is the ultimate product; 
+         * main effect here is to give null for null if true, else to give empty string 
+         * <p>
+         * conversion to JSON for complex types is done subsequently (often by the framework)
+         * <p>
+         * default is true */
+        public RestValueResolver preferJson(boolean preferJson) { this.preferJson = preferJson; return this; }
+        /** whether an outermost string must be wrapped in quotes, because a String return object is treated as
+         * already JSON-encoded
+         * <p>
+         * default is false */
+        public RestValueResolver asJerseyOutermostReturnValue(boolean asJerseyReturnJson) {
+            isJerseyReturnValue = asJerseyReturnJson;
+            return this;
+        }
+        public RestValueResolver raw(Boolean raw) { this.raw = raw; return this; }
+        public RestValueResolver context(Entity entity) { this.entity = entity; return this; }
+        public RestValueResolver timeout(Duration timeout) { this.timeout = timeout; return this; }
+        public RestValueResolver renderAs(Object rendererHintSource) { this.rendererHintSource = rendererHintSource; return this; }
+
+        public Object resolve() {
+            Object valueResult = getImmediateValue(valueToResolve, entity);
+            if (valueResult==UNRESOLVED) valueResult = valueToResolve;
+            if (rendererHintSource!=null && Boolean.FALSE.equals(raw)) {
+                valueResult = RendererHints.applyDisplayValueHintUnchecked(rendererHintSource, valueResult);
+            }
+            return WebResourceUtils.getValueForDisplay(mapper, valueResult, preferJson, isJerseyReturnValue);
+        }
+        
+        private static Object UNRESOLVED = "UNRESOLVED".toCharArray();
+        
+        private static Object getImmediateValue(Object value, @Nullable Entity context) {
+            return Tasks.resolving(value).as(Object.class).defaultValue(UNRESOLVED).timeout(Duration.ZERO).context(context).swallowExceptions().get();
+        }
+
+    }
+
+    protected CampPlatform camp() {
+        return BrooklynServerConfig.getCampPlatform(mgmt()).get();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/AccessResource.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/AccessResource.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/AccessResource.java
new file mode 100644
index 0000000..a948254
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/AccessResource.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.resources;
+
+import javax.ws.rs.core.Response;
+
+import brooklyn.management.internal.AccessManager;
+import brooklyn.management.internal.ManagementContextInternal;
+import brooklyn.rest.api.AccessApi;
+import brooklyn.rest.domain.AccessSummary;
+import org.apache.brooklyn.rest.transform.AccessTransformer;
+
+import com.google.common.annotations.Beta;
+
+@Beta
+public class AccessResource extends AbstractBrooklynRestResource implements AccessApi {
+
+    @Override
+    public AccessSummary get() {
+        AccessManager accessManager = ((ManagementContextInternal) mgmt()).getAccessManager();
+        return AccessTransformer.accessSummary(accessManager);
+    }
+
+    @Override
+    public Response locationProvisioningAllowed(boolean allowed) {
+        AccessManager accessManager = ((ManagementContextInternal) mgmt()).getAccessManager();
+        accessManager.setLocationProvisioningAllowed(allowed);
+        return Response.status(Response.Status.OK).build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ActivityResource.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ActivityResource.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ActivityResource.java
new file mode 100644
index 0000000..53a49d3
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ActivityResource.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.resources;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import brooklyn.entity.basic.BrooklynTaskTags;
+import brooklyn.entity.basic.BrooklynTaskTags.WrappedStream;
+import brooklyn.management.HasTaskChildren;
+import brooklyn.management.Task;
+import brooklyn.rest.api.ActivityApi;
+import brooklyn.rest.domain.TaskSummary;
+import org.apache.brooklyn.rest.transform.TaskTransformer;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+
+public class ActivityResource extends AbstractBrooklynRestResource implements ActivityApi {
+
+    @Override
+    public TaskSummary get(String taskId) {
+        Task<?> t = mgmt().getExecutionManager().getTask(taskId);
+        if (t == null)
+            throw WebResourceUtils.notFound("Cannot find task '%s'", taskId);
+        return TaskTransformer.FROM_TASK.apply(t);
+    }
+
+    @Override
+    public List<TaskSummary> children(String taskId) {
+        Task<?> t = mgmt().getExecutionManager().getTask(taskId);
+        if (t == null)
+            throw WebResourceUtils.notFound("Cannot find task '%s'", taskId);
+        if (!(t instanceof HasTaskChildren))
+            return Collections.emptyList();
+        return new LinkedList<TaskSummary>(Collections2.transform(Lists.newArrayList(((HasTaskChildren) t).getChildren()),
+                TaskTransformer.FROM_TASK));
+    }
+
+    public String stream(String taskId, String streamId) {
+        Task<?> t = mgmt().getExecutionManager().getTask(taskId);
+        if (t == null)
+            throw WebResourceUtils.notFound("Cannot find task '%s'", taskId);
+        WrappedStream stream = BrooklynTaskTags.stream(t, streamId);
+        if (stream == null)
+            throw WebResourceUtils.notFound("Cannot find stream '%s' in task '%s'", streamId, taskId);
+        return stream.streamContents.get();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java
----------------------------------------------------------------------
diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java
new file mode 100644
index 0000000..16292bd
--- /dev/null
+++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApidocResource.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.rest.resources;
+
+import brooklyn.rest.apidoc.Apidoc;
+
+import javax.ws.rs.Path;
+
+@Apidoc("API Documentation")
+@Path("/v1/apidoc")
+public class ApidocResource extends brooklyn.rest.apidoc.ApidocResource {
+
+}