Account for a possible rolled over file while reading the audit log file (#34909)

(cherry picked from commit 75cb6b38ed67dc9d32c9291b0c174ffa94e473bc)
This commit is contained in:
Andrei Stefan 2019-02-08 17:23:16 +02:00 committed by Andrei Stefan
parent e3c7b93917
commit 6359d988f0
3 changed files with 91 additions and 37 deletions

View File

@ -7,7 +7,7 @@ Project mainProject = project
group = "${group}.x-pack.qa.sql.security" group = "${group}.x-pack.qa.sql.security"
// Tests are pushed down to subprojects and will be checked there. // Tests are pushed down to subprojects and will be checked there.
testingConventions.enabled = false testingConventions.enabled = false
subprojects { subprojects {
// Use resources from the parent project in subprojects // Use resources from the parent project in subprojects
@ -41,8 +41,11 @@ subprojects {
} }
integTestRunner { integTestRunner {
def today = new Date().format('yyyy-MM-dd')
systemProperty 'tests.audit.logfile', systemProperty 'tests.audit.logfile',
"${ -> integTest.nodes[0].homeDir}/logs/${ -> integTest.nodes[0].clusterName }_audit.json" "${ -> integTest.nodes[0].homeDir}/logs/${ -> integTest.nodes[0].clusterName }_audit.json"
systemProperty 'tests.audit.yesterday.logfile',
"${ -> integTest.nodes[0].homeDir}/logs/${ -> integTest.nodes[0].clusterName }_audit-${today}.json"
} }
runqa { runqa {

View File

@ -84,6 +84,7 @@ public abstract class SqlSecurityTestCase extends ESRestTestCase {
* {@code plugin-security.policy}. So we may as well have gradle set the property. * {@code plugin-security.policy}. So we may as well have gradle set the property.
*/ */
private static final Path AUDIT_LOG_FILE = lookupAuditLog(); private static final Path AUDIT_LOG_FILE = lookupAuditLog();
private static final Path ROLLED_OVER_AUDIT_LOG_FILE = lookupRolledOverAuditLog();
@SuppressForbidden(reason="security doesn't work with mock filesystem") @SuppressForbidden(reason="security doesn't work with mock filesystem")
private static Path lookupAuditLog() { private static Path lookupAuditLog() {
@ -95,6 +96,16 @@ public abstract class SqlSecurityTestCase extends ESRestTestCase {
} }
return Paths.get(auditLogFileString); return Paths.get(auditLogFileString);
} }
@SuppressForbidden(reason="security doesn't work with mock filesystem")
private static Path lookupRolledOverAuditLog() {
String auditLogFileString = System.getProperty("tests.audit.yesterday.logfile");
if (null == auditLogFileString) {
throw new IllegalStateException("tests.audit.yesterday.logfile must be set to run this test. It should be automatically "
+ "set by gradle.");
}
return Paths.get(auditLogFileString);
}
private static boolean oneTimeSetup = false; private static boolean oneTimeSetup = false;
private static boolean auditFailure = false; private static boolean auditFailure = false;
@ -107,7 +118,12 @@ public abstract class SqlSecurityTestCase extends ESRestTestCase {
/** /**
* How much of the audit log was written before the test started. * How much of the audit log was written before the test started.
*/ */
private long auditLogWrittenBeforeTestStart; private static long auditLogWrittenBeforeTestStart;
/**
* If the audit log file rolled over. This is a rare case possible only at midnight.
*/
private static boolean auditFileRolledOver = false;
public SqlSecurityTestCase(Actions actions) { public SqlSecurityTestCase(Actions actions) {
this.actions = actions; this.actions = actions;
@ -164,7 +180,7 @@ public abstract class SqlSecurityTestCase extends ESRestTestCase {
return null; return null;
} }
if (false == Files.isRegularFile(AUDIT_LOG_FILE)) { if (false == Files.isRegularFile(AUDIT_LOG_FILE)) {
throw new IllegalStateException("expected tests.audit.logfile [" + AUDIT_LOG_FILE + "]to be a plain file but wasn't"); throw new IllegalStateException("expected tests.audit.logfile [" + AUDIT_LOG_FILE + "] to be a plain file but wasn't");
} }
try { try {
auditLogWrittenBeforeTestStart = Files.size(AUDIT_LOG_FILE); auditLogWrittenBeforeTestStart = Files.size(AUDIT_LOG_FILE);
@ -556,51 +572,85 @@ public abstract class SqlSecurityTestCase extends ESRestTestCase {
if (sm != null) { if (sm != null) {
sm.checkPermission(new SpecialPermission()); sm.checkPermission(new SpecialPermission());
} }
BufferedReader logReader = AccessController.doPrivileged((PrivilegedAction<BufferedReader>) () -> {
BufferedReader[] logReaders = new BufferedReader[2];
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
try { try {
return Files.newBufferedReader(AUDIT_LOG_FILE, StandardCharsets.UTF_8); // the audit log file rolled over during the test
// and we need to consume the rest of the rolled over file plus the new audit log file
if (auditFileRolledOver == false && Files.exists(ROLLED_OVER_AUDIT_LOG_FILE)) {
// once the audit file rolled over, it will stay like this
auditFileRolledOver = true;
// the order in the array matters, as the readers will be used in that order
logReaders[0] = Files.newBufferedReader(ROLLED_OVER_AUDIT_LOG_FILE, StandardCharsets.UTF_8);
}
logReaders[1] = Files.newBufferedReader(AUDIT_LOG_FILE, StandardCharsets.UTF_8);
return null;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
logReader.skip(auditLogWrittenBeforeTestStart);
// The "index" is used as a way of reading from both rolled over file and current audit file in order: rolled over file
// first, then the audit log file. Very rarely we will read from the rolled over file: when the test happened to run
// at midnight and the audit file rolled over during the test.
int index;
if (logReaders[0] != null) {
logReaders[0].skip(auditLogWrittenBeforeTestStart);
// start with the rolled over file first
index = 0;
} else {
// the current audit log file reader should always be non-null
logReaders[1].skip(auditLogWrittenBeforeTestStart);
// start with the current audit logging file
index = 1;
}
List<Map<String, Object>> logs = new ArrayList<>(); List<Map<String, Object>> logs = new ArrayList<>();
String line; String line;
while ((line = logReader.readLine()) != null) { while (index < 2) {
try { line = logReaders[index].readLine();
final Map<String, Object> log = XContentHelper.convertToMap(JsonXContent.jsonXContent, line, false); // when the end of the file is reached, either stop or move to the next reader
if (false == ("access_denied".equals(log.get("event.action")) if (line == null) {
|| "access_granted".equals(log.get("event.action")))) { if (++index == 2) {
continue; break;
} }
assertThat(log.containsKey("action"), is(true)); }
if (false == (SQL_ACTION_NAME.equals(log.get("action")) else {
|| GetIndexAction.NAME.equals(log.get("action")) try {
|| FieldCapabilitiesAction.NAME.equals(log.get("action")))) { final Map<String, Object> log = XContentHelper.convertToMap(JsonXContent.jsonXContent, line, false);
// TODO we may want to extend this and the assertions to SearchAction.NAME as well if (false == ("access_denied".equals(log.get("event.action"))
continue; || "access_granted".equals(log.get("event.action")))) {
} continue;
assertThat(log.containsKey("user.name"), is(true));
List<String> indices = new ArrayList<>();
if (log.containsKey("indices")) {
indices = (ArrayList<String>) log.get("indices");
if ("test_admin".equals(log.get("user.name"))) {
/*
* Sometimes we accidentally sneak access to the security tables. This is fine,
* SQL drops them from the interface. So we might have access to them, but we
* don't show them.
*/
indices.remove(".security");
indices.remove(".security-6");
} }
assertThat(log.containsKey("action"), is(true));
if (false == (SQL_ACTION_NAME.equals(log.get("action"))
|| GetIndexAction.NAME.equals(log.get("action"))
|| FieldCapabilitiesAction.NAME.equals(log.get("action")))) {
// TODO we may want to extend this and the assertions to SearchAction.NAME as well
continue;
}
assertThat(log.containsKey("user.name"), is(true));
List<String> indices = new ArrayList<>();
if (log.containsKey("indices")) {
indices = (ArrayList<String>) log.get("indices");
if ("test_admin".equals(log.get("user.name"))) {
/*
* Sometimes we accidentally sneak access to the security tables. This is fine,
* SQL drops them from the interface. So we might have access to them, but we
* don't show them.
*/
indices.remove(".security");
indices.remove(".security-6");
}
}
// Use a sorted list for indices for consistent error reporting
Collections.sort(indices);
log.put("indices", indices);
logs.add(log);
} catch (final ElasticsearchParseException e) {
throw new IllegalArgumentException("Unrecognized log: " + line, e);
} }
// Use a sorted list for indices for consistent error reporting
Collections.sort(indices);
log.put("indices", indices);
logs.add(log);
} catch (final ElasticsearchParseException e) {
throw new IllegalArgumentException("Unrecognized log: " + line, e);
} }
} }
List<Map<String, Object>> allLogs = new ArrayList<>(logs); List<Map<String, Object>> allLogs = new ArrayList<>(logs);

View File

@ -1,6 +1,7 @@
grant { grant {
// Needed to read the audit log file // Needed to read the audit log file
permission java.io.FilePermission "${tests.audit.logfile}", "read"; permission java.io.FilePermission "${tests.audit.logfile}", "read";
permission java.io.FilePermission "${tests.audit.yesterday.logfile}", "read";
//// Required by ssl subproject: //// Required by ssl subproject:
// Required for the net client to setup ssl rather than use global ssl. // Required for the net client to setup ssl rather than use global ssl.