/**
* Copyright 2016 Confluent Inc.
*
* Licensed 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 io.confluent.rest;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
import javax.security.auth.login.Configuration;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Configurable;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import io.confluent.common.metrics.KafkaMetric;
import io.confluent.rest.annotations.PerformanceMetric;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class SaslTest {
private static final Logger log = LoggerFactory.getLogger(SaslTest.class);
private File jaasFile;
private File loginPropertiesFile;
private String previousAuthConfig;
private SaslTestApplication app;
private CloseableHttpClient httpclient;
String httpUri = "http://localhost:8080";
@Before
public void setUp() throws Exception {
jaasFile = File.createTempFile("jaas", ".config");
loginPropertiesFile = File.createTempFile("login", ".properties");
String jaas = "c3 {\n"
+ " org.eclipse.jetty.jaas.spi.PropertyFileLoginModule required\n"
+ " debug=\"true\"\n"
+ " file=\"" + loginPropertiesFile.getAbsolutePath() + "\";\n"
+ "};\n";
Files.write(
jaasFile.toPath(),
jaas.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.TRUNCATE_EXISTING
);
String loginProperties = "jay: kafka,Administrators\n"
+ "neha: akfak,Administrators\n"
+ "jun: kafka-\n";
Files.write(
loginPropertiesFile.toPath(),
loginProperties.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.TRUNCATE_EXISTING
);
previousAuthConfig = System.getProperty("java.security.auth.login.config");
Configuration.setConfiguration(null);
System.setProperty("java.security.auth.login.config", jaasFile.getAbsolutePath());
httpclient = HttpClients.createDefault();
TestMetricsReporter.reset();
Properties props = new Properties();
props.put(RestConfig.LISTENERS_CONFIG, httpUri);
props.put(RestConfig.METRICS_REPORTER_CLASSES_CONFIG, "io.confluent.rest.TestMetricsReporter");
configBasic(props);
TestRestConfig config = new TestRestConfig(props);
app = new SaslTestApplication(config);
app.start();
}
@After
public void cleanup() throws Exception {
Configuration.setConfiguration(null);
if (previousAuthConfig != null) {
System.setProperty("java.security.auth.login.config", previousAuthConfig);
}
httpclient.close();
app.stop();
}
private void configBasic(Properties props) {
props.put(RestConfig.AUTHENTICATION_METHOD_CONFIG, RestConfig.AUTHENTICATION_METHOD_BASIC);
props.put(RestConfig.AUTHENTICATION_REALM_CONFIG, "c3");
props.put(RestConfig.AUTHENTICATION_ROLES_CONFIG, Arrays.asList("Administrators"));
}
@Test
public void testNoAuthAttempt() throws Exception {
HttpResponse response = makeGetRequest(httpUri + "/test");
assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatusLine().getStatusCode());
assertMetricsCollected();
}
@Test
public void testBadLoginAttempt() throws Exception {
HttpResponse response = makeGetRequest(httpUri + "/test", "this shouldnt work");
assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatusLine().getStatusCode());
assertMetricsCollected();
}
@Test
public void testAuthorizedAttempt() throws Exception {
CloseableHttpResponse response = null;
try {
response = makeGetRequest(httpUri + "/principal", "bmVoYTpha2Zhaw=="); // neha's user/pass
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusLine().getStatusCode());
assertEquals("neha", EntityUtils.toString(response.getEntity()));
response.close();
response = makeGetRequest(httpUri + "/role/Administrators", "bmVoYTpha2Zhaw=="); // neha's user/pass
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusLine().getStatusCode());
assertEquals("true", EntityUtils.toString(response.getEntity()));
response.close();
response = makeGetRequest(httpUri + "/role/blah", "bmVoYTpha2Zhaw=="); // neha's user/pass
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusLine().getStatusCode());
assertEquals("false", EntityUtils.toString(response.getEntity()));
response.close();
assertMetricsCollected();
} finally {
if (response != null) {
response.close();
}
}
}
@Test
public void testUnauthorizedAttempt() throws Exception {
HttpResponse response = makeGetRequest(httpUri + "/principal", "anVuOmthZmthLQ=="); // jun's user/pass
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatusLine().getStatusCode());
assertMetricsCollected();
}
private void assertMetricsCollected() {
assertNotEquals("Expected to have metrics.", 0, TestMetricsReporter.getMetricTimeseries().size());
for (KafkaMetric metric : TestMetricsReporter.getMetricTimeseries()) {
if (metric.metricName().name().equals("request-latency-max")) {
assertTrue("Metrics should be collected (max latency shouldn't be 0)", metric.value() != 0.0);
}
}
}
// returns the http response status code.
private CloseableHttpResponse makeGetRequest(
String url,
String basicAuth
) throws Exception {
log.debug("Making GET " + url);
HttpGet httpget = new HttpGet(url);
if (basicAuth != null) {
httpget.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + basicAuth);
}
CloseableHttpResponse response = null;
response = httpclient.execute(httpget);
return response;
}
// returns the http response status code.
private HttpResponse makeGetRequest(String url) throws Exception {
return makeGetRequest(url, null);
}
private static class SaslTestApplication extends Application<TestRestConfig> {
public SaslTestApplication(TestRestConfig props) {
super(props);
}
@Override
public void setupResources(Configurable<?> config, TestRestConfig appConfig) {
config.register(new SaslTestResource());
}
@Override
public Map<String, String> getMetricsTags() {
return Collections.singletonMap("instance-id", "1");
}
}
@Path("/")
@Produces(MediaType.TEXT_PLAIN)
public static class SaslTestResource {
@GET
@Path("/principal")
@PerformanceMetric("principal")
public String principal(@Context SecurityContext context) {
return context.getUserPrincipal().getName();
}
@GET
@Path("/role/{role}")
@PerformanceMetric("role")
public boolean hello(
@PathParam("role") String role,
@Context SecurityContext context
) {
return context.isUserInRole(role);
}
}
}