diff --git a/karaf/apache-brooklyn/src/main/resources/etc/org.ops4j.pax.web.cfg b/karaf/apache-brooklyn/src/main/resources/etc/org.ops4j.pax.web.cfg new file mode 100644 index 0000000000..39947eb1d1 --- /dev/null +++ b/karaf/apache-brooklyn/src/main/resources/etc/org.ops4j.pax.web.cfg @@ -0,0 +1,19 @@ +# +# 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. +# +# TODO use the PortService - ${port:8081,8200} +org.osgi.service.http.port=8081 + diff --git a/karaf/features/src/main/feature/feature.xml b/karaf/features/src/main/feature/feature.xml index c049f47bd9..c017baccb0 100644 --- a/karaf/features/src/main/feature/feature.xml +++ b/karaf/features/src/main/feature/feature.xml @@ -164,7 +164,7 @@ brooklyn-camp-base brooklyn-utils-rest-swagger - jetty + pax-jetty @@ -188,6 +188,8 @@ + pax-jetty + mvn:org.apache.brooklyn/brooklyn-karaf-jetty-config/${project.version} mvn:org.apache.brooklyn/brooklyn-jsgui/${project.version}/war war diff --git a/karaf/itest/pom.xml b/karaf/itest/pom.xml index a09503bb5a..92f0d2d7c2 100644 --- a/karaf/itest/pom.xml +++ b/karaf/itest/pom.xml @@ -30,6 +30,11 @@ ../pom.xml + + + org.apache.brooklyn.test.IntegrationTest + + @@ -154,6 +159,15 @@ + + ${project.groupId} + brooklyn-features + ${project.version} + xml + features + test + + ${project.groupId} apache-brooklyn @@ -167,6 +181,12 @@ ${geronimo-jta_1.1_spec.version} test + + ${project.groupId} + brooklyn-rest-resources + ${project.version} + test + ${project.groupId} brooklyn-rest-resources @@ -180,6 +200,13 @@ ${project.version} test + + + org.ops4j.pax.tinybundles + tinybundles + ${tinybundles.version} + test + @@ -187,7 +214,6 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 ${maven.compiler.source} ${maven.compiler.target} @@ -211,12 +237,23 @@ maven-surefire-plugin - - ${settings.localRepository} - + ${includedTestGroups} + ${excludedTestGroups} + + ${settings.localRepository} + - + + + + Integration + + org.apache.brooklyn.test.IntegrationTest + + + + diff --git a/karaf/itest/src/test/java/org/apache/brooklyn/AssemblyTest.java b/karaf/itest/src/test/java/org/apache/brooklyn/AssemblyTest.java index 22a4018666..398a374ddc 100644 --- a/karaf/itest/src/test/java/org/apache/brooklyn/AssemblyTest.java +++ b/karaf/itest/src/test/java/org/apache/brooklyn/AssemblyTest.java @@ -18,20 +18,9 @@ */ package org.apache.brooklyn; +import static org.apache.brooklyn.KarafTestUtils.defaultOptionsWith; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.ops4j.pax.exam.CoreOptions.junitBundles; -import static org.ops4j.pax.exam.CoreOptions.maven; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureConsole; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.karafDistributionConfiguration; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.keepRuntimeFolder; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.logLevel; - -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import javax.inject.Inject; @@ -42,9 +31,6 @@ import org.ops4j.pax.exam.Configuration; import org.ops4j.pax.exam.Option; import org.ops4j.pax.exam.junit.PaxExam; -import org.ops4j.pax.exam.karaf.options.LogLevelOption.LogLevel; -import org.ops4j.pax.exam.options.MavenArtifactUrlReference; -import org.ops4j.pax.exam.options.MavenUrlReference; import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; import org.ops4j.pax.exam.spi.reactors.PerClass; import org.ops4j.pax.exam.util.Filter; @@ -52,6 +38,8 @@ /** * Tests the apache-brooklyn karaf runtime assembly. + * + * Keeping it a non-integration test so we have at least a basic OSGi sanity check. (takes 14 sec) */ @RunWith(PaxExam.class) @ExamReactorStrategy(PerClass.class) @@ -73,43 +61,10 @@ public class AssemblyTest { @Configuration public static Option[] configuration() throws Exception { - return new Option[]{ - karafDistributionConfiguration() - .frameworkUrl(brooklynKarafDist()) - .unpackDirectory(new File("target/paxexam/unpack/")) - .useDeployFolder(false), - configureConsole().ignoreLocalConsole(), - logLevel(LogLevel.INFO), - keepRuntimeFolder(), - features(karafStandardFeaturesRepository(), "eventadmin"), - junitBundles() - }; - } - - public static MavenArtifactUrlReference brooklynKarafDist() { - return maven() - .groupId("org.apache.brooklyn") - .artifactId("apache-brooklyn") - .type("zip") - .versionAsInProject(); - } - - public static MavenUrlReference karafStandardFeaturesRepository() { - return maven() - .groupId("org.apache.karaf.features") - .artifactId("standard") - .type("xml") - .classifier("features") - .versionAsInProject(); - } - - public static MavenUrlReference brooklynFeaturesRepository() { - return maven() - .groupId("org.apache.brooklyn") - .artifactId("brooklyn-features") - .type("xml") - .classifier("features") - .versionAsInProject(); + return defaultOptionsWith( + // Uncomment this for remote debugging the tests on port 5005 + // KarafDistributionOption.debugConfiguration() + ); } @Test diff --git a/karaf/itest/src/test/java/org/apache/brooklyn/KarafTestUtils.java b/karaf/itest/src/test/java/org/apache/brooklyn/KarafTestUtils.java new file mode 100644 index 0000000000..013bf582f9 --- /dev/null +++ b/karaf/itest/src/test/java/org/apache/brooklyn/KarafTestUtils.java @@ -0,0 +1,80 @@ +/* + * 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; + +import static org.ops4j.pax.exam.CoreOptions.junitBundles; +import static org.ops4j.pax.exam.CoreOptions.maven; +import static org.ops4j.pax.exam.MavenUtils.asInProject; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureConsole; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.karafDistributionConfiguration; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.logLevel; + +import java.io.File; + +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.karaf.options.LogLevelOption.LogLevel; +import org.ops4j.pax.exam.options.MavenArtifactUrlReference; +import org.ops4j.pax.exam.options.MavenUrlReference; + +import com.google.common.collect.ObjectArrays; + +public class KarafTestUtils { + public static final Option[] DEFAULT_OPTIONS = { + karafDistributionConfiguration() + .frameworkUrl(brooklynKarafDist()) + .unpackDirectory(new File("target/paxexam/unpack/")) + .useDeployFolder(false), + configureConsole().ignoreLocalConsole(), + logLevel(LogLevel.INFO), + features(karafStandardFeaturesRepository(), "eventadmin"), + junitBundles() + }; + + public static MavenUrlReference karafStandardFeaturesRepository() { + return maven() + .groupId("org.apache.karaf.features") + .artifactId("standard") + .type("xml") + .classifier("features") + .version(asInProject()); + } + + + public static MavenArtifactUrlReference brooklynKarafDist() { + return maven() + .groupId("org.apache.brooklyn") + .artifactId("apache-brooklyn") + .type("zip") + .version(asInProject()); + } + + public static Option[] defaultOptionsWith(Option... options) { + return ObjectArrays.concat(DEFAULT_OPTIONS, options, Option.class); + } + + public static MavenUrlReference brooklynFeaturesRepository() { + return maven() + .groupId("org.apache.brooklyn") + .artifactId("brooklyn-features") + .type("xml") + .classifier("features") + .versionAsInProject(); + } +} diff --git a/karaf/itest/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTest.java b/karaf/itest/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTest.java index 6792ea2b39..f182f989e4 100644 --- a/karaf/itest/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTest.java +++ b/karaf/itest/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncherTest.java @@ -18,11 +18,14 @@ */ package org.apache.brooklyn.rest; -import java.io.File; +import static org.apache.brooklyn.KarafTestUtils.defaultOptionsWith; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features; + import java.util.concurrent.Callable; -import org.apache.brooklyn.AssemblyTest; -import org.apache.brooklyn.entity.brooklynnode.BrooklynNode; +import org.apache.brooklyn.KarafTestUtils; +import org.apache.brooklyn.entity.brooklynnode.BrooklynNode; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.http.HttpAsserts; import org.apache.brooklyn.util.http.HttpTool; @@ -31,15 +34,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.ops4j.pax.exam.Configuration; -import static org.ops4j.pax.exam.CoreOptions.junitBundles; import org.ops4j.pax.exam.Option; import org.ops4j.pax.exam.junit.PaxExam; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureConsole; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.karafDistributionConfiguration; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.logLevel; -import org.ops4j.pax.exam.karaf.options.LogLevelOption; import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; import org.ops4j.pax.exam.spi.reactors.PerClass; @@ -53,22 +49,12 @@ public class BrooklynRestApiLauncherTest { @Configuration public static Option[] configuration() throws Exception { - return new Option[]{ - karafDistributionConfiguration() - .frameworkUrl(AssemblyTest.brooklynKarafDist()) - .unpackDirectory(new File("target/paxexam/unpack/")) - .useDeployFolder(false), + return defaultOptionsWith( editConfigurationFilePut("etc/org.ops4j.pax.web.cfg", "org.osgi.service.http.port", HTTP_PORT), - configureConsole().ignoreLocalConsole(), - logLevel(LogLevelOption.LogLevel.INFO), -// features(AssemblyTest.karafStandardFeaturesRepository(), "eventadmin"), - features(AssemblyTest.brooklynFeaturesRepository(), "brooklyn-software-base"), - junitBundles() - - // for debugging -// , keepRuntimeFolder() -// , debugConfiguration() - }; + features(KarafTestUtils.brooklynFeaturesRepository(), "brooklyn-software-base") + // Uncomment this for remote debugging the tests on port 5005 + // ,KarafDistributionOption.debugConfiguration() + ); } @Test diff --git a/karaf/itest/src/test/java/org/apache/brooklyn/security/CustomSecurityProvider.java b/karaf/itest/src/test/java/org/apache/brooklyn/security/CustomSecurityProvider.java new file mode 100644 index 0000000000..ca9ac0ee61 --- /dev/null +++ b/karaf/itest/src/test/java/org/apache/brooklyn/security/CustomSecurityProvider.java @@ -0,0 +1,33 @@ +/* + * 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.security; + +import javax.servlet.http.HttpSession; + +import org.apache.brooklyn.rest.security.provider.AbstractSecurityProvider; +import org.apache.brooklyn.rest.security.provider.SecurityProvider; + +public class CustomSecurityProvider extends AbstractSecurityProvider implements SecurityProvider { + + @Override + public boolean authenticate(HttpSession session, String user, String password) { + return "custom".equals(user); + } + +} diff --git a/karaf/itest/src/test/java/org/apache/brooklyn/security/CustomSecurityProviderTest.java b/karaf/itest/src/test/java/org/apache/brooklyn/security/CustomSecurityProviderTest.java new file mode 100644 index 0000000000..a436ac499d --- /dev/null +++ b/karaf/itest/src/test/java/org/apache/brooklyn/security/CustomSecurityProviderTest.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.security; + +import static org.apache.brooklyn.KarafTestUtils.defaultOptionsWith; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.ops4j.pax.exam.CoreOptions.streamBundle; + +import java.io.IOException; + +import javax.inject.Inject; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.rest.BrooklynWebConfig; +import org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.test.IntegrationTest; +import org.apache.karaf.features.BootFinished; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerClass; +import org.ops4j.pax.tinybundles.core.TinyBundles; +import org.osgi.framework.Constants; + +import com.google.common.collect.ImmutableSet; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +@Category(IntegrationTest.class) +public class CustomSecurityProviderTest { + private static final String WEBCONSOLE_REALM = "webconsole"; + + /** + * To make sure the tests run only when the boot features are fully + * installed + */ + @Inject + BootFinished bootFinished; + + @Inject + ManagementContext managementContext; + + @Configuration + public static Option[] configuration() throws Exception { + return defaultOptionsWith( + streamBundle(TinyBundles.bundle() + .add(CustomSecurityProvider.class) + .add("OSGI-INF/blueprint/security.xml", CustomSecurityProviderTest.class.getResource("/custom-security-bp.xml")) + .set(Constants.BUNDLE_MANIFESTVERSION, "2") // defaults to 1 which doesn't work + .set(Constants.BUNDLE_SYMBOLICNAME, "org.apache.brooklyn.test.security") + .set(Constants.BUNDLE_VERSION, "1.0.0") + .set(Constants.DYNAMICIMPORT_PACKAGE, "*") + .set(Constants.EXPORT_PACKAGE, CustomSecurityProvider.class.getPackage().getName()) + .build()) + // Uncomment this for remote debugging the tests on port 5005 + // ,KarafDistributionOption.debugConfiguration() + ); + } + + @Before + public void setUp() { + // Works only before initializing the security provider (i.e. before first use) + // TODO Dirty hack to inject the needed properties. Improve once managementContext is configurable. + // Alternatively re-register a test managementContext service (how?) + BrooklynProperties brooklynProperties = (BrooklynProperties)managementContext.getConfig(); + brooklynProperties.put(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME.getName(), CustomSecurityProvider.class.getCanonicalName()); + } + + @Test(expected = FailedLoginException.class) + public void checkLoginFails() throws LoginException { + assertRealmRegisteredEventually(WEBCONSOLE_REALM); + doLogin("invalid", "auth"); + } + + @Test + public void checkLoginSucceeds() throws LoginException { + assertRealmRegisteredEventually(WEBCONSOLE_REALM); + String user = "custom"; + LoginContext lc = doLogin(user, "password"); + Subject subject = lc.getSubject(); + assertNotNull(subject); + assertEquals(subject.getPrincipals(), ImmutableSet.of( + new BrooklynLoginModule.UserPrincipal(user), + new BrooklynLoginModule.RolePrincipal("users"))); + } + + private LoginContext doLogin(final String username, final String password) throws LoginException { + assertRealmRegisteredEventually(WEBCONSOLE_REALM); + LoginContext lc = new LoginContext(WEBCONSOLE_REALM, new CallbackHandler() { + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + Callback callback = callbacks[i]; + if (callback instanceof PasswordCallback) { + PasswordCallback passwordCallback = (PasswordCallback)callback; + passwordCallback.setPassword(password.toCharArray()); + } else if (callback instanceof NameCallback) { + NameCallback nameCallback = (NameCallback)callback; + nameCallback.setName(username); + } + } + } + }); + lc.login(); + return lc; + } + + private void assertRealmRegisteredEventually(final String userPassRealm) { + // Need to wait a bit for the realm to get registered, any OSGi way to do this? + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + javax.security.auth.login.Configuration initialConfig = javax.security.auth.login.Configuration.getConfiguration(); + AppConfigurationEntry[] realm = initialConfig.getAppConfigurationEntry(userPassRealm); + assertNotNull(realm); + } + }); + } + +} diff --git a/karaf/itest/src/test/java/org/apache/brooklyn/security/StockSecurityProviderTest.java b/karaf/itest/src/test/java/org/apache/brooklyn/security/StockSecurityProviderTest.java new file mode 100644 index 0000000000..9be9e7d7f8 --- /dev/null +++ b/karaf/itest/src/test/java/org/apache/brooklyn/security/StockSecurityProviderTest.java @@ -0,0 +1,188 @@ +/* + * 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.security; + +import static org.apache.brooklyn.KarafTestUtils.defaultOptionsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.util.concurrent.Callable; + +import javax.inject.Inject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.rest.BrooklynWebConfig; +import org.apache.brooklyn.rest.security.provider.ExplicitUsersSecurityProvider; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.test.IntegrationTest; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.karaf.features.BootFinished; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerClass; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +@Category(IntegrationTest.class) +public class StockSecurityProviderTest { + + private static final String WEBCONSOLE_REALM = "webconsole"; + private static final String USER = "admin"; + private static final String PASSWORD = "password"; + + /** + * To make sure the tests run only when the boot features are fully + * installed + */ + @Inject + BootFinished bootFinished; + + @Inject + ManagementContext managementContext; + + @Configuration + public static Option[] configuration() throws Exception { + return defaultOptionsWith( + // Uncomment this for remote debugging the tests on port 5005 + // KarafDistributionOption.debugConfiguration() + ); + } + + @Before + public void setUp() { + //Works only before initializing the security provider (i.e. before first use) + addUser(USER, PASSWORD); + } + + @Test(expected = FailedLoginException.class) + public void checkLoginFails() throws LoginException { + doLogin("invalid", "auth"); + } + + @Test + public void checkLoginSucceeds() throws LoginException { + LoginContext lc = doLogin(USER, PASSWORD); + assertNotNull(lc.getSubject()); + } + + @Test + public void checkRestSecurityFails() throws IOException { + checkRestSecurity(null, null, HttpStatus.SC_UNAUTHORIZED); + } + + @Test + public void checkRestSecuritySucceeds() throws IOException { + checkRestSecurity(USER, PASSWORD, HttpStatus.SC_OK); + } + + private void checkRestSecurity(String username, String password, final int code) throws IOException { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + if (username != null && password != null) { + credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + } + try(CloseableHttpClient client = + HttpClientBuilder.create().setDefaultCredentialsProvider(credentialsProvider).build()) { + Asserts.succeedsEventually(new Callable() { + @Override + public Void call() throws Exception { + assertResponseEquals(client, code); + return null; + } + }); + } + } + + private void assertResponseEquals(CloseableHttpClient httpclient, int code) throws IOException, ClientProtocolException { + // TODO get this dynamically (from CXF service?) + // TODO port is static, should make it dynamic + HttpGet httpGet = new HttpGet("http://localhost:8081/v1/server/ha/state"); + try (CloseableHttpResponse response = httpclient.execute(httpGet)) { + assertEquals(code, response.getStatusLine().getStatusCode()); + } + } + + + private void addUser(String username, String password) { + // TODO Dirty hack to inject the needed properties. Improve once managementContext is configurable. + // Alternatively re-register a test managementContext service (how?) + BrooklynProperties brooklynProperties = (BrooklynProperties)managementContext.getConfig(); + brooklynProperties.put(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME.getName(), ExplicitUsersSecurityProvider.class.getCanonicalName()); + brooklynProperties.put(BrooklynWebConfig.USERS.getName(), username); + brooklynProperties.put(BrooklynWebConfig.PASSWORD_FOR_USER(username), password); + } + + private LoginContext doLogin(final String username, final String password) throws LoginException { + assertRealmRegisteredEventually(WEBCONSOLE_REALM); + LoginContext lc = new LoginContext(WEBCONSOLE_REALM, new CallbackHandler() { + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + Callback callback = callbacks[i]; + if (callback instanceof PasswordCallback) { + PasswordCallback passwordCallback = (PasswordCallback)callback; + passwordCallback.setPassword(password.toCharArray()); + } else if (callback instanceof NameCallback) { + NameCallback nameCallback = (NameCallback)callback; + nameCallback.setName(username); + } + } + } + }); + lc.login(); + return lc; + } + + private void assertRealmRegisteredEventually(final String userPassRealm) { + // Need to wait a bit for the realm to get registered, any OSGi way to do this? + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + javax.security.auth.login.Configuration initialConfig = javax.security.auth.login.Configuration.getConfiguration(); + AppConfigurationEntry[] realm = initialConfig.getAppConfigurationEntry(userPassRealm); + assertNotNull(realm); + } + }); + } + +} diff --git a/karaf/itest/src/test/java/org/apache/brooklyn/test/IntegrationTest.java b/karaf/itest/src/test/java/org/apache/brooklyn/test/IntegrationTest.java new file mode 100644 index 0000000000..9f5fb4389a --- /dev/null +++ b/karaf/itest/src/test/java/org/apache/brooklyn/test/IntegrationTest.java @@ -0,0 +1,26 @@ +/* + * 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.test; + +/** + * Used by junit for grouping tests + */ +public class IntegrationTest { + +} diff --git a/karaf/itest/src/test/resources/custom-security-bp.xml b/karaf/itest/src/test/resources/custom-security-bp.xml new file mode 100644 index 0000000000..ace4454de6 --- /dev/null +++ b/karaf/itest/src/test/resources/custom-security-bp.xml @@ -0,0 +1,40 @@ + + + + + + + brooklyn.webconsole.security.provider.symbolicName=org.apache.brooklyn.test.security + brooklyn.webconsole.security.provider.version=1.0.0 + brooklyn.webconsole.security.provider.role=users + + + diff --git a/karaf/jetty-config/pom.xml b/karaf/jetty-config/pom.xml new file mode 100644 index 0000000000..1446693237 --- /dev/null +++ b/karaf/jetty-config/pom.xml @@ -0,0 +1,57 @@ + + + + 4.0.0 + + + org.apache.brooklyn + brooklyn-karaf + 0.9.0-SNAPSHOT + ../pom.xml + + + brooklyn-karaf-jetty-config + bundle + + Jetty config fragment + + An OSGi fragment to extend the jetty classpath (Import-Packaaes) so it is + able to load all classes in the included jetty.xml. + + + + + + org.apache.felix + maven-bundle-plugin + + 2.5.4 + true + + + org.ops4j.pax.web.pax-web-jetty + + org.eclipse.jetty.jaas, + org.apache.brooklyn.rest.security.jaas, + * + + + + + + + diff --git a/karaf/jetty-config/src/main/resources/jetty.xml b/karaf/jetty-config/src/main/resources/jetty.xml new file mode 100644 index 0000000000..8957ea21c8 --- /dev/null +++ b/karaf/jetty-config/src/main/resources/jetty.xml @@ -0,0 +1,63 @@ + + + + + + + + + karaf + karaf + + + org.apache.karaf.jaas.boot.principal.RolePrincipal + + + + + + + + + default + karaf + + + org.apache.karaf.jaas.boot.principal.RolePrincipal + + + + + + + + + webconsole + webconsole + + + org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule$RolePrincipal + + + + + + + diff --git a/karaf/pom.xml b/karaf/pom.xml index af154a5bb1..dd7a51e349 100644 --- a/karaf/pom.xml +++ b/karaf/pom.xml @@ -40,9 +40,10 @@ 1.0.0 - 4.6.0 + 4.7.0 2.4.3 1.5.0 + 1.0.0 6.0.0 @@ -54,6 +55,7 @@ init + jetty-config features apache-brooklyn commands @@ -143,6 +145,22 @@ + + org.apache.rat + apache-rat-plugin + + + + sandbox/** + + release/** + README.md + + **/nbactions.xml + **/nb-configuration.xml + + + diff --git a/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynLauncher.java b/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynLauncher.java index 476c897ece..2ef038dd93 100644 --- a/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynLauncher.java +++ b/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynLauncher.java @@ -45,7 +45,7 @@ import org.apache.brooklyn.launcher.common.BrooklynPropertiesFactoryHelper; import org.apache.brooklyn.launcher.config.StopWhichAppsOnShutdown; import org.apache.brooklyn.rest.BrooklynWebConfig; -import org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter; +import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider; import org.apache.brooklyn.rest.security.provider.BrooklynUserWithRandomPasswordSecurityProvider; import org.apache.brooklyn.rest.util.ShutdownHandler; import org.apache.brooklyn.util.exceptions.Exceptions; @@ -274,7 +274,16 @@ protected void startWebApps() { BrooklynProperties brooklynProperties = (BrooklynProperties) managementContext.getConfig(); // No security options in properties and no command line options overriding. - if (Boolean.TRUE.equals(skipSecurityFilter) && bindAddress==null) { + Boolean skipSecurity = skipSecurityFilter; + if (skipSecurity == null) { + String securityProvider = managementContext.getConfig().getConfig(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME); + // The security provider will let anyone in, but still require a password to be entered. + // Skip password request dialog if we know the provider will let users through. + if (AnyoneSecurityProvider.class.getName().equals(securityProvider)) { + skipSecurity = true; + } + } + if (Boolean.TRUE.equals(skipSecurity) && bindAddress==null) { LOG.info("Starting Brooklyn web-console on loopback because security is explicitly disabled and no bind address specified"); bindAddress = Networking.LOOPBACK; } else if (BrooklynWebConfig.hasNoSecurityOptions(managementContext.getConfig())) { @@ -308,9 +317,7 @@ protected void startWebApps() { if (useHttps!=null) webServer.setHttpsEnabled(useHttps); webServer.setShutdownHandler(shutdownHandler); webServer.putAttributes(brooklynProperties); - if (skipSecurityFilter != Boolean.TRUE) { - webServer.setSecurityFilter(BrooklynPropertiesSecurityFilter.class); - } + webServer.skipSecurity(Boolean.TRUE.equals(skipSecurity)); for (WebAppContextProvider webapp : webApps) { webServer.addWar(webapp); } diff --git a/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java b/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java index 8b49ca9867..5e9f9f051f 100644 --- a/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java +++ b/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java @@ -34,6 +34,7 @@ import java.util.Map; import javax.annotation.Nullable; +import javax.security.auth.spi.LoginModule; import org.apache.brooklyn.api.location.PortRange; import org.apache.brooklyn.api.mgmt.ManagementContext; @@ -49,11 +50,15 @@ import org.apache.brooklyn.rest.BrooklynWebConfig; import org.apache.brooklyn.rest.RestApiSetup; import org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter; +import org.apache.brooklyn.rest.filter.EntitlementContextFilter; import org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter; -import org.apache.brooklyn.rest.filter.HaMasterCheckFilter; import org.apache.brooklyn.rest.filter.LoggingFilter; import org.apache.brooklyn.rest.filter.NoCacheFilter; import org.apache.brooklyn.rest.filter.RequestTaggingFilter; +import org.apache.brooklyn.rest.filter.RequestTaggingRsFilter; +import org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule; +import org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule.RolePrincipal; +import org.apache.brooklyn.rest.security.jaas.JaasUtils; import org.apache.brooklyn.rest.util.ManagementContextProvider; import org.apache.brooklyn.rest.util.ShutdownHandler; import org.apache.brooklyn.rest.util.ShutdownHandlerProvider; @@ -75,6 +80,7 @@ import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.web.ContextHandlerCollectionHotSwappable; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.jaas.JAASLoginService; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -191,7 +197,16 @@ public class BrooklynWebServer { private File webappTempDir; + /** + * @deprecated since 0.9.0, use {@link #consoleSecurity} to disable security or + * register an alternative JAAS {@link LoginModule}. + * {@link BrooklynLoginModule} used by default. + */ + @Deprecated private Class securityFilterClazz; + + @SetFromFlag + private boolean skipSecurity = false; private ShutdownHandler shutdownHandler; @@ -212,6 +227,7 @@ public BrooklynWebServer(Map flags, ManagementContext managementContext) { log.warn("Ignoring unknown flags " + leftovers); webappTempDir = BrooklynServerPaths.getBrooklynWebTmpDir(managementContext); + JaasUtils.init(managementContext); } public BrooklynWebServer(ManagementContext managementContext, int port) { @@ -222,10 +238,21 @@ public BrooklynWebServer(ManagementContext managementContext, int port, String w this(MutableMap.of("port", port, "war", warUrl), managementContext); } + /** @deprecated since 0.9.0, use {@link #skipSecurity} or {@link BrooklynLoginModule} */ + @Deprecated public void setSecurityFilter(Class filterClazz) { this.securityFilterClazz = filterClazz; } + public BrooklynWebServer skipSecurity() { + return skipSecurity(true); + } + + public BrooklynWebServer skipSecurity(boolean skipSecurity) { + this.skipSecurity = skipSecurity; + return this; + } + public void setShutdownHandler(@Nullable ShutdownHandler shutdownHandler) { this.shutdownHandler = shutdownHandler; } @@ -359,6 +386,14 @@ public synchronized void start() throws Exception { threadPool.setName("brooklyn-jetty-server-"+actualPort+"-"+threadPool.getName()); server = new Server(threadPool); + + // Can be moved to jetty-web.xml inside wars or a global jetty.xml. + JAASLoginService loginService = new JAASLoginService(); + loginService.setName("webconsole"); + loginService.setLoginModuleName("webconsole"); + loginService.setRoleClassNames(new String[] {RolePrincipal.class.getName()}); + server.addBean(loginService); + final ServerConnector connector; if (getHttpsEnabled()) { @@ -420,14 +455,15 @@ public synchronized void start() throws Exception { private WebAppContext deployRestApi(WebAppContext context) { RestApiSetup.installRest(context, - new ManagementContextProvider(managementContext), + new ManagementContextProvider(), new ShutdownHandlerProvider(shutdownHandler), + new RequestTaggingRsFilter(), new NoCacheFilter(), - new HaHotCheckResourceFilter()); + new HaHotCheckResourceFilter(), + new EntitlementContextFilter()); RestApiSetup.installServletFilters(context, RequestTaggingFilter.class, - LoggingFilter.class, - HaMasterCheckFilter.class); + LoggingFilter.class); if (securityFilterClazz != null) { RestApiSetup.installServletFilters(context, securityFilterClazz); } @@ -600,10 +636,27 @@ public WebAppContext deploy(String pathSpec, String war) { */ public WebAppContext deploy(WebAppContextProvider contextProvider) { WebAppContext context = contextProvider.get(managementContext, attributes, ignoreWebappDeploymentFailures); + initSecurity(context); deploy(context); return context; } + private void initSecurity(WebAppContext context) { + if (skipSecurity) { + // Could add in an override web.xml here + // instead of relying on the war having it (useful for downstream). + // context.addOverrideDescriptor("override-web.xml"); + // But then should do the same in OSGi. For now require the web.xml + // to have security pre-configured and ignore it if noConsoleSecurity used. + // + // Ignore security config in web.xml. + context.setDefaultSecurityHandlerClass(NopSecurityHandler.class); + } else { + // Cover for downstream projects which don't have the changes. + context.addOverrideDescriptor(getClass().getResource("/web-security.xml").toExternalForm()); + } + } + public void deploy(WebAppContext context) { try { handlers.updateHandler(context); diff --git a/launcher/src/main/java/org/apache/brooklyn/launcher/NopSecurityHandler.java b/launcher/src/main/java/org/apache/brooklyn/launcher/NopSecurityHandler.java new file mode 100644 index 0000000000..88b2a27f17 --- /dev/null +++ b/launcher/src/main/java/org/apache/brooklyn/launcher/NopSecurityHandler.java @@ -0,0 +1,34 @@ +/* + * 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.launcher; + +import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.security.ConstraintSecurityHandler; + +/** + * Ignores elements from web.xml, so + * we can skip configuration even if requested by web app. + */ +public class NopSecurityHandler extends ConstraintSecurityHandler { + + @Override + public void addConstraintMapping(ConstraintMapping mapping) { + } + +} diff --git a/launcher/src/main/resources/web-security.xml b/launcher/src/main/resources/web-security.xml new file mode 100644 index 0000000000..231145878c --- /dev/null +++ b/launcher/src/main/resources/web-security.xml @@ -0,0 +1,51 @@ + + + + + + Logout + /v1/logout + + + + + + + All + / + + + webconsole + + + + + BASIC + webconsole + + + + webconsole + + + diff --git a/launcher/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynEntityMirrorIntegrationTest.java b/launcher/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynEntityMirrorIntegrationTest.java index c3c94bc10a..524e75275a 100644 --- a/launcher/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynEntityMirrorIntegrationTest.java +++ b/launcher/src/test/java/org/apache/brooklyn/entity/brooklynnode/BrooklynEntityMirrorIntegrationTest.java @@ -83,13 +83,13 @@ public void tearDown() throws Exception { protected void setUpServer() { setUpServer(new LocalManagementContextForTests(), false); } - protected void setUpServer(ManagementContext mgmt, boolean useSecurityFilter) { + protected void setUpServer(ManagementContext mgmt, boolean skipSecurity) { try { if (serverMgmt!=null) throw new IllegalStateException("server already set up"); serverMgmt = mgmt; server = new BrooklynWebServer(mgmt); - if (useSecurityFilter) server.setSecurityFilter(BrooklynPropertiesSecurityFilter.class); + server.skipSecurity(skipSecurity); server.start(); serverMgmt.getHighAvailabilityManager().disabled(); diff --git a/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherTest.java b/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherTest.java index 101a21ae58..b70c1febcb 100644 --- a/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherTest.java +++ b/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynLauncherTest.java @@ -32,7 +32,6 @@ import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.core.catalog.internal.CatalogInitialization; -import org.apache.brooklyn.core.entity.factory.ApplicationBuilder; import org.apache.brooklyn.core.internal.BrooklynProperties; import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; @@ -43,7 +42,7 @@ import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.launcher.common.BrooklynPropertiesFactoryHelperTest; import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation; -import org.apache.brooklyn.test.HttpTestUtils; +import org.apache.brooklyn.util.http.HttpAsserts; import org.apache.brooklyn.util.io.FileUtil; import org.apache.brooklyn.util.net.Urls; import org.apache.brooklyn.util.os.Os; @@ -75,6 +74,7 @@ public void tearDown() throws Exception { public void testStartsWebServerOnExpectectedPort() throws Exception { launcher = newLauncherForTests(true) .webconsolePort("10000+") + .installSecurityFilter(false) .start(); String webServerUrlStr = launcher.getServerDetails().getWebServerUrl(); @@ -82,7 +82,7 @@ public void testStartsWebServerOnExpectectedPort() throws Exception { assertEquals(launcher.getApplications(), ImmutableList.of()); assertTrue(webServerUri.getPort() >= 10000 && webServerUri.getPort() < 10100, "port="+webServerUri.getPort()+"; uri="+webServerUri); - HttpTestUtils.assertUrlReachable(webServerUrlStr); + HttpAsserts.assertUrlReachable(webServerUrlStr); } // Integration because takes a few seconds to start web-console @@ -117,7 +117,7 @@ public void testCanDisableWebServerStartup() throws Exception { public void testStartsAppInstance() throws Exception { launcher = newLauncherForTests(true) .webconsole(false) - .application(new TestApplicationImpl()) + .application(EntitySpec.create(TestApplicationImpl.class)) .start(); assertOnlyApp(launcher, TestApplication.class); @@ -134,10 +134,11 @@ public void testStartsAppFromSpec() throws Exception { } @Test + @SuppressWarnings("deprecation") public void testStartsAppFromBuilder() throws Exception { launcher = newLauncherForTests(true) .webconsole(false) - .application(new ApplicationBuilder(EntitySpec.create(TestApplication.class)) { + .application(new org.apache.brooklyn.core.entity.factory.ApplicationBuilder(EntitySpec.create(TestApplication.class)) { @Override protected void doBuild() { }}) .start(); @@ -167,9 +168,7 @@ public void testStartsAppInSuppliedLocations() throws Exception { launcher = newLauncherForTests(true) .webconsole(false) .location("localhost") - .application(new ApplicationBuilder(EntitySpec.create(TestApplication.class)) { - @Override protected void doBuild() { - }}) + .application(EntitySpec.create(TestApplication.class)) .start(); Application app = Iterables.find(launcher.getApplications(), Predicates.instanceOf(TestApplication.class)); @@ -254,15 +253,16 @@ public Void apply(CatalogInitialization input) { throw new RuntimeException("deliberate-exception-for-testing"); } })) + .installSecurityFilter(false) .start(); // such an error should be thrown, then caught in this calling thread ManagementContext mgmt = launcher.getServerDetails().getManagementContext(); Assert.assertFalse( ((ManagementContextInternal)mgmt).errors().isEmpty() ); Assert.assertTrue( ((ManagementContextInternal)mgmt).errors().get(0).toString().contains("deliberate"), ""+((ManagementContextInternal)mgmt).errors() ); - HttpTestUtils.assertContentMatches( + HttpAsserts.assertContentMatches( Urls.mergePaths(launcher.getServerDetails().getWebServerUrl(), "v1/server/up"), "true"); - HttpTestUtils.assertContentMatches( + HttpAsserts.assertContentMatches( Urls.mergePaths(launcher.getServerDetails().getWebServerUrl(), "v1/server/healthy"), "false"); // TODO test errors api? diff --git a/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynWebServerTest.java b/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynWebServerTest.java index eafaa493e2..2b8406eca2 100644 --- a/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynWebServerTest.java +++ b/launcher/src/test/java/org/apache/brooklyn/launcher/BrooklynWebServerTest.java @@ -88,6 +88,7 @@ private LocalManagementContext newManagementContext(BrooklynProperties brooklynP @Test public void verifyHttp() throws Exception { webServer = new BrooklynWebServer(newManagementContext(brooklynProperties)); + webServer.skipSecurity(); try { webServer.start(); @@ -114,7 +115,7 @@ public void verifyHttps(String keystoreUrl) throws Exception { .put("keystorePassword", "password") .build(); webServer = new BrooklynWebServer(flags, newManagementContext(brooklynProperties)); - webServer.start(); + webServer.skipSecurity().start(); try { KeyStore keyStore = load("client.ks", "password"); @@ -172,6 +173,7 @@ public void verifyHttpsCiphers() throws Exception { private void verifyHttpsFromConfig(BrooklynProperties brooklynProperties) throws Exception { webServer = new BrooklynWebServer(MutableMap.of(), newManagementContext(brooklynProperties)); + webServer.skipSecurity(); webServer.start(); try { diff --git a/launcher/src/test/java/org/apache/brooklyn/launcher/WebAppRunnerTest.java b/launcher/src/test/java/org/apache/brooklyn/launcher/WebAppRunnerTest.java index 36dbfaca93..a53ea9584a 100644 --- a/launcher/src/test/java/org/apache/brooklyn/launcher/WebAppRunnerTest.java +++ b/launcher/src/test/java/org/apache/brooklyn/launcher/WebAppRunnerTest.java @@ -18,20 +18,16 @@ */ package org.apache.brooklyn.launcher; -import org.apache.brooklyn.core.entity.Entities; -import org.apache.brooklyn.core.internal.BrooklynProperties; -import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; -import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; -import org.apache.brooklyn.launcher.BrooklynWebServer; -import org.apache.brooklyn.launcher.BrooklynLauncher; -import org.apache.brooklyn.launcher.BrooklynServerDetails; - import static org.testng.Assert.assertNotNull; import static org.testng.Assert.fail; import java.util.List; import java.util.Map; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.test.HttpTestUtils; import org.apache.brooklyn.test.support.TestResourceUnavailableException; import org.apache.brooklyn.util.collections.MutableMap; @@ -75,9 +71,9 @@ BrooklynWebServer createWebServer(Map properties) { BrooklynProperties brooklynProperties = BrooklynProperties.Factory.newEmpty(); brooklynProperties.putAll(bigProps); - brooklynProperties.put("brooklyn.webconsole.security.provider","org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider"); brooklynProperties.put("brooklyn.webconsole.security.https.required","false"); - return new BrooklynWebServer(bigProps, newManagementContext(brooklynProperties)); + return new BrooklynWebServer(bigProps, newManagementContext(brooklynProperties)) + .skipSecurity(); } @Test diff --git a/parent/pom.xml b/parent/pom.xml index 5e721a34b8..6034751f6d 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -155,6 +155,11 @@ jetty-security ${jetty.version} + + org.eclipse.jetty + jetty-jaas + ${jetty.version} + org.eclipse.jetty jetty-webapp diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/LogoutApi.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/LogoutApi.java new file mode 100644 index 0000000000..ac1a345ea5 --- /dev/null +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/LogoutApi.java @@ -0,0 +1,49 @@ +/* + * 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.api; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +@Path("/logout") +@Api("Logout") +public interface LogoutApi { + + @POST + @ApiOperation(value = "Request a logout and clean session") + @ApiResponses(value = { + @ApiResponse(code = 307, message = "Redirect to /logout/user, keeping the request method") + }) + Response logout(); + + @POST + @Path("/{user}") + @ApiOperation(value = "Logout and clean session if matching user logged") + Response logoutUser( + @ApiParam(value = "User to log out", required = true) + @PathParam("user") final String user); +} diff --git a/rest/rest-resources/pom.xml b/rest/rest-resources/pom.xml index 83533e646f..e28776ca04 100644 --- a/rest/rest-resources/pom.xml +++ b/rest/rest-resources/pom.xml @@ -108,6 +108,15 @@ com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider + + org.eclipse.jetty + jetty-jaas + + + org.eclipse.jetty + jetty-server + + org.apache.brooklyn brooklyn-test-support @@ -205,5 +214,19 @@ ${basedir}/src/main/webapp + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.karaf.jaas.config, + * + + + + + diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java index f42548f594..df2be650e6 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynRestApi.java @@ -31,6 +31,7 @@ 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.LogoutResource; import org.apache.brooklyn.rest.resources.PolicyConfigResource; import org.apache.brooklyn.rest.resources.PolicyResource; import org.apache.brooklyn.rest.resources.ScriptResource; @@ -67,6 +68,7 @@ public static Iterable getBrooklynRestResources() resources.add(new ServerResource()); resources.add(new UsageResource()); resources.add(new VersionResource()); + resources.add(new LogoutResource()); return resources; } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java new file mode 100644 index 0000000000..a039b57f0a --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java @@ -0,0 +1,63 @@ +/* + * 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.security.Principal; + +import javax.annotation.Priority; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.ext.Provider; + +import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; +import org.apache.brooklyn.core.mgmt.entitlement.WebEntitlementContext; + +@Provider +@Priority(400) +public class EntitlementContextFilter implements ContainerRequestFilter, ContainerResponseFilter { + @Context + private HttpServletRequest request; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + SecurityContext securityContext = requestContext.getSecurityContext(); + Principal user = securityContext.getUserPrincipal(); + + if (user != null) { + String uri = request.getRequestURI(); + String remoteAddr = request.getRemoteAddr(); + + String uid = RequestTaggingRsFilter.getTag(); + WebEntitlementContext entitlementContext = new WebEntitlementContext(user.getName(), remoteAddr, uri, uid); + Entitlements.setEntitlementContext(entitlementContext); + } + } + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { + Entitlements.clearEntitlementContext(); + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java index 3c9c129b6e..6278b7c0e7 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java @@ -21,7 +21,9 @@ import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.Set; +import javax.annotation.Priority; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ResourceInfo; @@ -30,11 +32,13 @@ import javax.ws.rs.ext.Provider; import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState; import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.text.Strings; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; /** * Checks that if the method or resource class corresponding to a request @@ -46,7 +50,9 @@ * as this needs to know the method being invoked. */ @Provider +@Priority(300) public class HaHotCheckResourceFilter implements ContainerRequestFilter { + private static final Set SAFE_STANDBY_METHODS = ImmutableSet.of("GET", "HEAD"); public static final String SKIP_CHECK_HEADER = HaHotCheckHelperAbstract.SKIP_CHECK_HEADER; // Not quite standards compliant. Should instead be: @@ -78,7 +84,7 @@ public HaHotCheckResourceFilter(ContextResolver mgmt) { public void filter(ContainerRequestContext requestContext) throws IOException { String problem = lookForProblem(requestContext); if (Strings.isNonBlank(problem)) { - requestContext.abortWith(helper.disallowResponse(problem, requestContext.getUriInfo()+"/"+resourceInfo.getResourceMethod())); + requestContext.abortWith(helper.disallowResponse(problem, requestContext.getUriInfo().getAbsolutePath()+"/"+resourceInfo.getResourceMethod())); } } @@ -86,6 +92,10 @@ private String lookForProblem(ContainerRequestContext requestContext) { if (helper.isSkipCheckHeaderSet(requestContext.getHeaderString(SKIP_CHECK_HEADER))) return null; + if (isMasterRequiredForRequest(requestContext) && !isMaster()) { + return "server not in required HA master state"; + } + if (!isHaHotStateRequired()) return null; @@ -106,6 +116,26 @@ public static String lookForProblemIfServerNotRunning(ManagementContext mgmt) { return HaHotCheckHelperAbstract.getProblemMessageIfServerNotRunning(mgmt).orNull(); } + private boolean isMaster() { + return ManagementNodeState.MASTER.equals( + mgmt.getContext(ManagementContext.class) + .getHighAvailabilityManager() + .getNodeState()); + } + + private boolean isMasterRequiredForRequest(ContainerRequestContext requestContext) { + // gets usually okay + if (SAFE_STANDBY_METHODS.contains(requestContext.getMethod())) return false; + + String uri = requestContext.getUriInfo().getPath(); + // 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 use an annotation HaAnyStateAllowed or HaHotCheckRequired(false) or similar + if ("server/shutdown".equals(uri)) return false; + + return true; + } + protected boolean isHaHotStateRequired() { // TODO support super annotations Method m = resourceInfo.getResourceMethod(); diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java index 97fdda113d..ed33594ef5 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java @@ -18,6 +18,7 @@ */ package org.apache.brooklyn.rest.filter; +import javax.annotation.Priority; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; @@ -26,6 +27,7 @@ import javax.ws.rs.ext.Provider; @Provider +@Priority(200) public class NoCacheFilter implements ContainerResponseFilter { @Override diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilter.java new file mode 100644 index 0000000000..95e8533779 --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilter.java @@ -0,0 +1,77 @@ +/* + * 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 static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; + +import javax.annotation.Priority; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Context; +import javax.ws.rs.ext.Provider; + +import org.apache.brooklyn.util.text.Identifiers; + +/** + * Tags each request with a probabilistically unique id. Should be included before other + * filters to make sense. + */ +@Provider +@Priority(100) +public class RequestTaggingRsFilter implements ContainerRequestFilter, ContainerResponseFilter { + public static final String ATT_REQUEST_ID = RequestTaggingRsFilter.class.getName() + ".id"; + + @Context + private HttpServletRequest req; + + private static ThreadLocal tag = new ThreadLocal(); + + protected static String getTag() { + // Alternatively could use + // PhaseInterceptorChain.getCurrentMessage().getId() + + return checkNotNull(tag.get()); + } + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + String requestId = getRequestId(); + tag.set(requestId); + } + + private String getRequestId() { + Object id = req.getAttribute(ATT_REQUEST_ID); + if (id != null) { + return id.toString(); + } else { + return Identifiers.makeRandomId(6); + } + } + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { + tag.remove(); + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/LogoutResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/LogoutResource.java new file mode 100644 index 0000000000..66ce968fbd --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/LogoutResource.java @@ -0,0 +1,78 @@ +/* + * 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.net.URI; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; +import org.apache.brooklyn.core.mgmt.entitlement.WebEntitlementContext; +import org.apache.brooklyn.rest.api.LogoutApi; +import org.apache.brooklyn.util.exceptions.Exceptions; + +public class LogoutResource extends AbstractBrooklynRestResource implements LogoutApi { + @Context HttpServletRequest req; + @Context UriInfo uri; + + @Override + public Response logout() { + WebEntitlementContext ctx = (WebEntitlementContext) Entitlements.getEntitlementContext(); + URI dest = uri.getBaseUriBuilder().path(LogoutApi.class).path(LogoutApi.class, "logoutUser").build(ctx.user()); + + // When execution gets here we don't know whether this is the first fetch of logout() or a subsequent one + // with a re-authenticated user. The only way to tell is compare if user names changed. So redirect to an URL + // which contains the user name. + return Response.status(Status.TEMPORARY_REDIRECT) + .header("Location", dest.toASCIIString()) + .build(); + } + + @Override + public Response logoutUser(String user) { + // Will work when switching users, but will keep re-authenticating if user types in same user name. + // Could improve by keeping state in cookies to decide whether to request auth or declare successfull re-auth. + WebEntitlementContext ctx = (WebEntitlementContext) Entitlements.getEntitlementContext(); + if (user.equals(ctx.user())) { + doLogout(); + + return Response.status(Status.UNAUTHORIZED) + .header("WWW-Authenticate", "Basic realm=\"webconsole\"") + .build(); + } else { + return Response.temporaryRedirect(uri.getAbsolutePathBuilder().replacePath("/").build()).build(); + } + } + + private void doLogout() { + try { + req.logout(); + } catch (ServletException e) { + Exceptions.propagate(e); + } + + req.getSession().invalidate(); + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModule.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModule.java new file mode 100644 index 0000000000..75ddd3264a --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModule.java @@ -0,0 +1,367 @@ +/* + * 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.security.jaas; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; +import javax.servlet.http.HttpSession; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.config.StringConfigMap; +import org.apache.brooklyn.rest.BrooklynWebConfig; +import org.apache.brooklyn.rest.security.provider.DelegatingSecurityProvider; +import org.apache.brooklyn.rest.security.provider.SecurityProvider; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.text.Strings; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.Request; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// http://docs.oracle.com/javase/7/docs/technotes/guides/security/jaas/JAASLMDevGuide.html + +/** + *

+ * JAAS module delegating authentication to the {@link SecurityProvider} implementation + * configured in {@literal brooklyn.properties}, key {@literal brooklyn.webconsole.security.provider}. + * + *

+ * If used in an OSGi environment only implementations visible from {@literal brooklyn-rest-server} are usable by default. + * To use a custom security provider add the following configuration to the its bundle in {@literal src/main/resources/OSGI-INF/bundle/security-provider.xml}: + * + *

+ * {@code
+ *
+ *
+ *
+ *    
+ *        
+ *            brooklyn.webconsole.security.provider.symbolicName=BUNDLE_SYMBOLIC_NAME
+ *            brooklyn.webconsole.security.provider.version=BUNDLE_VERSION
+ *        
+ *    
+ *
+ *
+ *}
+ * 
+ */ +// Needs an explicit "org.apache.karaf.jaas.config" Import-Package in the manifest! +public class BrooklynLoginModule implements LoginModule { + private static final Logger log = LoggerFactory.getLogger(BrooklynLoginModule.class); + + /** + * 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"; + + private static class BasicPrincipal implements Principal { + private String name; + public BasicPrincipal(String name) { + this.name = checkNotNull(name, "name"); + } + @Override + public String getName() { + return name; + } + @Override + public int hashCode() { + return name.hashCode(); + } + @Override + public boolean equals(Object obj) { + if (obj instanceof BasicPrincipal) { + return name.equals(((BasicPrincipal)obj).name); + } + return false; + } + @Override + public String toString() { + return getClass().getSimpleName() + "[" +name + "]"; + } + } + public static class UserPrincipal extends BasicPrincipal { + public UserPrincipal(String name) { + super(name); + } + } + public static class RolePrincipal extends BasicPrincipal { + public RolePrincipal(String name) { + super(name); + } + } + + public static final String PROPERTY_BUNDLE_SYMBOLIC_NAME = BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME.getName() + ".symbolicName"; + public static final String PROPERTY_BUNDLE_VERSION = BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME.getName() + ".version"; + /** SecurityProvider doesn't know about roles, just attach one by default. Use the one specified here or DEFAULT_ROLE */ + public static final String PROPERTY_ROLE = BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME.getName() + ".role"; + public static final String DEFAULT_ROLE = "webconsole"; + + private Map options; + private BundleContext bundleContext; + + private static DelegatingSecurityProvider defaultProvider; + private HttpSession providerSession; + + private SecurityProvider provider; + private Subject subject; + private CallbackHandler callbackHandler; + private boolean loginSuccess; + private boolean commitSuccess; + private Collection principals; + + public BrooklynLoginModule() { + } + + private SecurityProvider getDefaultProvider() { + if (defaultProvider == null) { + createDefaultSecurityProvider(getManagementContext()); + } + return defaultProvider; + } + + private synchronized static SecurityProvider createDefaultSecurityProvider(ManagementContext mgmt) { + if (defaultProvider == null) { + defaultProvider = new DelegatingSecurityProvider(mgmt); + } + return defaultProvider; + } + + private ManagementContext getManagementContext() { + return ManagementContextHolder.getManagementContext(); + } + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + this.options = options; + + this.bundleContext = (BundleContext) options.get(BundleContext.class.getName()); + + loginSuccess = false; + commitSuccess = false; + + initProvider(); + } + + private void initProvider() { + StringConfigMap brooklynProperties = getManagementContext().getConfig(); + provider = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE); + String symbolicName = (String) options.get(PROPERTY_BUNDLE_SYMBOLIC_NAME); + String version = (String) options.get(PROPERTY_BUNDLE_VERSION); + String className = (String) options.get(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME.getName()); + if (className != null && symbolicName == null) { + throw new IllegalStateException("Missing JAAS module property " + PROPERTY_BUNDLE_SYMBOLIC_NAME + " pointing at the bundle where to load the security provider from."); + } + if (provider != null) return; + if (symbolicName != null) { + if (className == null) { + className = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME); + } + if (className != null) { + try { + Collection bundles = getMatchingBundles(symbolicName, version); + if (bundles.isEmpty()) { + throw new IllegalStateException("No bundle " + symbolicName + ":" + version + " found"); + } else if (bundles.size() > 1) { + log.warn("Found multiple bundles matching symbolicName " + symbolicName + " and version " + version + + " while trying to load security provider " + className + ". Will use first one that loads the class successfully."); + } + provider = tryLoadClass(className, bundles); + if (provider == null) { + throw new ClassNotFoundException("Unable to load class " + className + " from bundle " + symbolicName + ":" + version); + } + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + throw new IllegalStateException("Can not load or create security provider " + className + " for bundle " + symbolicName + ":" + version, e); + } + } + } else { + log.debug("Delegating security provider loading to Brooklyn."); + provider = getDefaultProvider(); + } + + log.debug("Using security provider " + provider); + } + + private SecurityProvider tryLoadClass(String className, Collection bundles) + throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + for (Bundle b : bundles) { + try { + @SuppressWarnings("unchecked") + Class securityProviderType = (Class) b.loadClass(className); + return DelegatingSecurityProvider.createSecurityProviderInstance(getManagementContext(), securityProviderType); + } catch (ClassNotFoundException e) { + } + } + return null; + } + + private Collection getMatchingBundles(final String symbolicName, final String version) { + Collection bundles = new ArrayList<>(); + for (Bundle b : bundleContext.getBundles()) { + if (b.getSymbolicName().equals(symbolicName) && + (version == null || b.getVersion().toString().equals(version))) { + bundles.add(b); + } + } + return bundles; + } + + @Override + public boolean login() throws LoginException { + if (callbackHandler == null) { + loginSuccess = false; + throw new FailedLoginException("Username and password not available"); + } + + NameCallback cbName = new NameCallback("Username: "); + PasswordCallback cbPassword = new PasswordCallback("Password: ", false); + + Callback[] callbacks = {cbName, cbPassword}; + + try { + callbackHandler.handle(callbacks); + } catch (IOException ioe) { + throw new LoginException(ioe.getMessage()); + } catch (UnsupportedCallbackException uce) { + throw new LoginException(uce.getMessage() + " not available to obtain information from user"); + } + String user = cbName.getName(); + String password = new String(cbPassword.getPassword()); + + providerSession = new SecurityProviderHttpSession(); + + Request req = getJettyRequest(); + if (req != null) { + String remoteAddr = req.getRemoteAddr(); + providerSession.setAttribute(BrooklynWebConfig.REMOTE_ADDRESS_SESSION_ATTRIBUTE, remoteAddr); + } + + if (!provider.authenticate(providerSession, user, password)) { + loginSuccess = false; + throw new FailedLoginException("Incorrect username or password"); + } + + if (user != null) { + providerSession.setAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE, user); + } + + principals = new ArrayList<>(2); + principals.add(new UserPrincipal(user)); + // Could introduce a new interface SecurityRoleAware, implemented by + // the SecurityProviders, returning the roles a user has assigned. + // For now a static role is good enough. + String role = (String) options.get(PROPERTY_ROLE); + if (role == null) { + role = DEFAULT_ROLE; + } + if (Strings.isNonEmpty(role)) { + principals.add(new RolePrincipal(role)); + } + loginSuccess = true; + return true; + } + + @Override + public boolean commit() throws LoginException { + if (loginSuccess) { + if (subject.isReadOnly()) { + throw new LoginException("Can't commit read-only subject"); + } + subject.getPrincipals().addAll(principals); + } + + commitSuccess = true; + return loginSuccess; + } + + @Override + public boolean abort() throws LoginException { + if (loginSuccess && commitSuccess) { + removePrincipal(); + } + clear(); + return loginSuccess; + } + + @Override + public boolean logout() throws LoginException { + Request req = getJettyRequest(); + if (req != null) { + log.info("REST logging {} out", + providerSession.getAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE)); + provider.logout(req.getSession()); + req.getSession().removeAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE); + } else { + log.error("Request object not available for logout"); + } + + removePrincipal(); + clear(); + return true; + } + + private void removePrincipal() throws LoginException { + if (subject.isReadOnly()) { + throw new LoginException("Read-only subject"); + } + subject.getPrincipals().removeAll(principals); + } + + private void clear() { + subject = null; + callbackHandler = null; + principals = null; + } + + private Request getJettyRequest() { + HttpChannel channel = HttpChannel.getCurrentHttpChannel(); + if (channel != null) { + return channel.getRequest(); + } else { + return null; + } + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/JaasUtils.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/JaasUtils.java new file mode 100644 index 0000000000..94aba5de96 --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/JaasUtils.java @@ -0,0 +1,48 @@ +/* + * 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.security.jaas; + +import java.net.URL; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JaasUtils { + private static final Logger log = LoggerFactory.getLogger(JaasUtils.class); + + private static final String JAAS_CONFIG = "java.security.auth.login.config"; + + public static void init(ManagementContext mgmt) { + ManagementContextHolder.setManagementContextStatic(mgmt); + String config = System.getProperty(JAAS_CONFIG); + if (config == null) { + URL configUrl = JaasUtils.class.getResource("/jaas.conf"); + if (configUrl != null) { + log.debug("Using classpath JAAS config from " + configUrl.toExternalForm()); + System.setProperty(JAAS_CONFIG, configUrl.toExternalForm()); + } else { + log.error("Can't find " + JAAS_CONFIG + " on classpath. Web server authentication will fail."); + } + } else { + log.debug("Using externally configured JAAS at " + config); + } + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/ManagementContextHolder.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/ManagementContextHolder.java new file mode 100644 index 0000000000..84704f1547 --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/ManagementContextHolder.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.security.jaas; + +import static com.google.common.base.Preconditions.checkNotNull; + +import org.apache.brooklyn.api.mgmt.ManagementContext; + +public class ManagementContextHolder { + private static ManagementContext mgmt; + public static ManagementContext getManagementContext() { + return checkNotNull(mgmt, "Management context not set yet"); + } + public void setManagementContext(ManagementContext mgmt) { + setManagementContextStatic(mgmt); + } + public static void setManagementContextStatic(ManagementContext mgmt) { + ManagementContextHolder.mgmt = checkNotNull(mgmt, "mgmt"); + } +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/SecurityProviderHttpSession.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/SecurityProviderHttpSession.java new file mode 100644 index 0000000000..52243192ec --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/SecurityProviderHttpSession.java @@ -0,0 +1,119 @@ +/* + * 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.security.jaas; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionContext; + +import org.apache.brooklyn.util.text.Identifiers; + +public class SecurityProviderHttpSession implements HttpSession { + String id = Identifiers.makeRandomId(5); + Map attributes = new ConcurrentHashMap<>(); + + @Override + public long getCreationTime() { + return 0; + } + + @Override + public String getId() { + return id; + } + + @Override + public long getLastAccessedTime() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public void setMaxInactiveInterval(int interval) { + } + + @Override + public int getMaxInactiveInterval() { + return 0; + } + + @Override + public HttpSessionContext getSessionContext() { + return null; + } + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Object getValue(String name) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + @Override + public String[] getValueNames() { + return null; + } + + @Override + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + @Override + public void putValue(String name, Object value) { + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + + @Override + public void removeValue(String name) { + } + + @Override + public void invalidate() { + id = Identifiers.makeRandomId(5); + attributes.clear(); + } + + @Override + public boolean isNew() { + return false; + } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java index 8b2b9da59d..ca7fdf128c 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java @@ -19,6 +19,7 @@ package org.apache.brooklyn.rest.security.provider; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.http.HttpSession; @@ -97,19 +98,7 @@ private synchronized SecurityProvider loadDelegate() { } else throw e; } - Constructor constructor; - try { - constructor = clazz.getConstructor(ManagementContext.class); - delegate = constructor.newInstance(mgmt); - } catch (Exception e) { - constructor = clazz.getConstructor(); - Object delegateO = constructor.newInstance(); - if (!(delegateO instanceof SecurityProvider)) { - // if classloaders get mangled it will be a different CL's SecurityProvider - throw new ClassCastException("Delegate is either not a security provider or has an incompatible classloader: "+delegateO); - } - delegate = (SecurityProvider) delegateO; - } + delegate = createSecurityProviderInstance(mgmt, clazz); } catch (Exception e) { log.warn("REST unable to instantiate security provider " + className + "; all logins are being disallowed", e); delegate = new BlackholeSecurityProvider(); @@ -120,6 +109,24 @@ private synchronized SecurityProvider loadDelegate() { return delegate; } + public static SecurityProvider createSecurityProviderInstance(ManagementContext mgmt, + Class clazz) throws NoSuchMethodException, InstantiationException, + IllegalAccessException, InvocationTargetException { + Constructor constructor; + try { + constructor = clazz.getConstructor(ManagementContext.class); + return constructor.newInstance(mgmt); + } catch (Exception e) { + constructor = clazz.getConstructor(); + Object delegateO = constructor.newInstance(); + if (!(delegateO instanceof SecurityProvider)) { + // if classloaders get mangled it will be a different CL's SecurityProvider + throw new ClassCastException("Delegate is either not a security provider or has an incompatible classloader: "+delegateO); + } + return (SecurityProvider) delegateO; + } + } + /** * Causes all existing sessions to be invalidated. */ diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java index cbe5d91d52..46a22385c0 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java @@ -18,17 +18,28 @@ */ package org.apache.brooklyn.rest.util; +import javax.servlet.ServletContext; +import javax.ws.rs.core.Context; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.server.BrooklynServiceAttributes; + +import com.google.common.annotations.VisibleForTesting; @Provider // Needed by tests in rest-resources module and by main code in rest-server public class ManagementContextProvider implements ContextResolver { + @Context + private ServletContext context; private ManagementContext mgmt; + public ManagementContextProvider() { + } + + @VisibleForTesting public ManagementContextProvider(ManagementContext mgmt) { this.mgmt = mgmt; } @@ -36,7 +47,11 @@ public ManagementContextProvider(ManagementContext mgmt) { @Override public ManagementContext getContext(Class type) { if (type == ManagementContext.class) { - return mgmt; + if (mgmt != null) { + return mgmt; + } else { + return (ManagementContext) context.getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT); + } } else { return null; } diff --git a/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml b/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml index 0c19c136c1..42ab968d00 100644 --- a/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml +++ b/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml @@ -20,11 +20,13 @@ limitations under the License. xmlns:jaxws="http://cxf.apache.org/blueprint/jaxws" xmlns:jaxrs="http://cxf.apache.org/blueprint/jaxrs" xmlns:cxf="http://cxf.apache.org/blueprint/core" + xmlns:jaas="http://karaf.apache.org/xmlns/jaas/v1.0.0" xsi:schemaLocation=" http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd http://cxf.apache.org/blueprint/jaxws http://cxf.apache.org/schemas/blueprint/jaxws.xsd http://cxf.apache.org/blueprint/jaxrs http://cxf.apache.org/schemas/blueprint/jaxrs.xsd http://cxf.apache.org/blueprint/core http://cxf.apache.org/schemas/blueprint/core.xsd + http://karaf.apache.org/xmlns/jaas/v1.0.0 http://karaf.apache.org/xmlns/jaas/v1.0.0 "> @@ -38,6 +40,15 @@ limitations under the License. + + + + + + + + @@ -99,6 +110,9 @@ limitations under the License. + + + @@ -118,12 +132,28 @@ limitations under the License. + + + + + + + + + + + + + diff --git a/rest/rest-resources/src/main/resources/jaas.conf b/rest/rest-resources/src/main/resources/jaas.conf new file mode 100644 index 0000000000..bb18334158 --- /dev/null +++ b/rest/rest-resources/src/main/resources/jaas.conf @@ -0,0 +1,21 @@ +/* + * 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. + */ +webconsole { + org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule required; +}; diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/EntitlementContextFilterTest.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/EntitlementContextFilterTest.java new file mode 100644 index 0000000000..cdd2867305 --- /dev/null +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/EntitlementContextFilterTest.java @@ -0,0 +1,94 @@ +/* + * 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 static org.testng.Assert.assertEquals; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; +import org.apache.brooklyn.core.mgmt.entitlement.WebEntitlementContext; +import org.apache.brooklyn.rest.BrooklynWebConfig; +import org.apache.brooklyn.rest.security.jaas.JaasUtils; +import org.apache.brooklyn.rest.security.provider.ExplicitUsersSecurityProvider; +import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; +import org.apache.cxf.interceptor.security.JAASLoginInterceptor; +import org.apache.cxf.jaxrs.JAXRSServerFactoryBean; +import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.http.HttpStatus; +import org.testng.annotations.Test; + +public class EntitlementContextFilterTest extends BrooklynRestResourceTest { + + private static final String USER_PASS = "admin"; + + public static class EntitlementResource { + @GET + @Path("/test") + public String test() { + WebEntitlementContext context = (WebEntitlementContext)Entitlements.getEntitlementContext(); + return context.user(); + } + } + + @Override + protected void configureCXF(JAXRSServerFactoryBean sf) { + BrooklynProperties props = (BrooklynProperties)getManagementContext().getConfig(); + props.put(BrooklynWebConfig.USERS, USER_PASS); + props.put(BrooklynWebConfig.PASSWORD_FOR_USER(USER_PASS), USER_PASS); + props.put(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE, new ExplicitUsersSecurityProvider(getManagementContext())); + + super.configureCXF(sf); + + JaasUtils.init(getManagementContext()); + + JAASLoginInterceptor jaas = new JAASLoginInterceptor(); + jaas.setContextName("webconsole"); + sf.getInInterceptors().add(jaas); + + } + + @Override + protected void addBrooklynResources() { + addResource(new RequestTaggingRsFilter()); + addResource(new EntitlementContextFilter()); + addResource(new EntitlementResource()); + } + + @Test + public void testEntitlementContextSet() { + Response response = fetch("/test"); + assertEquals(response.getStatus(), HttpStatus.SC_OK); + String tag = (String) response.readEntity(String.class); + assertEquals(tag, USER_PASS); + } + + protected Response fetch(String path) { + WebClient resource = WebClient.create(getEndpointAddress(), clientProviders, USER_PASS, USER_PASS, null) + .path(path) + .accept(MediaType.APPLICATION_JSON_TYPE); + Response response = resource.get(); + return response; + } + +} diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/HaHotCheckTest.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/HaHotCheckTest.java index 4b1faca5de..92c2f9acd8 100644 --- a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/HaHotCheckTest.java +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/HaHotCheckTest.java @@ -28,10 +28,10 @@ import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState; import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; -import org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter; import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; -import org.apache.brooklyn.rest.util.HaHotStateCheckClassResource; -import org.apache.brooklyn.rest.util.HaHotStateCheckResource; +import org.apache.brooklyn.rest.util.TestingHaHotStateCheckClassResource; +import org.apache.brooklyn.rest.util.TestingHaHotStateCheckResource; +import org.apache.brooklyn.rest.util.TestingHaMasterCheckResource; import org.apache.cxf.jaxrs.client.WebClient; import org.testng.annotations.Test; @@ -40,8 +40,9 @@ public class HaHotCheckTest extends BrooklynRestResourceTest { @Override protected void addBrooklynResources() { addResource(new HaHotCheckResourceFilter()); - addResource(new HaHotStateCheckResource()); - addResource(new HaHotStateCheckClassResource()); + addResource(new TestingHaHotStateCheckResource()); + addResource(new TestingHaHotStateCheckClassResource()); + addResource(new TestingHaMasterCheckResource()); ((LocalManagementContext)getManagementContext()).noteStartupComplete(); } @@ -58,6 +59,8 @@ public void testHaCheck() { testResourceFetch("/ha/method/ok", 200); testResourceFetch("/ha/method/fail", 200); testResourceFetch("/ha/class/fail", 200); + testResourcePost("/ha/post", 204); + testResourcePost("/server/shutdown", 204); getManagementContext().getHighAvailabilityManager().changeMode(HighAvailabilityMode.STANDBY); assertEquals(ha.getNodeState(), ManagementNodeState.STANDBY); @@ -65,6 +68,8 @@ public void testHaCheck() { testResourceFetch("/ha/method/ok", 200); testResourceFetch("/ha/method/fail", 403); testResourceFetch("/ha/class/fail", 403); + testResourcePost("/ha/post", 403); + testResourcePost("/server/shutdown", 204); ((ManagementContextInternal)getManagementContext()).terminate(); assertEquals(ha.getNodeState(), ManagementNodeState.TERMINATED); @@ -72,6 +77,8 @@ public void testHaCheck() { testResourceFetch("/ha/method/ok", 200); testResourceFetch("/ha/method/fail", 403); testResourceFetch("/ha/class/fail", 403); + testResourcePost("/ha/post", 403); + testResourcePost("/server/shutdown", 204); } @Test @@ -81,6 +88,8 @@ public void testHaCheckForce() { testResourceForcedFetch("/ha/method/ok", 200); testResourceForcedFetch("/ha/method/fail", 200); testResourceForcedFetch("/ha/class/fail", 200); + testResourceForcedPost("/ha/post", 204); + testResourceForcedPost("/server/shutdown", 204); getManagementContext().getHighAvailabilityManager().changeMode(HighAvailabilityMode.STANDBY); assertEquals(ha.getNodeState(), ManagementNodeState.STANDBY); @@ -88,6 +97,8 @@ public void testHaCheckForce() { testResourceForcedFetch("/ha/method/ok", 200); testResourceForcedFetch("/ha/method/fail", 200); testResourceForcedFetch("/ha/class/fail", 200); + testResourceForcedPost("/ha/post", 204); + testResourceForcedPost("/server/shutdown", 204); ((ManagementContextInternal)getManagementContext()).terminate(); assertEquals(ha.getNodeState(), ManagementNodeState.TERMINATED); @@ -95,6 +106,8 @@ public void testHaCheckForce() { testResourceForcedFetch("/ha/method/ok", 200); testResourceForcedFetch("/ha/method/fail", 200); testResourceForcedFetch("/ha/class/fail", 200); + testResourceForcedPost("/ha/post", 204); + testResourceForcedPost("/server/shutdown", 204); } @@ -102,10 +115,18 @@ private void testResourceFetch(String resourcePath, int code) { testResourceFetch(resourcePath, false, code); } + private void testResourcePost(String resourcePath, int code) { + testResourcePost(resourcePath, false, code); + } + private void testResourceForcedFetch(String resourcePath, int code) { testResourceFetch(resourcePath, true, code); } + private void testResourceForcedPost(String resourcePath, int code) { + testResourcePost(resourcePath, true, code); + } + private void testResourceFetch(String resourcePath, boolean force, int code) { WebClient resource = client().path(resourcePath) .accept(MediaType.APPLICATION_JSON_TYPE); @@ -117,4 +138,14 @@ private void testResourceFetch(String resourcePath, boolean force, int code) { assertEquals(response.getStatus(), code); } + private void testResourcePost(String resourcePath, boolean force, int code) { + WebClient resource = client().path(resourcePath) + .accept(MediaType.APPLICATION_JSON_TYPE); + if (force) { + resource.header(HaHotCheckResourceFilter.SKIP_CHECK_HEADER, "true"); + } + Response response = resource.post(null); + assertEquals(response.getStatus(), code); + } + } diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilterTest.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilterTest.java new file mode 100644 index 0000000000..0154055ae6 --- /dev/null +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilterTest.java @@ -0,0 +1,75 @@ +/* + * 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 static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotNull; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; +import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.http.HttpStatus; +import org.testng.annotations.Test; + +public class RequestTaggingRsFilterTest extends BrooklynRestResourceTest { + + @Path("/tag") + @Produces(MediaType.APPLICATION_JSON) + public static class TagResource { + @GET + public String tag() { + return RequestTaggingRsFilter.getTag(); + } + } + + @Override + protected void addBrooklynResources() { + addResource(new RequestTaggingRsFilter()); + addResource(new TagResource()); + } + + @Test + public void testTaggingFilter() { + String tag1 = fetchTag(); + String tag2 = fetchTag(); + assertNotEquals(tag1, tag2); + } + + private String fetchTag() { + Response response = fetch("/tag"); + assertEquals(response.getStatus(), HttpStatus.SC_OK); + String tag = (String) response.readEntity(String.class); + assertNotNull(tag); + return tag; + } + + protected Response fetch(String path) { + WebClient resource = client().path(path) + .accept(MediaType.APPLICATION_JSON_TYPE); + Response response = resource.get(); + return response; + } + +} diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModuleTest.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModuleTest.java new file mode 100644 index 0000000000..f33e80789d --- /dev/null +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModuleTest.java @@ -0,0 +1,195 @@ +/* + * 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.security.jaas; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; + +import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport; +import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; +import org.apache.brooklyn.rest.BrooklynWebConfig; +import org.apache.brooklyn.util.collections.MutableMap; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +// http://docs.oracle.com/javase/7/docs/technotes/guides/security/jaas/JAASLMDevGuide.html +public class BrooklynLoginModuleTest extends BrooklynMgmtUnitTestSupport { + private static final String ACCEPTED_USER = "user"; + private static final String ACCEPTED_PASSWORD = "password"; + private static final String DEFAULT_ROLE = "webconsole"; + private CallbackHandler GOOD_CB_HANDLER = new TestCallbackHandler( + ACCEPTED_USER, + ACCEPTED_PASSWORD); + private CallbackHandler BAD_CB_HANDLER = new TestCallbackHandler( + ACCEPTED_USER + ".invalid", + ACCEPTED_PASSWORD + ".invalid"); + + private Subject subject; + private Map sharedState; + private Map options; + + private BrooklynLoginModule module; + + @Override + @BeforeMethod(alwaysRun = true) + public void setUp() throws Exception { + BrooklynProperties properties = BrooklynProperties.Factory.newEmpty(); + properties.addFrom(ImmutableMap.of( + BrooklynWebConfig.USERS, ACCEPTED_USER, + BrooklynWebConfig.PASSWORD_FOR_USER("user"), ACCEPTED_PASSWORD)); + mgmt = LocalManagementContextForTests.builder(true).useProperties(properties).build(); + ManagementContextHolder.setManagementContextStatic(mgmt); + + super.setUp(); + + subject = new Subject(); + sharedState = MutableMap.of(); + options = ImmutableMap.of(); + + module = new BrooklynLoginModule(); + } + + @Test + public void testMissingCallback() throws LoginException { + module.initialize(subject, null, sharedState, options); + try { + module.login(); + fail("Login is supposed to fail due to missing callback"); + } catch (FailedLoginException e) { + // Expected, ignore + } + assertFalse(module.commit(), "commit"); + assertEmptyPrincipals(); + assertFalse(module.abort(), "abort"); + } + + @Test + public void testFailedLoginCommitAbort() throws LoginException { + badLogin(); + assertFalse(module.commit(), "commit"); + assertEmptyPrincipals(); + assertFalse(module.abort(), "abort"); + } + + @Test + public void testFailedLoginCommitAbortReadOnly() throws LoginException { + subject.setReadOnly(); + badLogin(); + assertFalse(module.commit(), "commit"); + assertEmptyPrincipals(); + assertFalse(module.abort(), "abort"); + } + + @Test + public void testFailedLoginAbort() throws LoginException { + badLogin(); + assertFalse(module.abort(), "abort"); + assertEmptyPrincipals(); + } + + @Test + public void testSuccessfulLoginCommitLogout() throws LoginException { + goodLogin(); + assertTrue(module.commit(), "commit"); + assertBrooklynPrincipal(); + assertTrue(module.logout(), "logout"); + assertEmptyPrincipals(); + } + + @Test + public void testSuccessfulLoginCommitAbort() throws LoginException { + goodLogin(); + assertTrue(module.commit(), "commit"); + assertBrooklynPrincipal(); + assertTrue(module.abort(), "logout"); + assertEmptyPrincipals(); + } + + @Test + public void testSuccessfulLoginCommitAbortReadOnly() throws LoginException { + subject.setReadOnly(); + goodLogin(); + try { + module.commit(); + fail("Commit expected to throw"); + } catch (LoginException e) { + // Expected + } + assertTrue(module.abort()); + } + + @Test + public void testSuccessfulLoginAbort() throws LoginException { + goodLogin(); + assertTrue(module.abort(), "abort"); + assertEmptyPrincipals(); + } + + @Test + public void testCustomRole() throws LoginException { + String role = "users"; + options = ImmutableMap.of(BrooklynLoginModule.PROPERTY_ROLE, role); + goodLogin(); + assertTrue(module.commit(), "commit"); + assertBrooklynPrincipal(role); + } + + private void goodLogin() throws LoginException { + module.initialize(subject, GOOD_CB_HANDLER, sharedState, options); + assertTrue(module.login(), "login"); + assertEmptyPrincipals(); + } + + private void badLogin() throws LoginException { + module.initialize(subject, BAD_CB_HANDLER, sharedState, options); + try { + module.login(); + fail("Login is supposed to fail due to invalid username+password pair"); + } catch (FailedLoginException e) { + // Expected, ignore + } + } + + private void assertBrooklynPrincipal() { + assertBrooklynPrincipal(DEFAULT_ROLE); + } + private void assertBrooklynPrincipal(String role) { + assertEquals(subject.getPrincipals(), ImmutableSet.of( + new BrooklynLoginModule.UserPrincipal(ACCEPTED_USER), + new BrooklynLoginModule.RolePrincipal(role))); + } + + private void assertEmptyPrincipals() { + assertEquals(subject.getPrincipals().size(), 0); + } + +} diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/jaas/TestCallbackHandler.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/jaas/TestCallbackHandler.java new file mode 100644 index 0000000000..4854196523 --- /dev/null +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/jaas/TestCallbackHandler.java @@ -0,0 +1,50 @@ +/* + * 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.security.jaas; + +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +public class TestCallbackHandler implements CallbackHandler { + private String username; + private String password; + + public TestCallbackHandler(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) + throws IOException, UnsupportedCallbackException { + for (Callback cb : callbacks) { + if (cb instanceof NameCallback) { + ((NameCallback)cb).setName(username); + } else if (cb instanceof PasswordCallback) { + ((PasswordCallback)cb).setPassword(password.toCharArray()); + } + } + } + +} diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckClassResource.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaHotStateCheckClassResource.java similarity index 95% rename from rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckClassResource.java rename to rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaHotStateCheckClassResource.java index 80e9c46ba0..a0084efa13 100644 --- a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckClassResource.java +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaHotStateCheckClassResource.java @@ -28,7 +28,7 @@ @Path("/ha/class") @Produces(MediaType.APPLICATION_JSON) @HaHotStateRequired -public class HaHotStateCheckClassResource { +public class TestingHaHotStateCheckClassResource { @GET @Path("fail") diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckResource.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaHotStateCheckResource.java similarity index 96% rename from rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckResource.java rename to rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaHotStateCheckResource.java index 5c9d4d17c8..512d725ad4 100644 --- a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckResource.java +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaHotStateCheckResource.java @@ -27,7 +27,7 @@ @Path("/ha/method") @Produces(MediaType.APPLICATION_JSON) -public class HaHotStateCheckResource { +public class TestingHaHotStateCheckResource { @GET @Path("ok") diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaMasterCheckResource.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaMasterCheckResource.java new file mode 100644 index 0000000000..d542868219 --- /dev/null +++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/TestingHaMasterCheckResource.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.util; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Produces(MediaType.APPLICATION_JSON) +public class TestingHaMasterCheckResource { + + @POST + @Path("/server/shutdown") + public void shutdown() { + + } + + @POST + @Path("/ha/post") + public void post() { + } + +} diff --git a/rest/rest-server-jersey/pom.xml b/rest/rest-server-jersey/pom.xml index a102681fcb..baaf31e54b 100644 --- a/rest/rest-server-jersey/pom.xml +++ b/rest/rest-server-jersey/pom.xml @@ -160,6 +160,10 @@ org.eclipse.jetty jetty-webapp
+ + org.eclipse.jetty + jetty-jaas + org.eclipse.jetty jetty-server @@ -255,6 +259,9 @@ **/HaHotCheckResourceFilter.java **/FormMapProvider.java **/ApidocResource.java + **/RequestTaggingFilter.java + **/EntitlementContextFilter.java + **/RequestTaggingRsFilter.java @@ -266,6 +273,22 @@ + + copy-rest-resources + generate-resources + copy-resources + + target/generated-resources/rest-deps + + + ../rest-resources/src/main/resources + + **/jaas.conf + + + + + copy-rest-test-resources generate-test-resources @@ -300,6 +323,18 @@ rest-resources + generate-resources + add-resource + + + + target/generated-resources/rest-deps + + + + + + rest-test-resources generate-test-resources add-test-resource diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java new file mode 100644 index 0000000000..2c3e2004fa --- /dev/null +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java @@ -0,0 +1,63 @@ +/* + * 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.security.Principal; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; +import org.apache.brooklyn.core.mgmt.entitlement.WebEntitlementContext; + +import com.sun.jersey.core.util.Priority; +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerRequestFilter; +import com.sun.jersey.spi.container.ContainerResponse; +import com.sun.jersey.spi.container.ContainerResponseFilter; + +@Priority(400) +public class EntitlementContextFilter implements ContainerRequestFilter, ContainerResponseFilter { + @Context + private HttpServletRequest servletRequest; + + @Override + public ContainerRequest filter(ContainerRequest request) { + SecurityContext securityContext = request.getSecurityContext(); + Principal user = securityContext.getUserPrincipal(); + + if (user != null) { + String uri = servletRequest.getRequestURI(); + String remoteAddr = servletRequest.getRemoteAddr(); + + String uid = RequestTaggingFilter.getTag(); + WebEntitlementContext entitlementContext = new WebEntitlementContext(user.getName(), remoteAddr, uri, uid); + Entitlements.setEntitlementContext(entitlementContext); + } + return request; + } + + @Override + public ContainerResponse filter(ContainerRequest request, ContainerResponse response) { + Entitlements.clearEntitlementContext(); + return response; + } + +} diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java index c65c78faa5..8f071674f6 100644 --- a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/HaHotCheckResourceFilter.java @@ -27,16 +27,17 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.ContextResolver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState; import org.apache.brooklyn.rest.domain.ApiError; import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.sun.jersey.api.model.AbstractMethod; +import com.sun.jersey.core.util.Priority; import com.sun.jersey.spi.container.ContainerRequest; import com.sun.jersey.spi.container.ContainerRequestFilter; import com.sun.jersey.spi.container.ContainerResponseFilter; @@ -52,7 +53,9 @@ * This follows a different pattern to {@link HaMasterCheckFilter} * as this needs to know the method being invoked. */ +@Priority(300) public class HaHotCheckResourceFilter implements ResourceFilterFactory { + private static final Set SAFE_STANDBY_METHODS = ImmutableSet.of("GET", "HEAD"); public static final String SKIP_CHECK_HEADER = "Brooklyn-Allow-Non-Master-Access"; private static final Logger log = LoggerFactory.getLogger(HaHotCheckResourceFilter.class); @@ -98,6 +101,10 @@ private String lookForProblem(ContainerRequest request) { if (isSkipCheckHeaderSet(request)) return null; + if (isMasterRequiredForRequest(request) && !isMaster()) { + return "server not in required HA master state"; + } + if (!isHaHotStateRequired(request)) return null; @@ -117,7 +124,7 @@ private String lookForProblem(ContainerRequest request) { 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 '"+SKIP_CHECK_HEADER+"' to force)"); + log.warn("Disallowing web request as "+problem+": "+request.getRequestUri()+"/"+am+" (caller should set '"+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()); @@ -132,6 +139,27 @@ private boolean isStateNotYetValid() { return mgmt.getRebindManager().isAwaitingInitialRebind(); } + private boolean isMaster() { + return ManagementNodeState.MASTER.equals( + mgmt.getHighAvailabilityManager() + .getNodeState()); + } + + private boolean isMasterRequiredForRequest(ContainerRequest requestContext) { + // gets usually okay + if (SAFE_STANDBY_METHODS.contains(requestContext.getMethod())) return false; + + String uri = requestContext.getRequestUri().toString(); + // 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 use an annotation HaAnyStateAllowed or HaHotCheckRequired(false) or similar + if ("server/shutdown".equals(uri) || + // Jersey compat + "/v1/server/shutdown".equals(uri)) return false; + + return true; + } + private boolean isHaHotStateRequired(ContainerRequest request) { return (am.getAnnotation(HaHotStateRequired.class) != null || am.getResource().getAnnotation(HaHotStateRequired.class) != null); diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java index 8a3c1c66b6..b66b3dceee 100644 --- a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/NoCacheFilter.java @@ -21,10 +21,12 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; +import com.sun.jersey.core.util.Priority; import com.sun.jersey.spi.container.ContainerRequest; import com.sun.jersey.spi.container.ContainerResponse; import com.sun.jersey.spi.container.ContainerResponseFilter; +@Priority(200) public class NoCacheFilter implements ContainerResponseFilter { @Override diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilter.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilter.java new file mode 100644 index 0000000000..588c5c1c58 --- /dev/null +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingRsFilter.java @@ -0,0 +1,76 @@ +/* + * 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 static com.google.common.base.Preconditions.checkNotNull; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; + +import org.apache.brooklyn.util.text.Identifiers; + +import com.sun.jersey.core.util.Priority; +import com.sun.jersey.spi.container.ContainerRequest; +import com.sun.jersey.spi.container.ContainerRequestFilter; +import com.sun.jersey.spi.container.ContainerResponse; +import com.sun.jersey.spi.container.ContainerResponseFilter; + +/** + * Tags each request with a probabilistically unique id. Should be included before other + * filters to make sense. + */ +@Priority(100) +public class RequestTaggingRsFilter implements ContainerRequestFilter, ContainerResponseFilter { + public static final String ATT_REQUEST_ID = RequestTaggingRsFilter.class.getName() + ".id"; + + @Context + private HttpServletRequest req; + + private static ThreadLocal tag = new ThreadLocal(); + + protected static String getTag() { + // Alternatively could use + // PhaseInterceptorChain.getCurrentMessage().getId() + + return checkNotNull(tag.get()); + } + + @Override + public ContainerRequest filter(ContainerRequest request) { + String requestId = getRequestId(); + tag.set(requestId); + return request; + } + + private String getRequestId() { + Object id = req.getAttribute(ATT_REQUEST_ID); + if (id != null) { + return id.toString(); + } else { + return Identifiers.makeRandomId(6); + } + } + + @Override + public ContainerResponse filter(ContainerRequest request, ContainerResponse response) { + tag.remove(); + return response; + } + +} diff --git a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java index d9013f0a2d..ce8b747104 100644 --- a/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java +++ b/rest/rest-server-jersey/src/main/java/org/apache/brooklyn/rest/filter/SwaggerFilter.java @@ -18,9 +18,7 @@ */ package org.apache.brooklyn.rest.filter; -import java.io.File; import java.io.IOException; -import java.net.URISyntaxException; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -29,7 +27,6 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; -import javax.ws.rs.core.UriBuilder; import org.apache.brooklyn.rest.apidoc.RestApiResourceScanner; diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java index b47a5919e2..8b67b126d4 100644 --- a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java +++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java @@ -47,7 +47,6 @@ import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider; import org.apache.brooklyn.rest.security.provider.SecurityProvider; import org.apache.brooklyn.rest.util.ManagementContextProvider; -import org.apache.brooklyn.rest.util.NullServletConfigProvider; import org.apache.brooklyn.rest.util.ServerStoppingShutdownHandler; import org.apache.brooklyn.rest.util.ShutdownHandlerProvider; import org.apache.brooklyn.util.core.osgi.Compat; @@ -251,7 +250,7 @@ private ContextHandler servletContextHandler(ManagementContext managementContext ResourceConfig config = new DefaultResourceConfig(); for (Object r: BrooklynRestApi.getAllResources()) config.getSingletons().add(r); - config.getSingletons().add(new ManagementContextProvider(mgmt)); + config.getSingletons().add(new ManagementContextProvider()); addShutdownListener(config, mgmt); @@ -382,7 +381,7 @@ private void installAsServletFilter(ServletContextHandler context, Listorg.eclipse.jetty jetty-servlet + + org.eclipse.jetty + jetty-jaas + javax.ws.rs javax.ws.rs-api diff --git a/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java b/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java index 6dd84e0623..c1d2d9f749 100644 --- a/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java +++ b/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java @@ -35,6 +35,8 @@ import org.apache.brooklyn.core.mgmt.entitlement.Entitlements; import org.apache.brooklyn.core.mgmt.entitlement.WebEntitlementContext; import org.apache.brooklyn.rest.BrooklynWebConfig; +import org.apache.brooklyn.rest.resources.LogoutResource; +import org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule; import org.apache.brooklyn.rest.security.provider.DelegatingSecurityProvider; import org.apache.brooklyn.rest.util.OsgiCompat; import org.apache.brooklyn.util.text.Strings; @@ -44,7 +46,10 @@ /** * Provides basic HTTP authentication. + * + * @deprecated since 0.9.0, use JAAS authentication instead, see {@link BrooklynLoginModule}, {@link LogoutResource}, {@link EntitlementContextFilter}. */ +@Deprecated public class BrooklynPropertiesSecurityFilter implements Filter { /** @@ -53,7 +58,7 @@ public class BrooklynPropertiesSecurityFilter implements Filter { * 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"; + public static final String AUTHENTICATED_USER_SESSION_ATTRIBUTE = BrooklynLoginModule.AUTHENTICATED_USER_SESSION_ATTRIBUTE; /** * The session attribute set to indicate the remote address of the HTTP request. diff --git a/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaMasterCheckFilter.java b/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaMasterCheckFilter.java index bfb1caf73e..156479d7e9 100644 --- a/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaMasterCheckFilter.java +++ b/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/HaMasterCheckFilter.java @@ -44,8 +44,10 @@ *

* 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. + * + * @deprecated since 0.9.0. Use JAX-RS {@link HaHotCheckResourceFilter} instead. */ -// TODO Merge with HaHotCheckResourceFilter so the functionality is available in Karaf +@Deprecated public class HaMasterCheckFilter implements Filter { private static final Set SAFE_STANDBY_METHODS = Sets.newHashSet("GET", "HEAD"); diff --git a/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingFilter.java b/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingFilter.java index 3553aaa1b0..85f5bf2f95 100644 --- a/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingFilter.java +++ b/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/RequestTaggingFilter.java @@ -33,7 +33,7 @@ * Tags each request with a probabilistically unique id. Should be included before other * filters to make sense. */ -//TODO Re-implement as JAX-RS filter +// TODO Deprecate after porting LoggingFilter public class RequestTaggingFilter implements Filter { private static ThreadLocal tag = new ThreadLocal(); @@ -45,6 +45,7 @@ protected static String getTag() { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String requestId = Identifiers.makeRandomId(6); + request.setAttribute(RequestTaggingRsFilter.ATT_REQUEST_ID, requestId); tag.set(requestId); try { chain.doFilter(request, response); diff --git a/rest/rest-server/src/main/webapp/WEB-INF/web.xml b/rest/rest-server/src/main/webapp/WEB-INF/web.xml index 7ae55a0cb7..b763b8ee2b 100644 --- a/rest/rest-server/src/main/webapp/WEB-INF/web.xml +++ b/rest/rest-server/src/main/webapp/WEB-INF/web.xml @@ -29,15 +29,6 @@ /* - - Brooklyn Properties Authentication Filter - org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter - - - Brooklyn Properties Authentication Filter - /* - - Brooklyn Logging Filter org.apache.brooklyn.rest.filter.LoggingFilter @@ -47,15 +38,6 @@ /* - - Brooklyn HA Master Filter - org.apache.brooklyn.rest.filter.HaMasterCheckFilter - - - Brooklyn HA Master Filter - /* - - diff --git a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java index d9f0f1a1b5..afec450817 100644 --- a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java +++ b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java @@ -36,10 +36,13 @@ import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.server.BrooklynServerConfig; import org.apache.brooklyn.core.server.BrooklynServiceAttributes; -import org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter; -import org.apache.brooklyn.rest.filter.HaMasterCheckFilter; +import org.apache.brooklyn.rest.filter.EntitlementContextFilter; +import org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter; import org.apache.brooklyn.rest.filter.LoggingFilter; +import org.apache.brooklyn.rest.filter.NoCacheFilter; import org.apache.brooklyn.rest.filter.RequestTaggingFilter; +import org.apache.brooklyn.rest.filter.RequestTaggingRsFilter; +import org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule.RolePrincipal; import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider; import org.apache.brooklyn.rest.security.provider.SecurityProvider; import org.apache.brooklyn.rest.util.ManagementContextProvider; @@ -51,6 +54,7 @@ import org.apache.brooklyn.util.net.Networking; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.text.WildcardGlobs; +import org.eclipse.jetty.jaas.JAASLoginService; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; @@ -89,11 +93,9 @@ enum StartMode { SERVLET, /** web-xml is not fully supported */ @Beta WEB_XML } - public static final List> DEFAULT_FILTERS = ImmutableList.of( + public static final List> DEFAULT_FILTERS = ImmutableList.>of( RequestTaggingFilter.class, - BrooklynPropertiesSecurityFilter.class, - LoggingFilter.class, - HaMasterCheckFilter.class); + LoggingFilter.class); private boolean forceUseOfDefaultCatalogWithJavaClassPath = false; private Class securityProvider; @@ -217,8 +219,12 @@ private ContextHandler servletContextHandler(ManagementContext managementContext installWar(context); RestApiSetup.installRest(context, - new ManagementContextProvider(managementContext), - new ShutdownHandlerProvider(shutdownListener)); + new ManagementContextProvider(), + new ShutdownHandlerProvider(shutdownListener), + new RequestTaggingRsFilter(), + new NoCacheFilter(), + new HaHotCheckResourceFilter(), + new EntitlementContextFilter()); RestApiSetup.installServletFilters(context, this.filters); context.setContextPath("/"); @@ -245,7 +251,6 @@ private void installWar(WebAppContext context) { /** NB: not fully supported; use one of the other {@link StartMode}s */ private ContextHandler webXmlContextHandler(ManagementContext mgmt) { RestApiSetup.initSwagger(); - // TODO add security to web.xml WebAppContext context; if (findMatchingFile("src/main/webapp")!=null) { // running in source mode; need to use special classpath @@ -290,7 +295,9 @@ public static Server startServer(ManagementContext mgmt, ContextHandler context, @Deprecated public static Server startServer(ContextHandler context, String summary, InetSocketAddress bindLocation) { Server server = new Server(bindLocation); - + + initJaas(server); + server.setHandler(context); try { server.start(); @@ -303,6 +310,15 @@ public static Server startServer(ContextHandler context, String summary, InetSoc return server; } + // TODO Why parallel code for server init here and in BrooklynWebServer? + private static void initJaas(Server server) { + JAASLoginService loginService = new JAASLoginService(); + loginService.setName("webconsole"); + loginService.setLoginModuleName("webconsole"); + loginService.setRoleClassNames(new String[] {RolePrincipal.class.getName()}); + server.addBean(loginService); + } + public static BrooklynRestApiLauncher launcher() { return new BrooklynRestApiLauncher(); }