/*
 * 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.functional;

import static org.apache.accumulo.harness.AccumuloITBase.MINI_CLUSTER_ONLY;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.accumulo.cluster.ClusterUser;
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.BatchWriter;
import org.apache.accumulo.core.client.MutationsRejectedException;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.security.SecurityErrorCode;
import org.apache.accumulo.core.client.security.tokens.AuthenticationToken;
import org.apache.accumulo.core.client.security.tokens.PasswordToken;
import org.apache.accumulo.core.client.summary.Summary;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.metadata.MetadataTable;
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.harness.AccumuloClusterHarness;
import org.apache.hadoop.io.Text;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This test verifies the default permissions so a clean instance must be used. A shared instance
 * might not be representative of a fresh installation.
 */
@Tag(MINI_CLUSTER_ONLY)
public class PermissionsIT extends AccumuloClusterHarness {
  private static final Logger log = LoggerFactory.getLogger(PermissionsIT.class);

  @Override
  protected Duration defaultTimeout() {
    return Duration.ofSeconds(90);
  }

  @BeforeEach
  public void limitToMini() throws Exception {
    assumeTrue(getClusterType() == ClusterType.MINI);
    try (AccumuloClient c = Accumulo.newClient().from(getClientProps()).build()) {
      Set<String> users = c.securityOperations().listLocalUsers();
      ClusterUser user = getUser(0);
      if (users.contains(user.getPrincipal())) {
        c.securityOperations().dropLocalUser(user.getPrincipal());
      }
    }
  }

  private void loginAs(ClusterUser user) throws IOException {
    // Force a re-login as the provided user
    user.getToken();
  }

  @Test
  public void systemPermissionsTest() throws Exception {
    ClusterUser testUser = getUser(0), rootUser = getAdminUser();

    // verify that the test is being run by root
    try (AccumuloClient c = Accumulo.newClient().from(getClientProps()).build()) {
      verifyHasOnlyTheseSystemPermissions(c, c.whoami(), SystemPermission.values());

      // create the test user
      String principal = testUser.getPrincipal();
      AuthenticationToken token = testUser.getToken();
      PasswordToken passwordToken = null;
      if (token instanceof PasswordToken) {
        passwordToken = (PasswordToken) token;
      }
      loginAs(rootUser);
      c.securityOperations().createLocalUser(principal, passwordToken);
      loginAs(testUser);
      try (AccumuloClient test_user_client =
          Accumulo.newClient().from(c.properties()).as(principal, token).build()) {
        loginAs(rootUser);
        verifyHasNoSystemPermissions(c, principal, SystemPermission.values());

        // test each permission
        for (SystemPermission perm : SystemPermission.values()) {
          log.debug("Verifying the {} permission", perm);

          // test permission before and after granting it
          String tableNamePrefix = getUniqueNames(1)[0];
          Thread.sleep(1000);
          testMissingSystemPermission(tableNamePrefix, c, rootUser, test_user_client, testUser,
              perm);
          loginAs(rootUser);
          c.securityOperations().grantSystemPermission(principal, perm);
          verifyHasOnlyTheseSystemPermissions(c, principal, perm);
          testGrantedSystemPermission(tableNamePrefix, c, rootUser, test_user_client, testUser,
              perm);
          loginAs(rootUser);
          c.securityOperations().revokeSystemPermission(principal, perm);
          verifyHasNoSystemPermissions(c, principal, perm);
        }
      }
    }
  }

  static Map<String,String> map(Iterable<Entry<String,String>> i) {
    Map<String,String> result = new HashMap<>();
    for (Entry<String,String> e : i) {
      result.put(e.getKey(), e.getValue());
    }
    return result;
  }

  private void testMissingSystemPermission(String tableNamePrefix, AccumuloClient root_client,
      ClusterUser rootUser, AccumuloClient test_user_client, ClusterUser testUser,
      SystemPermission perm) throws Exception {
    String tableName, user, password = "password", namespace;
    boolean passwordBased = testUser.getPassword() != null;
    log.debug("Confirming that the lack of the {} permission properly restricts the user", perm);

    // test permission prior to granting it
    switch (perm) {
      case CREATE_TABLE:
        tableName = tableNamePrefix + "__CREATE_TABLE_WITHOUT_PERM_TEST__";
        try {
          loginAs(testUser);
          test_user_client.tableOperations().create(tableName);
          throw new IllegalStateException("Should NOT be able to create a table");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || root_client.tableOperations().list().contains(tableName)) {
            throw e;
          }
        }
        break;
      case DROP_TABLE:
        tableName = tableNamePrefix + "__DROP_TABLE_WITHOUT_PERM_TEST__";
        loginAs(rootUser);
        root_client.tableOperations().create(tableName);
        try {
          loginAs(testUser);
          test_user_client.tableOperations().delete(tableName);
          throw new IllegalStateException("Should NOT be able to delete a table");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.tableOperations().list().contains(tableName)) {
            throw e;
          }
        }
        break;
      case ALTER_TABLE:
        tableName = tableNamePrefix + "__ALTER_TABLE_WITHOUT_PERM_TEST__";
        loginAs(rootUser);
        root_client.tableOperations().create(tableName);
        try {
          loginAs(testUser);
          test_user_client.tableOperations().setProperty(tableName,
              Property.TABLE_BLOOM_ERRORRATE.getKey(), "003.14159%");
          throw new IllegalStateException("Should NOT be able to set a table property");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || root_client.tableOperations().getConfiguration(tableName)
                  .get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
            throw e;
          }
        }
        try {
          // Add test for modifyProperties
          loginAs(testUser);
          test_user_client.tableOperations().modifyProperties(tableName, properties -> {
            properties.put(Property.TABLE_BLOOM_ERRORRATE.getKey(), "003.14159%");
          });
          throw new IllegalStateException("Should NOT be able to set a table property");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || root_client.tableOperations().getConfiguration(tableName)
                  .get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
            throw e;
          }
        }
        loginAs(rootUser);
        root_client.tableOperations().setProperty(tableName,
            Property.TABLE_BLOOM_ERRORRATE.getKey(), "003.14159%");
        // add check on modify properties
        root_client.tableOperations().modifyProperties(tableName, properties -> {
          properties.put(Property.TABLE_BLOOM_SIZE.getKey(), "2048576");
        });
        try {
          loginAs(testUser);
          test_user_client.tableOperations().removeProperty(tableName,
              Property.TABLE_BLOOM_ERRORRATE.getKey());
          throw new IllegalStateException("Should NOT be able to remove a table property");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.tableOperations().getConfiguration(tableName)
                  .get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
            throw e;
          }
        }
        String table2 = tableName + "2";
        try {
          loginAs(testUser);
          test_user_client.tableOperations().rename(tableName, table2);
          throw new IllegalStateException("Should NOT be able to rename a table");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.tableOperations().list().contains(tableName)
              || root_client.tableOperations().list().contains(table2)) {
            throw e;
          }
        }
        break;
      case CREATE_USER:
        user = "__CREATE_USER_WITHOUT_PERM_TEST__";
        try {
          loginAs(testUser);
          test_user_client.securityOperations().createLocalUser(user,
              (passwordBased ? new PasswordToken(password) : null));
          throw new IllegalStateException("Should NOT be able to create a user");
        } catch (AccumuloSecurityException e) {
          AuthenticationToken userToken = testUser.getToken();
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || (userToken instanceof PasswordToken
                  && root_client.securityOperations().authenticateUser(user, userToken))) {
            throw e;
          }
        }
        break;
      case DROP_USER:
        user = "__DROP_USER_WITHOUT_PERM_TEST__";
        loginAs(rootUser);
        root_client.securityOperations().createLocalUser(user,
            (passwordBased ? new PasswordToken(password) : null));
        try {
          loginAs(testUser);
          test_user_client.securityOperations().dropLocalUser(user);
          throw new IllegalStateException("Should NOT be able to delete a user");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.securityOperations().listLocalUsers().contains(user)) {
            log.info("Failed to authenticate as {}", user);
            throw e;
          }
        }
        break;
      case ALTER_USER:
        user = "__ALTER_USER_WITHOUT_PERM_TEST__";
        loginAs(rootUser);
        root_client.securityOperations().createLocalUser(user,
            (passwordBased ? new PasswordToken(password) : null));
        try {
          loginAs(testUser);
          test_user_client.securityOperations().changeUserAuthorizations(user,
              new Authorizations("A", "B"));
          throw new IllegalStateException("Should NOT be able to alter a user");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.securityOperations().getUserAuthorizations(user).isEmpty()) {
            throw e;
          }
        }
        break;
      case SYSTEM:
        try {
          // Test setProperty
          loginAs(testUser);
          test_user_client.instanceOperations()
              .setProperty(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey(), "10000");
          throw new IllegalStateException("Should NOT be able to set System Property");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || root_client.instanceOperations().getSystemConfiguration()
                  .get(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey()).equals("10000")) {
            throw e;
          }
        }
        // Test removal of property
        loginAs(rootUser);
        root_client.instanceOperations()
            .setProperty(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey(), "10000");
        try {
          loginAs(testUser);
          test_user_client.instanceOperations()
              .removeProperty(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey());
          throw new IllegalStateException("Should NOT be able to remove System Property");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.instanceOperations().getSystemConfiguration()
                  .get(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey()).equals("10000")) {
            throw e;
          }
        }
        break;
      case CREATE_NAMESPACE:
        namespace = "__CREATE_NAMESPACE_WITHOUT_PERM_TEST__";
        try {
          loginAs(testUser);
          test_user_client.namespaceOperations().create(namespace);
          throw new IllegalStateException("Should NOT be able to create a namespace");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || root_client.namespaceOperations().list().contains(namespace)) {
            throw e;
          }
        }
        break;
      case DROP_NAMESPACE:
        namespace = "__DROP_NAMESPACE_WITHOUT_PERM_TEST__";
        loginAs(rootUser);
        root_client.namespaceOperations().create(namespace);
        try {
          loginAs(testUser);
          test_user_client.namespaceOperations().delete(namespace);
          throw new IllegalStateException("Should NOT be able to delete a namespace");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.namespaceOperations().list().contains(namespace)) {
            throw e;
          }
        }
        break;
      case ALTER_NAMESPACE:
        namespace = "__ALTER_NAMESPACE_WITHOUT_PERM_TEST__";
        loginAs(rootUser);
        root_client.namespaceOperations().create(namespace);
        try {
          loginAs(testUser);
          test_user_client.namespaceOperations().setProperty(namespace,
              Property.TABLE_BLOOM_ERRORRATE.getKey(), "003.14159%");
          throw new IllegalStateException("Should NOT be able to set a namespace property");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || root_client.namespaceOperations().getConfiguration(namespace)
                  .get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
            throw e;
          }
        }
        try {
          loginAs(testUser);
          // Verify modifyProperties also checks permissions
          test_user_client.namespaceOperations().modifyProperties(namespace, properties -> {
            properties.put(Property.TABLE_BLOOM_ERRORRATE.getKey(), "003.14159%");
          });
          throw new IllegalStateException("Should NOT be able to set a namespace property");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || root_client.namespaceOperations().getConfiguration(namespace)
                  .get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
            throw e;
          }
        }
        loginAs(rootUser);
        root_client.namespaceOperations().setProperty(namespace,
            Property.TABLE_BLOOM_ERRORRATE.getKey(), "003.14159%");
        // add check on modify properties
        root_client.namespaceOperations().modifyProperties(namespace, properties -> {
          properties.put(Property.TABLE_BLOOM_SIZE.getKey(), "2048576");
        });
        try {
          loginAs(testUser);
          test_user_client.namespaceOperations().removeProperty(namespace,
              Property.TABLE_BLOOM_ERRORRATE.getKey());
          throw new IllegalStateException("Should NOT be able to remove a namespace property");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.namespaceOperations().getConfiguration(namespace)
                  .get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
            throw e;
          }
        }
        String namespace2 = namespace + "2";
        try {
          loginAs(testUser);
          test_user_client.namespaceOperations().rename(namespace, namespace2);
          throw new IllegalStateException("Should NOT be able to rename a namespace");
        } catch (AccumuloSecurityException e) {
          loginAs(rootUser);
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED
              || !root_client.namespaceOperations().list().contains(namespace)
              || root_client.namespaceOperations().list().contains(namespace2)) {
            throw e;
          }
        }
        break;
      case OBTAIN_DELEGATION_TOKEN:
        if (saslEnabled()) {
          // not implemented
        }
        break;
      case GRANT:
        loginAs(testUser);
        try {
          test_user_client.securityOperations().grantSystemPermission(testUser.getPrincipal(),
              SystemPermission.GRANT);
          throw new IllegalStateException("Should NOT be able to grant System.GRANT to yourself");
        } catch (AccumuloSecurityException e) {
          // Expected
          loginAs(rootUser);
          assertFalse(root_client.securityOperations().hasSystemPermission(testUser.getPrincipal(),
              SystemPermission.GRANT));
        }
        break;
      default:
        throw new IllegalArgumentException("Unrecognized System Permission: " + perm);
    }
  }

  private void testGrantedSystemPermission(String tableNamePrefix, AccumuloClient root_client,
      ClusterUser rootUser, AccumuloClient test_user_client, ClusterUser testUser,
      SystemPermission perm) throws Exception {
    String tableName, user, password = "password", namespace;
    boolean passwordBased = testUser.getPassword() != null;
    log.debug("Confirming that the presence of the {} permission properly permits the user", perm);

    // test permission after granting it
    switch (perm) {
      case CREATE_TABLE:
        tableName = tableNamePrefix + "__CREATE_TABLE_WITH_PERM_TEST__";
        loginAs(testUser);
        test_user_client.tableOperations().create(tableName);
        loginAs(rootUser);
        if (!root_client.tableOperations().list().contains(tableName)) {
          throw new IllegalStateException("Should be able to create a table");
        }
        break;
      case DROP_TABLE:
        tableName = tableNamePrefix + "__DROP_TABLE_WITH_PERM_TEST__";
        loginAs(rootUser);
        root_client.tableOperations().create(tableName);
        loginAs(testUser);
        test_user_client.tableOperations().delete(tableName);
        loginAs(rootUser);
        if (root_client.tableOperations().list().contains(tableName)) {
          throw new IllegalStateException("Should be able to delete a table");
        }
        break;
      case ALTER_TABLE:
        tableName = tableNamePrefix + "__ALTER_TABLE_WITH_PERM_TEST__";
        String table2 = tableName + "2";
        loginAs(rootUser);
        root_client.tableOperations().create(tableName);
        testArbitraryProperty(root_client, tableName, true);
        loginAs(testUser);
        test_user_client.tableOperations().setProperty(tableName,
            Property.TABLE_BLOOM_ERRORRATE.getKey(), "003.14159%");
        loginAs(rootUser);
        Map<String,String> properties = root_client.tableOperations().getConfiguration(tableName);
        if (!properties.get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
          throw new IllegalStateException("Should be able to set a table property");
        }
        loginAs(testUser);
        test_user_client.tableOperations().removeProperty(tableName,
            Property.TABLE_BLOOM_ERRORRATE.getKey());
        loginAs(rootUser);
        properties = root_client.tableOperations().getConfiguration(tableName);
        if (properties.get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
          throw new IllegalStateException("Should be able to remove a table property");
        }
        loginAs(testUser);
        test_user_client.tableOperations().rename(tableName, table2);
        loginAs(rootUser);
        if (root_client.tableOperations().list().contains(tableName)
            || !root_client.tableOperations().list().contains(table2)) {
          throw new IllegalStateException("Should be able to rename a table");
        }
        break;
      case CREATE_USER:
        user = "__CREATE_USER_WITH_PERM_TEST__";
        loginAs(testUser);
        test_user_client.securityOperations().createLocalUser(user,
            (passwordBased ? new PasswordToken(password) : null));
        loginAs(rootUser);
        if (passwordBased && !root_client.securityOperations().authenticateUser(user,
            new PasswordToken(password))) {
          throw new IllegalStateException("Should be able to create a user");
        }
        break;
      case DROP_USER:
        user = "__DROP_USER_WITH_PERM_TEST__";
        loginAs(rootUser);
        root_client.securityOperations().createLocalUser(user,
            (passwordBased ? new PasswordToken(password) : null));
        loginAs(testUser);
        test_user_client.securityOperations().dropLocalUser(user);
        loginAs(rootUser);
        if (passwordBased && root_client.securityOperations().authenticateUser(user,
            new PasswordToken(password))) {
          throw new IllegalStateException("Should be able to delete a user");
        }
        break;
      case ALTER_USER:
        user = "__ALTER_USER_WITH_PERM_TEST__";
        loginAs(rootUser);
        root_client.securityOperations().createLocalUser(user,
            (passwordBased ? new PasswordToken(password) : null));
        loginAs(testUser);
        test_user_client.securityOperations().changeUserAuthorizations(user,
            new Authorizations("A", "B"));
        loginAs(rootUser);
        if (root_client.securityOperations().getUserAuthorizations(user).isEmpty()) {
          throw new IllegalStateException("Should be able to alter a user");
        }
        break;
      case SYSTEM:
        // Test setProperty
        loginAs(testUser);
        test_user_client.instanceOperations()
            .setProperty(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey(), "10000");
        loginAs(rootUser);
        if (!root_client.instanceOperations().getSystemConfiguration()
            .get(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey()).equals("10000")) {
          throw new IllegalStateException("Should be able to set system property");
        }
        // Test removal of property
        loginAs(testUser);
        test_user_client.instanceOperations()
            .removeProperty(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey());
        loginAs(rootUser);
        if (root_client.instanceOperations().getSystemConfiguration()
            .get(Property.TSERV_TOTAL_MUTATION_QUEUE_MAX.getKey()).equals("10000")) {
          throw new IllegalStateException("Should be able remove systemproperty");
        }
        break;
      case CREATE_NAMESPACE:
        namespace = "__CREATE_NAMESPACE_WITH_PERM_TEST__";
        loginAs(testUser);
        test_user_client.namespaceOperations().create(namespace);
        loginAs(rootUser);
        if (!root_client.namespaceOperations().list().contains(namespace)) {
          throw new IllegalStateException("Should be able to create a namespace");
        }
        break;
      case DROP_NAMESPACE:
        namespace = "__DROP_NAMESPACE_WITH_PERM_TEST__";
        loginAs(rootUser);
        root_client.namespaceOperations().create(namespace);
        loginAs(testUser);
        test_user_client.namespaceOperations().delete(namespace);
        loginAs(rootUser);
        if (root_client.namespaceOperations().list().contains(namespace)) {
          throw new IllegalStateException("Should be able to delete a namespace");
        }
        break;
      case ALTER_NAMESPACE:
        namespace = "__ALTER_NAMESPACE_WITH_PERM_TEST__";
        String namespace2 = namespace + "2";
        loginAs(rootUser);
        root_client.namespaceOperations().create(namespace);
        loginAs(testUser);
        test_user_client.namespaceOperations().setProperty(namespace,
            Property.TABLE_BLOOM_ERRORRATE.getKey(), "003.14159%");
        loginAs(rootUser);
        Map<String,String> propies = root_client.namespaceOperations().getConfiguration(namespace);
        if (!propies.get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
          throw new IllegalStateException("Should be able to set a table property");
        }
        loginAs(testUser);
        test_user_client.namespaceOperations().removeProperty(namespace,
            Property.TABLE_BLOOM_ERRORRATE.getKey());
        loginAs(rootUser);
        propies = root_client.namespaceOperations().getConfiguration(namespace);
        if (propies.get(Property.TABLE_BLOOM_ERRORRATE.getKey()).equals("003.14159%")) {
          throw new IllegalStateException("Should be able to remove a table property");
        }
        loginAs(testUser);
        test_user_client.namespaceOperations().rename(namespace, namespace2);
        loginAs(rootUser);
        if (root_client.namespaceOperations().list().contains(namespace)
            || !root_client.namespaceOperations().list().contains(namespace2)) {
          throw new IllegalStateException("Should be able to rename a table");
        }
        break;
      case OBTAIN_DELEGATION_TOKEN:
        if (saslEnabled()) {
          // not implemented
        }
        break;
      case GRANT:
        loginAs(rootUser);
        root_client.securityOperations().grantSystemPermission(testUser.getPrincipal(),
            SystemPermission.GRANT);
        loginAs(testUser);
        test_user_client.securityOperations().grantSystemPermission(testUser.getPrincipal(),
            SystemPermission.CREATE_TABLE);
        loginAs(rootUser);
        assertTrue(root_client.securityOperations().hasSystemPermission(testUser.getPrincipal(),
            SystemPermission.CREATE_TABLE), "Test user should have CREATE_TABLE");
        assertTrue(root_client.securityOperations().hasSystemPermission(testUser.getPrincipal(),
            SystemPermission.GRANT), "Test user should have GRANT");
        root_client.securityOperations().revokeSystemPermission(testUser.getPrincipal(),
            SystemPermission.CREATE_TABLE);
        break;
      default:
        throw new IllegalArgumentException("Unrecognized System Permission: " + perm);
    }
  }

  private void verifyHasOnlyTheseSystemPermissions(AccumuloClient root_client, String user,
      SystemPermission... perms) throws AccumuloException, AccumuloSecurityException {
    List<SystemPermission> permList = Arrays.asList(perms);
    for (SystemPermission p : SystemPermission.values()) {
      if (permList.contains(p)) {
        // should have these
        if (!root_client.securityOperations().hasSystemPermission(user, p)) {
          throw new IllegalStateException(user + " SHOULD have system permission " + p);
        }
      } else {
        // should not have these
        if (root_client.securityOperations().hasSystemPermission(user, p)) {
          throw new IllegalStateException(user + " SHOULD NOT have system permission " + p);
        }
      }
    }
  }

  private void verifyHasNoSystemPermissions(AccumuloClient root_client, String user,
      SystemPermission... perms) throws AccumuloException, AccumuloSecurityException {
    for (SystemPermission p : perms) {
      if (root_client.securityOperations().hasSystemPermission(user, p)) {
        throw new IllegalStateException(user + " SHOULD NOT have system permission " + p);
      }
    }
  }

  @Test
  public void tablePermissionTest() throws Exception {
    // create the test user
    ClusterUser testUser = getUser(0), rootUser = getAdminUser();

    String principal = testUser.getPrincipal();
    AuthenticationToken token = testUser.getToken();
    PasswordToken passwordToken = null;
    if (token instanceof PasswordToken) {
      passwordToken = (PasswordToken) token;
    }
    loginAs(rootUser);
    try (AccumuloClient c = Accumulo.newClient().from(getClientProps()).build()) {
      c.securityOperations().createLocalUser(principal, passwordToken);
      loginAs(testUser);
      try (AccumuloClient test_user_client =
          Accumulo.newClient().from(c.properties()).as(principal, token).build()) {

        // check for read-only access to metadata table
        loginAs(rootUser);
        verifyHasOnlyTheseTablePermissions(c, c.whoami(), MetadataTable.NAME, TablePermission.READ,
            TablePermission.ALTER_TABLE);
        String tableName = getUniqueNames(1)[0] + "__TABLE_PERMISSION_TEST__";

        // test each permission
        for (TablePermission perm : TablePermission.values()) {
          if (perm == TablePermission.ALTER_TABLE) {
            // we give test user client ALTER_TABLE permission when creating table
            continue;
          }
          log.debug("Verifying the {} permission", perm);

          // test permission before and after granting it
          createTestTable(c, principal, tableName);
          loginAs(testUser);
          testMissingTablePermission(test_user_client, perm, tableName);
          loginAs(rootUser);
          c.securityOperations().grantTablePermission(principal, tableName, perm);
          verifyHasOnlyTheseTablePermissions(c, principal, tableName, perm,
              TablePermission.ALTER_TABLE);
          loginAs(testUser);
          testGrantedTablePermission(test_user_client, perm, tableName);

          loginAs(rootUser);
          createTestTable(c, principal, tableName);
          c.securityOperations().revokeTablePermission(principal, tableName, perm);
          verifyHasOnlyAlterTablePermission(c, principal, tableName, perm);
        }
      }
    }
  }

  private void createTestTable(AccumuloClient c, String testUser, String tableName)
      throws Exception {
    if (!c.tableOperations().exists(tableName)) {
      // create the test table
      c.tableOperations().create(tableName);
      // put in some initial data
      try (BatchWriter writer = c.createBatchWriter(tableName)) {
        Mutation m = new Mutation(new Text("row"));
        m.put("cf", "cq", "val");
        writer.addMutation(m);
      }

      // verify proper permissions for creator and test user
      verifyHasOnlyTheseTablePermissions(c, c.whoami(), tableName, TablePermission.values());
      verifyHasOnlyAlterTablePermission(c, testUser, tableName, TablePermission.values());

    }
  }

  private void testMissingTablePermission(AccumuloClient test_user_client, TablePermission perm,
      String tableName) throws Exception {
    Mutation m;
    log.debug("Confirming that the lack of the {} permission properly restricts the user", perm);

    // test permission prior to granting it
    switch (perm) {
      case READ:
        try (Scanner scanner = test_user_client.createScanner(tableName, Authorizations.EMPTY)) {
          int i = 0;
          for (Entry<Key,Value> entry : scanner) {
            i += 1 + entry.getKey().getRowData().length();
          }
          if (i != 0) {
            throw new IllegalStateException("Should NOT be able to read from the table");
          }
        } catch (RuntimeException e) {
          AccumuloSecurityException se = (AccumuloSecurityException) e.getCause();
          if (se.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED) {
            throw se;
          }
        }
        break;
      case WRITE:
        try {
          try (BatchWriter bw = test_user_client.createBatchWriter(tableName)) {
            m = new Mutation(new Text("row"));
            m.put("a", "b", "c");
            bw.addMutation(m);
          } catch (MutationsRejectedException e1) {
            if (!e1.getSecurityErrorCodes().isEmpty()) {
              throw new AccumuloSecurityException(test_user_client.whoami(),
                  org.apache.accumulo.core.clientImpl.thrift.SecurityErrorCode.PERMISSION_DENIED,
                  e1);
            }
          }
          throw new IllegalStateException("Should NOT be able to write to a table");
        } catch (AccumuloSecurityException e) {
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED) {
            throw e;
          }
        }
        // Now see if we can flush, this should be allowed because we give the user ALTER_TABLE
        test_user_client.tableOperations().flush(tableName, new Text("myrow"), new Text("myrow~"),
            false);
        break;
      case BULK_IMPORT:
        // test for bulk import permission would go here
        break;
      case ALTER_TABLE:
        Map<String,Set<Text>> groups = new HashMap<>();
        groups.put("tgroup", Set.of(new Text("t1"), new Text("t2")));
        try {
          test_user_client.tableOperations().setLocalityGroups(tableName, groups);
          throw new IllegalStateException("User should not be able to set locality groups");
        } catch (AccumuloSecurityException e) {
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED) {
            throw e;
          }
        }
        try {
          test_user_client.tableOperations().flush(tableName, new Text("myrow"), new Text("myrow~"),
              false);
          throw new IllegalStateException("Should NOT be able to flush a table");
        } catch (AccumuloSecurityException e) {
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED) {
            throw e;
          }
        }
        testArbitraryProperty(test_user_client, tableName, false);
        break;
      case DROP_TABLE:
        try {
          test_user_client.tableOperations().delete(tableName);
          throw new IllegalStateException("User should not be able delete the table");
        } catch (AccumuloSecurityException e) {
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED) {
            throw e;
          }
        }
        break;
      case GRANT:
        try {
          test_user_client.securityOperations().grantTablePermission(getAdminPrincipal(), tableName,
              TablePermission.GRANT);
          throw new IllegalStateException("User should not be able grant permissions");
        } catch (AccumuloSecurityException e) {
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED) {
            throw e;
          }
        }
        break;
      case GET_SUMMARIES:
        try {
          test_user_client.tableOperations().summaries(tableName).retrieve();
          throw new IllegalStateException("User should not be able to get table summaries");
        } catch (AccumuloSecurityException e) {
          if (e.getSecurityErrorCode() != SecurityErrorCode.PERMISSION_DENIED) {
            throw e;
          }
        }
        break;
      default:
        throw new IllegalArgumentException("Unrecognized table Permission: " + perm);
    }
  }

  private void testGrantedTablePermission(AccumuloClient test_user_client, TablePermission perm,
      String tableName)
      throws AccumuloException, AccumuloSecurityException, TableNotFoundException {
    log.debug("Confirming that the presence of the {} permission properly permits the user", perm);

    // test permission after granting it
    switch (perm) {
      case READ:
        try (Scanner scanner = test_user_client.createScanner(tableName, Authorizations.EMPTY)) {
          for (Entry<Key,Value> keyValueEntry : scanner) {
            assertNotNull(keyValueEntry);
          }
        }
        break;
      case WRITE:
        test_user_client.tableOperations().flush(tableName, new Text("myrow"), new Text("myrow~"),
            false);
        try (BatchWriter bw = test_user_client.createBatchWriter(tableName)) {
          Mutation m = new Mutation(new Text("row"));
          m.put("a", "b", "c");
          bw.addMutation(m);
        }
        break;
      case BULK_IMPORT:
        // test for bulk import permission would go here
        break;
      case ALTER_TABLE:
        test_user_client.tableOperations().flush(tableName, new Text("myrow"), new Text("myrow~"),
            false);
        testArbitraryProperty(test_user_client, tableName, true);
        break;
      case DROP_TABLE:
        test_user_client.tableOperations().delete(tableName);
        break;
      case GRANT:
        test_user_client.securityOperations().grantTablePermission(getAdminPrincipal(), tableName,
            TablePermission.GRANT);
        break;
      case GET_SUMMARIES:
        List<Summary> summaries =
            test_user_client.tableOperations().summaries(tableName).retrieve();
        // just make sure it's not blocked by permissions, the actual summaries are tested in
        // SummaryIT
        assertTrue(summaries.isEmpty());
        break;
      default:
        throw new IllegalArgumentException("Unrecognized table Permission: " + perm);
    }
  }

  private void verifyHasOnlyTheseTablePermissions(AccumuloClient root_client, String user,
      String table, TablePermission... perms) throws AccumuloException, AccumuloSecurityException {
    List<TablePermission> permList = Arrays.asList(perms);
    for (TablePermission p : TablePermission.values()) {
      if (permList.contains(p)) {
        // should have these
        if (!root_client.securityOperations().hasTablePermission(user, table, p)) {
          throw new IllegalStateException(
              user + " SHOULD have table permission " + p + " for table " + table);
        }
      } else {
        // should not have these
        if (root_client.securityOperations().hasTablePermission(user, table, p)) {
          throw new IllegalStateException(
              user + " SHOULD NOT have table permission " + p + " for table " + table);
        }
      }
    }
  }

  private void verifyHasOnlyAlterTablePermission(AccumuloClient root_client, String user,
      String table, TablePermission... perms) throws AccumuloException, AccumuloSecurityException {
    for (TablePermission p : perms) {
      if (p == TablePermission.ALTER_TABLE) {
        if (!root_client.securityOperations().hasTablePermission(user, table, p)) {
          root_client.securityOperations().grantTablePermission(user, table, p);
        }
      } else if (root_client.securityOperations().hasTablePermission(user, table, p)) {
        throw new IllegalStateException(
            user + " SHOULD NOT have table permission " + p + " for table " + table);
      }
    }
  }

  private void testArbitraryProperty(AccumuloClient c, String tableName, boolean havePerm)
      throws AccumuloException, TableNotFoundException {
    // Set variables for the property name to use and the initial value
    String propertyName = "table.custom.description";
    String description1 = "Description";

    // Make sure the property name is valid
    assertTrue(Property.isValidPropertyKey(propertyName));
    // Set the property to the desired value
    try {
      c.tableOperations().setProperty(tableName, propertyName, description1);

      // Loop through properties to make sure the new property is added to the list
      long count = c.tableOperations().getConfiguration(tableName).entrySet().stream()
          .filter(e -> e.getKey().equals(propertyName) && e.getValue().equals(description1))
          .count();
      assertEquals(count, 1);

      // Set the property as something different
      String description2 = "set second";
      c.tableOperations().setProperty(tableName, propertyName, description2);

      // Loop through properties to make sure the new property is added to the list
      count = c.tableOperations().getConfiguration(tableName).entrySet().stream()
          .filter(e -> e.getKey().equals(propertyName) && e.getValue().equals(description2))
          .count();
      assertEquals(count, 1);

      // Remove the property and make sure there is no longer a value associated with it
      c.tableOperations().removeProperty(tableName, propertyName);

      // Loop through properties to make sure the new property is added to the list
      count = c.tableOperations().getConfiguration(tableName).entrySet().stream()
          .filter(e -> e.getKey().equals(propertyName)).count();
      assertEquals(count, 0);
      if (!havePerm) {
        throw new IllegalStateException("User should not been able to alter property.");
      }
    } catch (AccumuloSecurityException se) {
      if (havePerm) {
        throw new IllegalStateException("User should have been able to alter property");
      }
    }
  }
}
