blob: cf00e0c32e9bd871dfccb366bac87fd71ff56d31 [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.tinkerpop.gremlin.server;
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.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.tinkerpop.gremlin.driver.Channelizer;
import org.apache.tinkerpop.gremlin.driver.Client;
import org.apache.tinkerpop.gremlin.driver.Cluster;
import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
import org.apache.tinkerpop.gremlin.server.auth.Krb5Authenticator;
import org.apache.tinkerpop.gremlin.server.auth.SimpleAuthenticator;
import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer;
import org.apache.tinkerpop.gremlin.server.channel.NioChannelizer;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONTokens;
import org.apache.tinkerpop.gremlin.util.Log4jRecordingAppender;
import org.apache.tinkerpop.shaded.jackson.databind.JsonNode;
import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.apache.log4j.Level.INFO;
import static org.apache.tinkerpop.gremlin.server.GremlinServer.AUDIT_LOGGER_NAME;
import static org.apache.tinkerpop.gremlin.server.GremlinServerAuthKrb5IntegrateTest.TESTCONSOLE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Test audit logs and like other descendants of AbstractGremlinServerIntegrationTest this test suite assumes that
* tests are run sequentially and thus the server and kdcServer variables can be reused.
*
* @author Marc de Lignie
*/
public class GremlinServerAuditLogIntegrateTest extends AbstractGremlinServerIntegrationTest {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(GremlinServerAuditLogIntegrateTest.class);
private Log4jRecordingAppender recordingAppender = null;
private final ObjectMapper mapper = new ObjectMapper();
private final Base64.Encoder encoder = Base64.getUrlEncoder();
private static final boolean AUDIT_LOG_ENABLED = true;
private static final boolean AUDIT_LOG_DISABLED = false;
private static final String TESTCONSOLE2 = "GremlinConsole2";
private KdcFixture kdcServer;
@Before
@Override
public void setUp() throws Exception {
recordingAppender = new Log4jRecordingAppender();
final Logger rootLogger = Logger.getRootLogger();
rootLogger.addAppender(recordingAppender);
try {
final String buildDir = System.getProperty("build.dir");
kdcServer = new KdcFixture(buildDir +
"/test-classes/org/apache/tinkerpop/gremlin/server/gremlin-console-jaas.conf");
kdcServer.setUp();
} catch(Exception e) {
logger.warn(e.getMessage());
}
super.setUp();
}
@After
@Override
public void tearDown() throws Exception {
super.tearDown();
final Logger rootLogger = Logger.getRootLogger();
rootLogger.removeAppender(recordingAppender);
kdcServer.close();
}
/**
* Configure specific Gremlin Server settings for specific tests.
*/
@Override
public Settings overrideSettings(final Settings settings) {
settings.host = kdcServer.hostname;
final Settings.SslSettings sslConfig = new Settings.SslSettings();
sslConfig.enabled = false;
settings.ssl = sslConfig;
final Settings.AuthenticationSettings authSettings = new Settings.AuthenticationSettings();
settings.authentication = authSettings;
authSettings.enableAuditLog = AUDIT_LOG_ENABLED;
authSettings.className = Krb5Authenticator.class.getName();
final Map<String,Object> authConfig = new HashMap<>();
authSettings.config = authConfig;
final String nameOfTest = name.getMethodName();
switch (nameOfTest) {
case "shouldAuditLogWithAllowAllAuthenticator":
authSettings.authenticator = AllowAllAuthenticator.class.getName();
break;
case "shouldAuditLogWithSimpleAuthenticator":
authSettings.authenticator = SimpleAuthenticator.class.getName();
authConfig.put(SimpleAuthenticator.CONFIG_CREDENTIALS_DB, "conf/tinkergraph-credentials.properties");
break;
case "shouldNotAuditLogWhenDisabled":
authSettings.enableAuditLog = AUDIT_LOG_DISABLED;
case "shouldAuditLogWithKrb5Authenticator":
case "shouldAuditLogTwoClientsWithKrb5Authenticator":
authConfig.put("keytab", kdcServer.serviceKeytabFile.getAbsolutePath());
authConfig.put("principal", kdcServer.serverPrincipal);
break;
case "shouldAuditLogWithNioTransport":
settings.channelizer = NioChannelizer.class.getName();
authConfig.put("keytab", kdcServer.serviceKeytabFile.getAbsolutePath());
authConfig.put("principal", kdcServer.serverPrincipal);
break;
case "shouldAuditLogWithHttpTransport":
settings.host = "localhost";
settings.channelizer = HttpChannelizer.class.getName();
authSettings.authenticator = SimpleAuthenticator.class.getName();
authConfig.put(SimpleAuthenticator.CONFIG_CREDENTIALS_DB, "conf/tinkergraph-credentials.properties");
break;
}
return settings;
}
@Test
public void shouldAuditLogWithAllowAllAuthenticator() throws Exception {
final Cluster cluster = TestClientFactory.build().addContactPoint(kdcServer.hostname).create();
final Client client = cluster.connect();
try {
assertEquals(2, client.submit("1+1").all().get().get(0).getInt());
assertEquals(3, client.submit("1+2").all().get().get(0).getInt());
assertEquals(4, client.submit("1+3").all().get().get(0).getInt());
} finally {
cluster.close();
}
// wait for logger to flush - (don't think there is a way to detect this)
stopServer();
Thread.sleep(1000);
// WebSocketChannelizer does not add SaslAuthenticationHandler for AllowAllAuthenticator,
// so no authenticated user log line available
assertTrue(recordingAppender.logMatchesAny(AUDIT_LOGGER_NAME, INFO, "User with address .*? requested: 1\\+1"));
assertTrue(recordingAppender.logMatchesAny(AUDIT_LOGGER_NAME, INFO, "User with address .*? requested: 1\\+2"));
assertTrue(recordingAppender.logMatchesAny(AUDIT_LOGGER_NAME, INFO, "User with address .*? requested: 1\\+3"));
}
@Test
public void shouldAuditLogWithSimpleAuthenticator() throws Exception {
final String username = "stephen";
final String password = "password";
final Cluster cluster = TestClientFactory.build().credentials(username, password)
.addContactPoint(kdcServer.hostname).create();
final Client client = cluster.connect();
try {
assertEquals(2, client.submit("1+1").all().get().get(0).getInt());
assertEquals(3, client.submit("1+2").all().get().get(0).getInt());
assertEquals(4, client.submit("1+3").all().get().get(0).getInt());
assertEquals(5, client.submit("1+4").all().get().get(0).getInt());
} finally {
cluster.close();
}
// wait for logger to flush - (don't think there is a way to detect this)
stopServer();
Thread.sleep(1000);
final String simpleAuthenticatorName = SimpleAuthenticator.class.getSimpleName();
final List<LoggingEvent> log = recordingAppender.getEvents();
final Stream<LoggingEvent> auditEvents = log.stream().filter(event -> event.getLoggerName().equals(AUDIT_LOGGER_NAME));
final LoggingEvent authEvent = auditEvents
.filter(event -> event.getMessage().toString().contains(simpleAuthenticatorName)).iterator().next();
final String authMsg = authEvent.getMessage().toString();
assertTrue(authEvent.getLevel() == INFO &&
authMsg.matches(String.format("User %s with address .*? authenticated by %s", username, simpleAuthenticatorName)));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+1")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+2")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+3")));
}
@Test
public void shouldAuditLogWithKrb5Authenticator() throws Exception {
final Cluster cluster = TestClientFactory.build().jaasEntry(TESTCONSOLE)
.protocol(kdcServer.serverPrincipalName).addContactPoint(kdcServer.hostname).create();
final Client client = cluster.connect();
try {
assertEquals(2, client.submit("1+1").all().get().get(0).getInt());
assertEquals(3, client.submit("1+2").all().get().get(0).getInt());
assertEquals(4, client.submit("1+3").all().get().get(0).getInt());
} finally {
cluster.close();
}
// wait for logger to flush - (don't think there is a way to detect this)
stopServer();
Thread.sleep(1000);
final List<LoggingEvent> log = recordingAppender.getEvents();
final Stream<LoggingEvent> auditEvents = log.stream().filter(event -> event.getLoggerName().equals(AUDIT_LOGGER_NAME));
final LoggingEvent authEvent = auditEvents
.filter(event -> event.getMessage().toString().contains("Krb5Authenticator")).iterator().next();
final String authMsg = authEvent.getMessage().toString();
assertTrue(authEvent.getLevel() == INFO &&
authMsg.matches(String.format("User %s with address .*? authenticated by Krb5Authenticator", kdcServer.clientPrincipalName)));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+1")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+2")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+3")));
}
@Test
public void shouldNotAuditLogWhenDisabled() throws Exception {
final Cluster cluster = TestClientFactory.build().jaasEntry(TESTCONSOLE)
.protocol(kdcServer.serverPrincipalName).addContactPoint(kdcServer.hostname).create();
final Client client = cluster.connect();
try {
assertEquals(2, client.submit("1+1").all().get().get(0).getInt());
assertEquals(3, client.submit("1+2").all().get().get(0).getInt());
assertEquals(4, client.submit("1+3").all().get().get(0).getInt());
} finally {
cluster.close();
}
// wait for logger to flush - (don't think there is a way to detect this)
stopServer();
Thread.sleep(1000);
final List<LoggingEvent> log = recordingAppender.getEvents();
assertFalse(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User .*? with address .*? authenticated by Krb5Authenticator")));
assertFalse(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+1")));
assertFalse(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+2")));
assertFalse(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+3")));
}
@Test
public void shouldAuditLogWithNioTransport() throws Exception {
final Cluster cluster = TestClientFactory.build().channelizer(Channelizer.NioChannelizer.class.getName())
.jaasEntry(TESTCONSOLE).protocol(kdcServer.serverPrincipalName).addContactPoint(kdcServer.hostname).create();
final Client client = cluster.connect();
try {
assertEquals(2, client.submit("1+1").all().get().get(0).getInt());
assertEquals(3, client.submit("1+2").all().get().get(0).getInt());
assertEquals(4, client.submit("1+3").all().get().get(0).getInt());
} finally {
cluster.close();
}
// wait for logger to flush - (don't think there is a way to detect this)
stopServer();
Thread.sleep(1000);
final List<LoggingEvent> log = recordingAppender.getEvents();
final Stream<LoggingEvent> auditEvents = log.stream().filter(event -> event.getLoggerName().equals(AUDIT_LOGGER_NAME));
final LoggingEvent authEvent = auditEvents
.filter(event -> event.getMessage().toString().contains("Krb5Authenticator")).iterator().next();
final String authMsg = authEvent.getMessage().toString();
assertTrue(authEvent.getLevel() == INFO &&
authMsg.matches(String.format("User %s with address .*? authenticated by Krb5Authenticator", kdcServer.clientPrincipalName)));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+1")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+2")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+3")));
}
@Test
public void shouldAuditLogWithHttpTransport() throws Exception {
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpGet httpget = new HttpGet(TestClientFactory.createURLString("?gremlin=1-1"));
httpget.addHeader("Authorization", "Basic " + encoder.encodeToString("stephen:password".getBytes()));
try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
assertEquals(200, response.getStatusLine().getStatusCode());
assertEquals("application/json", response.getEntity().getContentType().getValue());
final String json = EntityUtils.toString(response.getEntity());
final JsonNode node = mapper.readTree(json);
assertEquals(0, node.get("result").get("data").get(GraphSONTokens.VALUEPROP).get(0).intValue());
}
// wait for logger to flush - (don't think there is a way to detect this)
stopServer();
Thread.sleep(1000);
final String simpleAuthenticatorName = SimpleAuthenticator.class.getSimpleName();
final List<LoggingEvent> log = recordingAppender.getEvents();
final Stream<LoggingEvent> auditEvents = log.stream().filter(event -> event.getLoggerName().equals(AUDIT_LOGGER_NAME));
final LoggingEvent authEvent = auditEvents
.filter(event -> event.getMessage().toString().contains(simpleAuthenticatorName)).iterator().next();
final String authMsg = authEvent.getMessage().toString();
assertTrue(authEvent.getLevel() == INFO &&
authMsg.matches(String.format("User %s with address .*? authenticated by %s", "stephen", simpleAuthenticatorName)));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1-1")));
}
@Test
public void shouldAuditLogTwoClientsWithKrb5Authenticator() throws Exception {
final Cluster cluster = TestClientFactory.build().jaasEntry(TESTCONSOLE)
.protocol(kdcServer.serverPrincipalName).addContactPoint(kdcServer.hostname).create();
final Client client = cluster.connect();
final Cluster cluster2 = TestClientFactory.build().jaasEntry(TESTCONSOLE2)
.protocol(kdcServer.serverPrincipalName).addContactPoint(kdcServer.hostname).create();
final Client client2 = cluster2.connect();
try {
assertEquals(2, client.submit("1+1").all().get().get(0).getInt());
assertEquals(22, client2.submit("11+11").all().get().get(0).getInt());
assertEquals(3, client.submit("1+2").all().get().get(0).getInt());
assertEquals(23, client2.submit("11+12").all().get().get(0).getInt());
assertEquals(24, client2.submit("11+13").all().get().get(0).getInt());
assertEquals(4, client.submit("1+3").all().get().get(0).getInt());
} finally {
cluster.close();
cluster2.close();
}
// wait for logger to flush - (don't think there is a way to detect this)
stopServer();
Thread.sleep(1000);
final List<LoggingEvent> log = recordingAppender.getEvents();
final Stream<LoggingEvent> auditEvents = log.stream().filter(event -> event.getLoggerName().equals(AUDIT_LOGGER_NAME));
final LoggingEvent authEvent = auditEvents
.filter(event -> event.getMessage().toString().contains("Krb5Authenticator")).iterator().next();
final String authMsg = authEvent.getMessage().toString();
assertTrue(authEvent.getLevel() == INFO &&
authMsg.matches(String.format("User %s with address .*? authenticated by Krb5Authenticator", kdcServer.clientPrincipalName)));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+1")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+2")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 1\\+3")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches(String.format("User %s with address .*? authenticated by Krb5Authenticator", kdcServer.clientPrincipalName2))));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 11\\+11")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 11\\+12")));
assertTrue(log.stream().anyMatch(item -> item.getLevel() == INFO &&
item.getMessage().toString().matches("User with address .*? requested: 11\\+13")));
}
}