blob: 4312e36ffedb4941147b1569c2d9ecc611c5013f [file] [log] [blame]
/*
* 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.sling.scripting.core.it;
import static org.apache.sling.testing.paxexam.SlingOptions.slingAuthForm;
import static org.apache.sling.testing.paxexam.SlingOptions.slingQuickstartOakTar;
import static org.apache.sling.testing.paxexam.SlingOptions.slingScriptingJavascript;
import static org.apache.sling.testing.paxexam.SlingOptions.versionResolver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.ops4j.pax.exam.CoreOptions.composite;
import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
import static org.ops4j.pax.exam.CoreOptions.vmOption;
import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.Callable;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
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.options.ModifiableCompositeOption;
import org.ops4j.pax.exam.options.extra.VMOption;
import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
import org.ops4j.pax.exam.spi.reactors.PerClass;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tests for SLING-10147 - verify scripting variables implementation details are not
* exposed to not authorized users
*/
@RunWith(Enclosed.class)
public class SLING_10147IT {
private SLING_10147IT() {
// private constructor to hide the implicit public one
}
/**
* Base class to share the common parts of LoadedIT and NotLoadedIT
* tests
*/
public abstract static class BaseIT extends ScriptingCoreTestSupport {
protected final Logger logger = LoggerFactory.getLogger(getClass());
protected CloseableHttpClient httpClient;
@Rule
public TestRule watcher = new TestWatcher() {
/* (non-Javadoc)
* @see org.junit.rules.TestWatcher#starting(org.junit.runner.Description)
*/
@Override
protected void starting(Description description) {
logger.info("Starting test: {}", description.getMethodName());
}
/* (non-Javadoc)
* @see org.junit.rules.TestWatcher#finished(org.junit.runner.Description)
*/
@Override
protected void finished(Description description) {
logger.info("Finished test: {}", description.getMethodName());
}
};
@Before
public void before() {
waitForServices();
int timeout = 15; // seconds
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(timeout * 1000)
.setConnectionRequestTimeout(timeout * 1000)
.setSocketTimeout(timeout * 1000).build();
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
.setRedirectStrategy(new LaxRedirectStrategy())
.setDefaultRequestConfig(config);
httpClient = httpClientBuilder.build();
}
/**
* Wait for services to be available
*/
protected void waitForServices() {
final BundleContext bundleContext = FrameworkUtil.getBundle(SLING_10147IT.class).getBundleContext();
Awaitility.await()
.atMost(Duration.ofMinutes(5))
.pollInterval(Duration.ofSeconds(5))
.until(new Callable<Boolean>() {
private String [] servicesLists = new String[] {
"org.apache.sling.jcr.api.SlingRepository",
"org.apache.sling.engine.auth.Authenticator",
"org.apache.sling.api.resource.ResourceResolverFactory",
"org.apache.sling.api.servlets.ServletResolver",
"javax.script.ScriptEngineManager"
};
@Override
public Boolean call() throws Exception {
boolean foundAllServices = true;
for (String serviceClass : servicesLists) {
ServiceReference<?> serviceReference = bundleContext.getServiceReference(serviceClass);
if (serviceReference == null) {
foundAllServices = false;
break;
}
}
return foundAllServices;
}
});
}
@After
public void after() throws IOException {
if (httpClient != null) {
httpClient.close();
httpClient = null;
}
}
@Configuration
public Option[] configuration() {
versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.api");
versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.resourceresolver");
versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.servlets.resolver");
versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.scripting.api");
final Option webconsolesecurityprovider = mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.extensions.webconsolesecurityprovider").versionAsInProject();
return composite(
slingQuickstartOakTar(String.format("target/%s", getClass().getSimpleName()), httpPort),
slingScriptingJavascript(),
slingAuthForm(),
baseConfiguration(),
mavenBundle().groupId("org.apache.httpcomponents").artifactId("httpcore-osgi").version(versionResolver),
mavenBundle().groupId("org.apache.httpcomponents").artifactId("httpclient-osgi").version(versionResolver),
webconsolesecurityprovider,
optionalRemoteDebug()
).remove(
scriptingCore // remove the old version
).getOptions();
}
/**
* Optionally configure remote debugging on the port supplied by the "debugPort"
* system property.
*/
protected ModifiableCompositeOption optionalRemoteDebug() {
VMOption option = null;
String property = System.getProperty("debugPort");
if (property != null) {
option = vmOption(String.format("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=%s", property));
}
return composite(option);
}
protected void checkContentType(CloseableHttpResponse response ,String expected) {
// Remove whatever follows semicolon in content-type
HttpEntity entity = response.getEntity();
Header contentTypeHeader = entity.getContentType();
String contentType = contentTypeHeader == null ? null : contentTypeHeader.getValue();
if (contentType != null) {
contentType = contentType.split(";")[0].trim();
}
// check for match
assertEquals(expected, contentType);
}
}
/**
* Verify that when authorized that the scripting variables is accessible
*/
@RunWith(PaxExam.class)
@ExamReactorStrategy(PerClass.class)
public static class AuthorizedIT extends BaseIT {
@Test
public void testGetSlingBindingsVariablesListJsonWithAuthorizedUser() throws IOException, AuthenticationException {
// make GET request and verify it returned a 200 ok
HttpGet request = new HttpGet(String.format("http://localhost:%d/.SLING_availablebindings.json?extension=esp", httpPort()));
// pre-emptive basic authentication
request.addHeader(new BasicScheme().authenticate(new UsernamePasswordCredentials("admin", "admin"), request, null));
if (logger.isInfoEnabled()) {
logger.info("Executing GET Request to: {}", request.getURI());
}
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (logger.isInfoEnabled()) {
logger.info("Response Content\r\n: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
}
// should have passed all security checks and returned the JSON payload
assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
checkContentType(response, "application/json");
}
}
}
/**
* Verify that when not authorized that the scripting variables is not accessible
*/
@RunWith(PaxExam.class)
@ExamReactorStrategy(PerClass.class)
public static class NotAuthorizedIT extends BaseIT {
@Configuration
@Override
public Option[] configuration() {
return composite(super.configuration())
.add(// testing - add a user to use to login and verify
factoryConfiguration("org.apache.sling.jcr.repoinit.RepositoryInitializer")
.put("scripts", new String[] {
"create user sling10147 with password sling10147"
})
.asOption()
)
.getOptions();
}
@Test
public void testGetSlingBindingsVariablesListJsonWithNotAuthorizedUser() throws IOException, AuthenticationException {
// make GET request and verify it is denied for user without sufficient rights
HttpGet request = new HttpGet(String.format("http://localhost:%d/.SLING_availablebindings.json?extension=esp", httpPort()));
// pre-emptive basic authentication
request.addHeader(new BasicScheme().authenticate(new UsernamePasswordCredentials("sling10147", "sling10147"), request, null));
if (logger.isInfoEnabled()) {
logger.info("Executing GET Request to: {}", request.getURI());
}
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (logger.isInfoEnabled()) {
logger.info("Response Content\r\n: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
}
// should have been denied access by WebConsoleSecurityProvider2 service access not granted to the user
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatusLine().getStatusCode());
checkContentType(response, "text/html");
}
}
@Test
public void testGetSlingBindingsVariablesListJsonWithAnonymousUser() throws IOException, AuthenticationException {
// make GET request and verify it returned a forms login page challenge
HttpGet request = new HttpGet(String.format("http://localhost:%d/.SLING_availablebindings.json?extension=esp", httpPort()));
if (logger.isInfoEnabled()) {
logger.info("Executing GET Request to: {}", request.getURI());
}
try (CloseableHttpResponse response = httpClient.execute(request)) {
String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
if (logger.isInfoEnabled()) {
logger.info("Response Content\r\n: {}", content);
}
// should have been challenged for authentication by the WebConsoleSecurity2#authenitcate call
assertEquals(HttpServletResponse.SC_OK, response.getStatusLine().getStatusCode());
checkContentType(response, "text/html");
assertTrue("Expected forms based login page", content.contains("Login to Apache Sling"));
}
}
}
/**
* Verify that when no WebConsoleSecurityProvider2 service is available that
* the scripting variables is not accessible
*/
@RunWith(PaxExam.class)
@ExamReactorStrategy(PerClass.class)
public static class HasNoWebConsoleSecurityProvider2IT extends BaseIT {
@Configuration
@Override
public Option[] configuration() {
// remove the security provider bundle from the configuration
final Option webconsolesecurityprovider = mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.extensions.webconsolesecurityprovider").versionAsInProject();
return composite(super.configuration())
.remove(webconsolesecurityprovider)
.getOptions();
}
@Test
public void testGetSlingBindingsVariablesListJsonWithoutWebConsoleSecurityProvider2() throws IOException, AuthenticationException {
// make GET request and verify it returned a 403 error
HttpGet request = new HttpGet(String.format("http://localhost:%d/.SLING_availablebindings.json?extension=esp", httpPort()));
// pre-emptive basic authentication
request.addHeader(new BasicScheme().authenticate(new UsernamePasswordCredentials("admin", "admin"), request, null));
if (logger.isInfoEnabled()) {
logger.info("Executing GET Request to: {}", request.getURI());
}
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (logger.isInfoEnabled()) {
logger.info("Response Content\r\n: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
}
// should have failed fast due to the missing WebConsoleSecurityProvider2 service
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatusLine().getStatusCode());
checkContentType(response, "text/html");
}
}
}
}