/**
* Copyright 2015 StreamSets Inc.
*
* Licensed under 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 com.streamsets.datacollector.http;
import com.google.common.collect.ImmutableSet;
import com.streamsets.datacollector.main.MainStandalonePipelineManagerModule;
import com.streamsets.datacollector.main.RuntimeInfo;
import com.streamsets.datacollector.main.RuntimeModule;
import com.streamsets.datacollector.task.Task;
import com.streamsets.datacollector.task.TaskWrapper;
import com.streamsets.datacollector.util.Configuration;
import com.streamsets.lib.security.http.CORSConstants;
import com.streamsets.lib.security.http.RemoteSSOService;
import com.streamsets.testing.NetworkUtils;
import dagger.ObjectGraph;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.UUID;
public class TestHttpAccessControl {
private static String createTestDir() {
File dir = new File("target", UUID.randomUUID().toString());
Assert.assertTrue(dir.mkdirs());
return dir.getAbsolutePath();
}
private static class MockRegistrationServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(HttpServletResponse.SC_OK);
}
}
private static Server mockRegistrationServer;
private static int registrationPort;
private static String baseDir;
private static Task server;
private static String baseURL;
private static RuntimeInfo runtimeInfo;
@Before
public void setup() throws Exception {
registrationPort = NetworkUtils.getRandomPort();
mockRegistrationServer = new Server(registrationPort);
ServletContextHandler contextHandler = new ServletContextHandler();
contextHandler.addServlet(
new ServletHolder(new MockRegistrationServlet()),
"/security/public-rest/v1/components/registration"
);
contextHandler.setContextPath("/");
mockRegistrationServer.setHandler(contextHandler);
mockRegistrationServer.start();
server = null;
baseDir = createTestDir();
Assert.assertTrue(new File(baseDir, "etc").mkdir());
Assert.assertTrue(new File(baseDir, "data").mkdir());
Assert.assertTrue(new File(baseDir, "log").mkdir());
Assert.assertTrue(new File(baseDir, "web").mkdir());
System.setProperty(RuntimeModule.SDC_PROPERTY_PREFIX + RuntimeInfo.CONFIG_DIR, baseDir + "/etc");
System.setProperty(RuntimeModule.SDC_PROPERTY_PREFIX + RuntimeInfo.DATA_DIR, baseDir + "/data");
System.setProperty(RuntimeModule.SDC_PROPERTY_PREFIX + RuntimeInfo.LOG_DIR, baseDir + "/log");
System.setProperty(RuntimeModule.SDC_PROPERTY_PREFIX + RuntimeInfo.STATIC_WEB_DIR, baseDir + "/web");
/**
* Tests sending of restricted headers (Origin and Access-Control-Request-Method) which are
* used for CORS. These headers are by default skipped by the {@link java.net.HttpURLConnection}.
* The system property {@code sun.net.http.allowRestrictedHeaders} must be defined in order to
* allow these headers.
*/
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
}
@After
public void cleanup() throws Exception {
stopServer();
System.getProperties().remove(RuntimeModule.SDC_PROPERTY_PREFIX + RuntimeInfo.CONFIG_DIR);
System.getProperties().remove(RuntimeModule.SDC_PROPERTY_PREFIX + RuntimeInfo.DATA_DIR);
System.getProperties().remove(RuntimeModule.SDC_PROPERTY_PREFIX + RuntimeInfo.LOG_DIR);
System.getProperties().remove(RuntimeModule.SDC_PROPERTY_PREFIX + RuntimeInfo.STATIC_WEB_DIR);
if (mockRegistrationServer != null) {
mockRegistrationServer.stop();
}
}
private static String startServer(String authenticationType, boolean dpmEnabled) throws Exception {
int port = NetworkUtils.getRandomPort();
Configuration conf = new Configuration();
conf.set(WebServerTask.HTTP_PORT_KEY, port);
conf.set(WebServerTask.AUTHENTICATION_KEY, authenticationType);
conf.set(RemoteSSOService.SECURITY_SERVICE_APP_AUTH_TOKEN_CONFIG, "token");
conf.set(RemoteSSOService.SECURITY_SERVICE_COMPONENT_ID_CONFIG, "token");
conf.set(RemoteSSOService.DPM_ENABLED, dpmEnabled);
conf.set(RemoteSSOService.DPM_BASE_URL_CONFIG, "http://localhost:" + registrationPort);
Writer writer = writer = new FileWriter(new File(System.getProperty(RuntimeModule.SDC_PROPERTY_PREFIX +
RuntimeInfo.CONFIG_DIR), "sdc.properties"));
conf.save(writer);
writer.close();
File realmFile = new File(System.getProperty(RuntimeModule.SDC_PROPERTY_PREFIX +
RuntimeInfo.CONFIG_DIR), authenticationType + "-realm.properties");
writer = new FileWriter(realmFile);
writer.write("admin: admin,user,admin\n");
writer.write("multiRoleUser: multiRoleUser,user,creator,manager\n");
writer.close();
Files.setPosixFilePermissions(realmFile.toPath(), ImmutableSet.of(PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE));
ObjectGraph dagger = ObjectGraph.create(MainStandalonePipelineManagerModule.class);
runtimeInfo = dagger.get(RuntimeInfo.class);
runtimeInfo.setAttribute(RuntimeInfo.LOG4J_CONFIGURATION_URL_ATTR,
new URL("file://" + baseDir + "/log4j.properties"));
server = dagger.get(TaskWrapper.class);
server.init();
server.run();
return "http://127.0.0.1:" + port;
}
private static void stopServer() {
if (server != null) {
server.stop();
}
}
@Test
public void testForFormAuthentication() throws Exception {
String userInfoURI = startServer("form", false) + "/rest/v1/system/info/currentUser";
testPreFlightRequest(userInfoURI);
// failing in jenkins
testCORSGetRequest(userInfoURI);
}
@Test
public void testForSSOAuthentication() throws Exception {
String userInfoURI = startServer("", true) + "/rest/v1/system/info/currentUser";
testPreFlightRequest(userInfoURI);
}
@Test
public void testDisabledTrace() throws Exception {
String serverUrl = startServer("form", false) + "/jmx";
HttpURLConnection conn = (HttpURLConnection) new URL(serverUrl).openConnection();
conn.setRequestMethod("TRACE");
Assert.assertEquals(403, conn.getResponseCode());
}
/**
* Browser "pre flighted" requests first send an HTTP request by the 'OPTIONS' method to the resource on the other
* domain, in order to determine whether the actual request is safe to send.
*
* No authentication required for OPTIONS method
*
* @param userInfoURI URI
*/
private void testPreFlightRequest(String userInfoURI) {
Response response = ClientBuilder
.newClient()
.target(userInfoURI)
.request()
.options();
Assert.assertEquals(200, response.getStatus());
MultivaluedMap<String, Object> responseHeader = response.getHeaders();
List<Object> allowOriginHeader = responseHeader.get(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER);
Assert.assertNotNull(allowOriginHeader);
Assert.assertEquals(1, allowOriginHeader.size());
Assert.assertEquals(CORSConstants.HTTP_ACCESS_CONTROL_ALLOW_ORIGIN_DEFAULT, allowOriginHeader.get(0));
List<Object> allowHeadersHeader = responseHeader.get(CrossOriginFilter.ACCESS_CONTROL_ALLOW_HEADERS_HEADER);
Assert.assertNotNull(allowHeadersHeader);
Assert.assertEquals(1, allowHeadersHeader.size());
Assert.assertEquals(CORSConstants.HTTP_ACCESS_CONTROL_ALLOW_HEADERS_DEFAULT, allowHeadersHeader.get(0));
List<Object> allowMethodsHeader = responseHeader.get(CrossOriginFilter.ACCESS_CONTROL_ALLOW_METHODS_HEADER);
Assert.assertNotNull(allowMethodsHeader);
Assert.assertEquals(1, allowMethodsHeader.size());
Assert.assertEquals(CORSConstants.HTTP_ACCESS_CONTROL_ALLOW_METHODS_DEFAULT, allowMethodsHeader.get(0));
}
private void testCORSGetRequest(String userInfoURI) throws Exception {
HttpAuthenticationFeature authenticationFeature = HttpAuthenticationFeature.basic("admin", "admin");
Response response = ClientBuilder.newClient()
.target(userInfoURI)
.register(authenticationFeature)
.request()
.header("Origin", "http://example.com")
.header("Access-Control-Request-Method", "GET")
.get();
Assert.assertEquals(200, response.getStatus());
MultivaluedMap<String, Object> responseHeader = response.getHeaders();
List<Object> allowOriginHeader = responseHeader.get(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER);
Assert.assertNotNull(allowOriginHeader);
Assert.assertEquals(1, allowOriginHeader.size());
Assert.assertEquals("http://example.com", allowOriginHeader.get(0));
}
}