/*
 * 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
 *
 *   https://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.accumulo.test;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.accumulo.core.client.Accumulo;
import org.apache.accumulo.core.client.AccumuloClient;
import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.AccumuloSecurityException;
import org.apache.accumulo.core.client.BatchScanner;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.TableExistsException;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.admin.ImportConfiguration;
import org.apache.accumulo.core.client.admin.NewTableConfiguration;
import org.apache.accumulo.core.client.admin.TableOperations;
import org.apache.accumulo.core.client.admin.TabletAvailability;
import org.apache.accumulo.core.client.security.tokens.PasswordToken;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.accumulo.core.security.SystemPermission;
import org.apache.accumulo.core.security.TablePermission;
import org.apache.accumulo.miniclusterImpl.MiniAccumuloConfigImpl;
import org.apache.accumulo.server.security.AuditedSecurityOperation;
import org.apache.accumulo.test.functional.ConfigurableMacBase;
import org.apache.hadoop.io.Text;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Tests that Accumulo is outputting audit messages as expected. Since this is using
 * MiniAccumuloCluster, it could take a while if we test everything in isolation. We test blocks of
 * related operations, run the whole test in one MiniAccumulo instance, trying to clean up objects
 * between each test. The MiniAccumuloClusterTest sets up the log4j stuff differently to an
 * installed instance, instead piping everything through stdout and writing to a set location so we
 * have to find the logs and grep the bits we need out.
 */
public class AuditMessageIT extends ConfigurableMacBase {

  private static final String AUDIT_USER_1 = "AuditUser1";
  private static final String AUDIT_USER_2 = "AuditUser2";
  private static final String PASSWORD = "password";
  private static final String OLD_TEST_TABLE_NAME = "apples";
  private static final String OLD_TEST_TABLE_NAME_ID = "1";
  private static final String NEW_TEST_TABLE_NAME = "oranges";
  private static final String THIRD_TEST_TABLE_NAME = "pears";
  private static final Authorizations auths = new Authorizations("private", "public");

  @Override
  protected Duration defaultTimeout() {
    return Duration.ofMinutes(1);
  }

  @Override
  public void beforeClusterStart(MiniAccumuloConfigImpl cfg) {
    cfg.getClusterServerConfiguration().setNumDefaultTabletServers(1);
  }

  // Must be static to survive Junit re-initialising the class every time.
  private static String lastAuditTimestamp;
  private AccumuloClient auditAccumuloClient;
  private AccumuloClient client;

  private static long findAuditMessage(ArrayList<String> input, String pattern) {
    return input.stream().filter(Pattern.compile(".*" + pattern + ".*").asMatchPredicate()).count();
  }

  /**
   * Returns a List of Audit messages that have been grep'd out of the MiniAccumuloCluster output.
   *
   * @param stepName A unique name for the test being executed, to identify the System.out messages.
   * @return A List of the Audit messages, sorted (so in chronological order).
   */
  private ArrayList<String> getAuditMessages(String stepName) throws IOException {
    // ACCUMULO-3144 Make sure we give the processes enough time to flush the write buffer
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new IOException("Interrupted waiting for data to be flushed to output streams");
    }

    // Grab the audit messages
    System.out.println("Start of captured audit messages for step " + stepName);

    final var matchesAuditLogPattern = Pattern.compile(
        ".* \\[" + AuditedSecurityOperation.AUDITLOG.replace("org.apache.", "").replace(".", "[.]")
            + "\\] .*")
        .asPredicate();

    ArrayList<String> result;
    try (var paths = Files.list(getCluster().getConfig().getLogDir().toPath())) {
      // keep only the files ending with .out
      result = paths.filter(file -> file.getFileName().toString().contains(".out"))
          .filter(file -> Files.isRegularFile(file) && Files.isReadable(file)).flatMap(path -> {
            try {
              return Files.lines(path, UTF_8);
            } catch (IOException e) {
              throw new RuntimeException(e);
            }
            // keep only the lines that match the audit log pattern
          }).filter(matchesAuditLogPattern)
          // only keep the message if there is no previous timestamp (first run)
          // or if the timestamp is after the lastAuditTimestamp (newer message)
          .filter(line -> lastAuditTimestamp == null
              || line.substring(0, 23).compareTo(lastAuditTimestamp) > 0)
          // sort chronologically, print for debugging, and collect
          .sorted().peek(System.out::println).collect(Collectors.toCollection(ArrayList::new));
    }

    System.out.println("End of captured audit messages for step " + stepName);
    if (!result.isEmpty()) {
      lastAuditTimestamp = result.get(result.size() - 1).substring(0, 23);
    }

    return result;
  }

  private void grantEverySystemPriv(AccumuloClient client, String user)
      throws AccumuloSecurityException, AccumuloException {
    SystemPermission[] arrayOfP = {SystemPermission.SYSTEM, SystemPermission.ALTER_TABLE,
        SystemPermission.ALTER_USER, SystemPermission.CREATE_TABLE, SystemPermission.CREATE_USER,
        SystemPermission.DROP_TABLE, SystemPermission.DROP_USER};
    for (SystemPermission p : arrayOfP) {
      client.securityOperations().grantSystemPermission(user, p);
    }
  }

  @BeforeEach
  public void resetInstance() throws Exception {
    client = Accumulo.newClient().from(getClientProperties()).build();

    removeUsersAndTables();

    // This will set the lastAuditTimestamp for the first test
    getAuditMessages("setup");
  }

  @AfterEach
  public void cleanUp() throws Exception {
    removeUsersAndTables();
    client.close();
  }

  private void removeUsersAndTables() throws Exception {
    for (String user : Arrays.asList(AUDIT_USER_1, AUDIT_USER_2)) {
      if (client.securityOperations().listLocalUsers().contains(user)) {
        client.securityOperations().dropLocalUser(user);
      }
    }

    TableOperations tops = client.tableOperations();
    for (String table : Arrays.asList(THIRD_TEST_TABLE_NAME, NEW_TEST_TABLE_NAME,
        OLD_TEST_TABLE_NAME)) {
      if (tops.exists(table)) {
        tops.delete(table);
      }
    }
  }

  @Test
  public void testTableOperationsAudits() throws AccumuloException, AccumuloSecurityException,
      TableExistsException, TableNotFoundException, InterruptedException, IOException {

    client.securityOperations().createLocalUser(AUDIT_USER_1, new PasswordToken(PASSWORD));
    client.securityOperations().grantSystemPermission(AUDIT_USER_1, SystemPermission.SYSTEM);
    client.securityOperations().grantSystemPermission(AUDIT_USER_1, SystemPermission.CREATE_TABLE);

    // Connect as Audit User and do a bunch of stuff.
    // Testing activity begins here
    auditAccumuloClient =
        getCluster().createAccumuloClient(AUDIT_USER_1, new PasswordToken(PASSWORD));
    auditAccumuloClient.tableOperations().create(OLD_TEST_TABLE_NAME);
    auditAccumuloClient.tableOperations().rename(OLD_TEST_TABLE_NAME, NEW_TEST_TABLE_NAME);
    Map<String,String> emptyMap = Collections.emptyMap();
    Set<String> emptySet = Collections.emptySet();
    auditAccumuloClient.tableOperations().clone(NEW_TEST_TABLE_NAME, OLD_TEST_TABLE_NAME, true,
        emptyMap, emptySet);
    auditAccumuloClient.tableOperations().delete(OLD_TEST_TABLE_NAME);
    auditAccumuloClient.tableOperations().offline(NEW_TEST_TABLE_NAME);
    auditAccumuloClient.tableOperations().delete(NEW_TEST_TABLE_NAME);
    // Testing activity ends here

    ArrayList<String> auditMessages = getAuditMessages("testTableOperationsAudits");

    assertEquals(1, findAuditMessage(auditMessages,
        "action: createTable; targetTable: " + OLD_TEST_TABLE_NAME));
    assertEquals(1, findAuditMessage(auditMessages,
        "action: renameTable; targetTable: " + OLD_TEST_TABLE_NAME));
    assertEquals(1,
        findAuditMessage(auditMessages, "action: cloneTable; targetTable: " + NEW_TEST_TABLE_NAME));
    assertEquals(1, findAuditMessage(auditMessages,
        "action: deleteTable; targetTable: " + OLD_TEST_TABLE_NAME));
    assertEquals(1, findAuditMessage(auditMessages,
        "action: offlineTable; targetTable: " + NEW_TEST_TABLE_NAME));
    assertEquals(1, findAuditMessage(auditMessages,
        "action: deleteTable; targetTable: " + NEW_TEST_TABLE_NAME));

  }

  @Test
  public void testUserOperationsAudits()
      throws AccumuloSecurityException, AccumuloException, TableExistsException, IOException {

    client.securityOperations().createLocalUser(AUDIT_USER_1, new PasswordToken(PASSWORD));
    client.securityOperations().grantSystemPermission(AUDIT_USER_1, SystemPermission.SYSTEM);
    client.securityOperations().grantSystemPermission(AUDIT_USER_1, SystemPermission.CREATE_USER);
    grantEverySystemPriv(client, AUDIT_USER_1);

    // Connect as Audit User and do a bunch of stuff.
    // Start testing activities here
    auditAccumuloClient =
        getCluster().createAccumuloClient(AUDIT_USER_1, new PasswordToken(PASSWORD));
    auditAccumuloClient.securityOperations().createLocalUser(AUDIT_USER_2,
        new PasswordToken(PASSWORD));

    // It seems only root can grant stuff.
    client.securityOperations().grantSystemPermission(AUDIT_USER_2, SystemPermission.ALTER_TABLE);
    client.securityOperations().revokeSystemPermission(AUDIT_USER_2, SystemPermission.ALTER_TABLE);
    auditAccumuloClient.tableOperations().create(NEW_TEST_TABLE_NAME);
    client.securityOperations().grantTablePermission(AUDIT_USER_2, NEW_TEST_TABLE_NAME,
        TablePermission.READ);
    client.securityOperations().revokeTablePermission(AUDIT_USER_2, NEW_TEST_TABLE_NAME,
        TablePermission.READ);
    auditAccumuloClient.securityOperations().changeLocalUserPassword(AUDIT_USER_2,
        new PasswordToken("anything"));
    auditAccumuloClient.securityOperations().changeUserAuthorizations(AUDIT_USER_2, auths);
    auditAccumuloClient.securityOperations().dropLocalUser(AUDIT_USER_2);
    // Stop testing activities here

    ArrayList<String> auditMessages = getAuditMessages("testUserOperationsAudits");

    // The user is allowed to create this user and it succeeded
    assertEquals(2,
        findAuditMessage(auditMessages, "action: createUser; targetUser: " + AUDIT_USER_2));
    assertEquals(1, findAuditMessage(auditMessages, "action: grantSystemPermission; permission: "
        + SystemPermission.ALTER_TABLE + "; targetUser: " + AUDIT_USER_2));
    assertEquals(1, findAuditMessage(auditMessages, "action: revokeSystemPermission; permission: "
        + SystemPermission.ALTER_TABLE + "; targetUser: " + AUDIT_USER_2));
    assertEquals(1, findAuditMessage(auditMessages, "action: grantTablePermission; permission: "
        + TablePermission.READ + "; targetTable: " + NEW_TEST_TABLE_NAME));
    assertEquals(1, findAuditMessage(auditMessages, "action: revokeTablePermission; permission: "
        + TablePermission.READ + "; targetTable: " + NEW_TEST_TABLE_NAME));
    // changePassword is allowed and succeeded
    assertEquals(2, findAuditMessage(auditMessages,
        "action: changePassword; targetUser: " + AUDIT_USER_2 + ""));
    assertEquals(1, findAuditMessage(auditMessages, "action: changeAuthorizations; targetUser: "
        + AUDIT_USER_2 + "; authorizations: " + auths));

    // allowed to dropUser and succeeded
    assertEquals(2,
        findAuditMessage(auditMessages, "action: dropUser; targetUser: " + AUDIT_USER_2));
  }

  @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "paths provided by test")
  @Test
  public void testImportExportOperationsAudits() throws AccumuloSecurityException,
      AccumuloException, TableExistsException, TableNotFoundException, IOException {

    client.securityOperations().createLocalUser(AUDIT_USER_1, new PasswordToken(PASSWORD));
    client.securityOperations().grantSystemPermission(AUDIT_USER_1, SystemPermission.SYSTEM);
    client.securityOperations().changeUserAuthorizations(AUDIT_USER_1, auths);
    grantEverySystemPriv(client, AUDIT_USER_1);

    // Connect as Audit User and do a bunch of stuff.
    // Start testing activities here
    auditAccumuloClient =
        getCluster().createAccumuloClient(AUDIT_USER_1, new PasswordToken(PASSWORD));
    auditAccumuloClient.tableOperations().create(OLD_TEST_TABLE_NAME);

    // Insert some play data
    try (BatchWriter bw = auditAccumuloClient.createBatchWriter(OLD_TEST_TABLE_NAME)) {
      Mutation m = new Mutation("myRow");
      m.put("cf1", "cq1", "v1");
      m.put("cf1", "cq2", "v3");
      bw.addMutation(m);
    }

    // Prepare to export the table
    Path confDirPath = getCluster().getConfig().getDir().toPath();
    Path exportDir = confDirPath.resolve("export");
    Path exportDirBulk = confDirPath.resolve("export_bulk");
    if (!Files.isDirectory(exportDirBulk)) {
      Files.createDirectories(exportDirBulk);
    }

    auditAccumuloClient.tableOperations().offline(OLD_TEST_TABLE_NAME, true);
    auditAccumuloClient.tableOperations().exportTable(OLD_TEST_TABLE_NAME, exportDir.toString());

    // We've exported the table metadata to the MiniAccumuloCluster root dir. Grab the .rf file path
    // to re-import it
    Path distCpTxt = exportDir.resolve("distcp.txt");
    Path importFile = null;

    // Just grab the first rf file, it will do for now.
    String filePrefix = "file:";

    try (java.util.Scanner it = new java.util.Scanner(distCpTxt, UTF_8)) {
      var pattern = Pattern.compile(".*\\.rf");
      while (it.hasNext() && importFile == null) {
        String line = it.nextLine();
        if (pattern.matcher(line).matches()) {
          importFile = Path.of(line.replaceFirst(filePrefix, ""));
        }
      }
    }
    Path fileName = importFile.getFileName();
    Files.copy(importFile, exportDir.resolve(fileName), REPLACE_EXISTING);
    Files.copy(importFile, exportDirBulk.resolve(fileName), REPLACE_EXISTING);
    auditAccumuloClient.tableOperations().importTable(NEW_TEST_TABLE_NAME,
        Collections.singleton(exportDir.toString()), ImportConfiguration.empty());

    // Now do a Directory (bulk) import of the same data.
    auditAccumuloClient.tableOperations().create(THIRD_TEST_TABLE_NAME);
    auditAccumuloClient.tableOperations().importDirectory(exportDirBulk.toString())
        .to(THIRD_TEST_TABLE_NAME).load();
    auditAccumuloClient.tableOperations().online(OLD_TEST_TABLE_NAME);

    // Stop testing activities here

    ArrayList<String> auditMessages = getAuditMessages("testImportExportOperationsAudits");

    assertEquals(1, findAuditMessage(auditMessages, String
        .format(AuditedSecurityOperation.CAN_CREATE_TABLE_AUDIT_TEMPLATE, OLD_TEST_TABLE_NAME)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            String.format(AuditedSecurityOperation.CAN_ONLINE_OFFLINE_TABLE_AUDIT_TEMPLATE,
                "offlineTable", OLD_TEST_TABLE_NAME, OLD_TEST_TABLE_NAME_ID)));
    assertEquals(1, findAuditMessage(auditMessages, String.format(
        AuditedSecurityOperation.CAN_EXPORT_AUDIT_TEMPLATE, OLD_TEST_TABLE_NAME, exportDir)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            String.format(AuditedSecurityOperation.CAN_IMPORT_AUDIT_TEMPLATE, NEW_TEST_TABLE_NAME,
                Pattern.quote(Set.of(filePrefix + exportDir).toString()))));
    assertEquals(1, findAuditMessage(auditMessages, String
        .format(AuditedSecurityOperation.CAN_CREATE_TABLE_AUDIT_TEMPLATE, THIRD_TEST_TABLE_NAME)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            String.format(AuditedSecurityOperation.CAN_BULK_IMPORT_AUDIT_TEMPLATE,
                THIRD_TEST_TABLE_NAME, filePrefix + exportDirBulk, null)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            String.format(AuditedSecurityOperation.CAN_ONLINE_OFFLINE_TABLE_AUDIT_TEMPLATE,
                "onlineTable", OLD_TEST_TABLE_NAME, OLD_TEST_TABLE_NAME_ID)));

  }

  @Test
  public void testDataOperationsAudits() throws AccumuloSecurityException, AccumuloException,
      TableExistsException, TableNotFoundException, IOException {

    client.securityOperations().createLocalUser(AUDIT_USER_1, new PasswordToken(PASSWORD));
    client.securityOperations().grantSystemPermission(AUDIT_USER_1, SystemPermission.SYSTEM);
    client.securityOperations().changeUserAuthorizations(AUDIT_USER_1, auths);
    grantEverySystemPriv(client, AUDIT_USER_1);

    // Connect as Audit User and do a bunch of stuff.
    // Start testing activities here
    auditAccumuloClient =
        getCluster().createAccumuloClient(AUDIT_USER_1, new PasswordToken(PASSWORD));
    auditAccumuloClient.tableOperations().create(OLD_TEST_TABLE_NAME);

    // Insert some play data
    try (BatchWriter bw = auditAccumuloClient.createBatchWriter(OLD_TEST_TABLE_NAME)) {
      Mutation m = new Mutation("myRow");
      m.put("cf1", "cq1", "v1");
      m.put("cf1", "cq2", "v3");
      bw.addMutation(m);
    }

    // Start testing activities here
    // A regular scan
    try (Scanner scanner = auditAccumuloClient.createScanner(OLD_TEST_TABLE_NAME, auths)) {
      for (Map.Entry<Key,Value> entry : scanner) {
        System.out.println("Scanner row: " + entry.getKey() + " " + entry.getValue());
      }
    }

    // A batch scan
    try (BatchScanner bs = auditAccumuloClient.createBatchScanner(OLD_TEST_TABLE_NAME, auths, 1)) {
      bs.fetchColumn(new Text("cf1"), new Text("cq1"));
      bs.setRanges(Arrays.asList(new Range("myRow", "myRow~")));
      for (Map.Entry<Key,Value> entry : bs) {
        System.out.println("BatchScanner row: " + entry.getKey() + " " + entry.getValue());
      }
    }

    // Delete some data.
    auditAccumuloClient.tableOperations().deleteRows(OLD_TEST_TABLE_NAME, new Text("myRow"),
        new Text("myRow~"));

    // End of testing activities

    ArrayList<String> auditMessages = getAuditMessages("testDataOperationsAudits");
    assertTrue(
        findAuditMessage(auditMessages, "action: scan; targetTable: " + OLD_TEST_TABLE_NAME) >= 1);
    assertTrue(
        findAuditMessage(auditMessages, "action: scan; targetTable: " + OLD_TEST_TABLE_NAME) >= 1);
    assertEquals(1,
        findAuditMessage(auditMessages,
            String.format(AuditedSecurityOperation.CAN_DELETE_RANGE_AUDIT_TEMPLATE,
                OLD_TEST_TABLE_NAME, "myRow", "myRow~")));

  }

  @Test
  public void testDeniedAudits() throws AccumuloSecurityException, AccumuloException,
      TableExistsException, TableNotFoundException, IOException {

    // Create our user with no privs
    client.securityOperations().createLocalUser(AUDIT_USER_1, new PasswordToken(PASSWORD));
    NewTableConfiguration ntc =
        new NewTableConfiguration().withInitialTabletAvailability(TabletAvailability.HOSTED);
    client.tableOperations().create(OLD_TEST_TABLE_NAME, ntc);
    auditAccumuloClient =
        getCluster().createAccumuloClient(AUDIT_USER_1, new PasswordToken(PASSWORD));

    // Start testing activities
    // We should get denied or / failed audit messages here.
    // We don't want the thrown exceptions to stop our tests, and we are not testing that the
    // Exceptions are thrown.

    final var tableOps = auditAccumuloClient.tableOperations();
    assertThrows(AccumuloSecurityException.class, () -> tableOps.create(NEW_TEST_TABLE_NAME));

    assertThrows(AccumuloSecurityException.class,
        () -> tableOps.rename(OLD_TEST_TABLE_NAME, NEW_TEST_TABLE_NAME));

    assertThrows(AccumuloSecurityException.class, () -> tableOps.clone(OLD_TEST_TABLE_NAME,
        NEW_TEST_TABLE_NAME, false, Collections.emptyMap(), Collections.emptySet()));

    assertThrows(AccumuloSecurityException.class, () -> tableOps.delete(OLD_TEST_TABLE_NAME));

    assertThrows(AccumuloSecurityException.class, () -> tableOps.offline(OLD_TEST_TABLE_NAME));

    try (Scanner scanner = auditAccumuloClient.createScanner(OLD_TEST_TABLE_NAME, auths)) {
      Iterator<Map.Entry<Key,Value>> iterator = scanner.iterator();
      assertThrows(RuntimeException.class, iterator::next);
    }

    assertThrows(AccumuloSecurityException.class,
        () -> tableOps.deleteRows(OLD_TEST_TABLE_NAME, new Text("myRow"), new Text("myRow~")));

    assertThrows(AccumuloSecurityException.class,
        () -> tableOps.flush(OLD_TEST_TABLE_NAME, new Text("myRow"), new Text("myRow~"), false));

    // ... that will do for now.
    // End of testing activities

    ArrayList<String> auditMessages = getAuditMessages("testDeniedAudits");
    assertEquals(1, findAuditMessage(auditMessages, "operation: denied;.*" + String
        .format(AuditedSecurityOperation.CAN_CREATE_TABLE_AUDIT_TEMPLATE, NEW_TEST_TABLE_NAME)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            "operation: denied;.*"
                + String.format(AuditedSecurityOperation.CAN_RENAME_TABLE_AUDIT_TEMPLATE,
                    OLD_TEST_TABLE_NAME, NEW_TEST_TABLE_NAME)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            "operation: denied;.*"
                + String.format(AuditedSecurityOperation.CAN_CLONE_TABLE_AUDIT_TEMPLATE,
                    OLD_TEST_TABLE_NAME, NEW_TEST_TABLE_NAME)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            "operation: denied;.*"
                + String.format(AuditedSecurityOperation.CAN_DELETE_TABLE_AUDIT_TEMPLATE,
                    OLD_TEST_TABLE_NAME, OLD_TEST_TABLE_NAME_ID)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            "operation: denied;.*"
                + String.format(AuditedSecurityOperation.CAN_ONLINE_OFFLINE_TABLE_AUDIT_TEMPLATE,
                    "offlineTable", OLD_TEST_TABLE_NAME, OLD_TEST_TABLE_NAME_ID)));
    assertEquals(1, findAuditMessage(auditMessages,
        "operation: denied;.*action: scan; targetTable: " + OLD_TEST_TABLE_NAME));
    assertEquals(1,
        findAuditMessage(auditMessages,
            "operation: denied;.*"
                + String.format(AuditedSecurityOperation.CAN_DELETE_RANGE_AUDIT_TEMPLATE,
                    OLD_TEST_TABLE_NAME, "myRow", "myRow~")));
    assertEquals(1, findAuditMessage(auditMessages, "operation: denied;.*" + String
        .format(AuditedSecurityOperation.CAN_FLUSH_TABLE_AUDIT_TEMPLATE, "1", "\\+default")));
  }

  @Test
  public void testFailedAudits() throws AccumuloException, IOException {

    // Start testing activities
    // Test that we get a few "failed" audit messages come through when we tell it to do dumb stuff
    // We don't want the thrown exceptions to stop our tests, and we are not testing that the
    // Exceptions are thrown.
    assertThrows(AccumuloSecurityException.class,
        () -> client.securityOperations().dropLocalUser(AUDIT_USER_2));

    assertThrows(AccumuloSecurityException.class, () -> client.securityOperations()
        .revokeSystemPermission(AUDIT_USER_2, SystemPermission.ALTER_TABLE));

    assertThrows(AccumuloSecurityException.class, () -> client.securityOperations()
        .createLocalUser("root", new PasswordToken("super secret")));

    ArrayList<String> auditMessages = getAuditMessages("testFailedAudits");
    // ... that will do for now.
    // End of testing activities

    // We're permitted to drop this user, but it fails because the user doesn't actually exist.
    assertEquals(2, findAuditMessage(auditMessages,
        String.format(AuditedSecurityOperation.DROP_USER_AUDIT_TEMPLATE, AUDIT_USER_2)));
    assertEquals(1,
        findAuditMessage(auditMessages,
            String.format(AuditedSecurityOperation.REVOKE_SYSTEM_PERMISSION_AUDIT_TEMPLATE,
                SystemPermission.ALTER_TABLE, AUDIT_USER_2)));
    assertEquals(1, findAuditMessage(auditMessages,
        String.format(AuditedSecurityOperation.CREATE_USER_AUDIT_TEMPLATE, "root", "")));

  }

}
