org.apache.hadoop.hbase.security.access.AccessController.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hbase.security.access.AccessController.java

Source

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hadoop.hbase.security.access;

import java.io.IOException;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.CompoundConfiguration;
import org.apache.hadoop.hbase.CoprocessorEnvironment;
import org.apache.hadoop.hbase.DoNotRetryIOException;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HRegionInfo;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.KeyValue.Type;
import org.apache.hadoop.hbase.KeyValueUtil;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.TableNotDisabledException;
import org.apache.hadoop.hbase.TableNotFoundException;
import org.apache.hadoop.hbase.Tag;
import org.apache.hadoop.hbase.client.Append;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Durability;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Increment;
import org.apache.hadoop.hbase.client.Mutation;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Query;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver;
import org.apache.hadoop.hbase.coprocessor.CoprocessorException;
import org.apache.hadoop.hbase.coprocessor.CoprocessorService;
import org.apache.hadoop.hbase.coprocessor.EndpointObserver;
import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment;
import org.apache.hadoop.hbase.coprocessor.MasterObserver;
import org.apache.hadoop.hbase.coprocessor.ObserverContext;
import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessorEnvironment;
import org.apache.hadoop.hbase.coprocessor.RegionServerObserver;
import org.apache.hadoop.hbase.filter.ByteArrayComparable;
import org.apache.hadoop.hbase.filter.CompareFilter;
import org.apache.hadoop.hbase.filter.Filter;
import org.apache.hadoop.hbase.filter.FilterList;
import org.apache.hadoop.hbase.io.hfile.HFile;
import org.apache.hadoop.hbase.ipc.RequestContext;
import org.apache.hadoop.hbase.master.MasterServices;
import org.apache.hadoop.hbase.master.RegionPlan;
import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
import org.apache.hadoop.hbase.protobuf.ResponseConverter;
import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos;
import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.AccessControlService;
import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos.SnapshotDescription;
import org.apache.hadoop.hbase.regionserver.HRegion;
import org.apache.hadoop.hbase.regionserver.InternalScanner;
import org.apache.hadoop.hbase.regionserver.RegionScanner;
import org.apache.hadoop.hbase.regionserver.ScanType;
import org.apache.hadoop.hbase.regionserver.Store;
import org.apache.hadoop.hbase.regionserver.StoreFile;
import org.apache.hadoop.hbase.regionserver.wal.WALEdit;
import org.apache.hadoop.hbase.security.AccessDeniedException;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.UserProvider;
import org.apache.hadoop.hbase.security.access.Permission.Action;
import org.apache.hadoop.hbase.util.ByteRange;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.hadoop.hbase.util.SimpleByteRange;
import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.protobuf.Message;
import com.google.protobuf.RpcCallback;
import com.google.protobuf.RpcController;
import com.google.protobuf.Service;

/**
 * Provides basic authorization checks for data access and administrative
 * operations.
 *
 * <p>
 * {@code AccessController} performs authorization checks for HBase operations
 * based on:
 * <ul>
 *   <li>the identity of the user performing the operation</li>
 *   <li>the scope over which the operation is performed, in increasing
 *   specificity: global, table, column family, or qualifier</li>
 *   <li>the type of action being performed (as mapped to
 *   {@link Permission.Action} values)</li>
 * </ul>
 * If the authorization check fails, an {@link AccessDeniedException}
 * will be thrown for the operation.
 * </p>
 *
 * <p>
 * To perform authorization checks, {@code AccessController} relies on the
 * RpcServerEngine being loaded to provide
 * the user identities for remote requests.
 * </p>
 *
 * <p>
 * The access control lists used for authorization can be manipulated via the
 * exposed {@link AccessControlService} Interface implementation, and the associated
 * {@code grant}, {@code revoke}, and {@code user_permission} HBase shell
 * commands.
 * </p>
 */
public class AccessController extends BaseRegionObserver implements MasterObserver, RegionServerObserver,
        AccessControlService.Interface, CoprocessorService, EndpointObserver {

    public static final Log LOG = LogFactory.getLog(AccessController.class);

    private static final Log AUDITLOG = LogFactory.getLog("SecurityLogger." + AccessController.class.getName());

    TableAuthManager authManager = null;

    // flags if we are running on a region of the _acl_ table
    boolean aclRegion = false;

    // defined only for Endpoint implementation, so it can have way to
    // access region services.
    private RegionCoprocessorEnvironment regionEnv;

    /** Mapping of scanner instances to the user who created them */
    private Map<InternalScanner, String> scannerOwners = new MapMaker().weakKeys().makeMap();

    private UserProvider userProvider;

    // if we are able to support cell ACLs
    boolean cellFeaturesEnabled;

    // if we should check EXEC permissions
    boolean shouldCheckExecPermission;

    // if we should terminate access checks early as soon as table or CF grants
    // allow access; pre-0.98 compatible behavior
    boolean compatibleEarlyTermination;

    private volatile boolean initialized = false;

    public HRegion getRegion() {
        return regionEnv != null ? regionEnv.getRegion() : null;
    }

    public TableAuthManager getAuthManager() {
        return authManager;
    }

    void initialize(RegionCoprocessorEnvironment e) throws IOException {
        final HRegion region = e.getRegion();
        Configuration conf = e.getConfiguration();
        Map<byte[], ListMultimap<String, TablePermission>> tables = AccessControlLists.loadAll(region);
        // For each table, write out the table's permissions to the respective
        // znode for that table.
        for (Map.Entry<byte[], ListMultimap<String, TablePermission>> t : tables.entrySet()) {
            byte[] entry = t.getKey();
            ListMultimap<String, TablePermission> perms = t.getValue();
            byte[] serialized = AccessControlLists.writePermissionsAsBytes(perms, conf);
            this.authManager.getZKPermissionWatcher().writeToZookeeper(entry, serialized);
        }
        initialized = true;
    }

    /**
     * Writes all table ACLs for the tables in the given Map up into ZooKeeper
     * znodes.  This is called to synchronize ACL changes following {@code _acl_}
     * table updates.
     */
    void updateACL(RegionCoprocessorEnvironment e, final Map<byte[], List<Cell>> familyMap) {
        Set<byte[]> entries = new TreeSet<byte[]>(Bytes.BYTES_RAWCOMPARATOR);
        for (Map.Entry<byte[], List<Cell>> f : familyMap.entrySet()) {
            List<Cell> cells = f.getValue();
            for (Cell cell : cells) {
                KeyValue kv = KeyValueUtil.ensureKeyValue(cell);
                if (Bytes.equals(kv.getFamilyArray(), kv.getFamilyOffset(), kv.getFamilyLength(),
                        AccessControlLists.ACL_LIST_FAMILY, 0, AccessControlLists.ACL_LIST_FAMILY.length)) {
                    entries.add(kv.getRow());
                }
            }
        }
        ZKPermissionWatcher zkw = this.authManager.getZKPermissionWatcher();
        Configuration conf = regionEnv.getConfiguration();
        for (byte[] entry : entries) {
            try {
                ListMultimap<String, TablePermission> perms = AccessControlLists.getPermissions(conf, entry);
                byte[] serialized = AccessControlLists.writePermissionsAsBytes(perms, conf);
                zkw.writeToZookeeper(entry, serialized);
            } catch (IOException ex) {
                LOG.error("Failed updating permissions mirror for '" + Bytes.toString(entry) + "'", ex);
            }
        }
    }

    /**
     * Check the current user for authorization to perform a specific action
     * against the given set of row data.
     *
     * <p>Note: Ordering of the authorization checks
     * has been carefully optimized to short-circuit the most common requests
     * and minimize the amount of processing required.</p>
     *
     * @param permRequest the action being requested
     * @param e the coprocessor environment
     * @param families the map of column families to qualifiers present in
     * the request
     * @return an authorization result
     */
    AuthResult permissionGranted(String request, User user, Action permRequest, RegionCoprocessorEnvironment e,
            Map<byte[], ? extends Collection<?>> families) {
        HRegionInfo hri = e.getRegion().getRegionInfo();
        TableName tableName = hri.getTable();

        // 1. All users need read access to hbase:meta table.
        // this is a very common operation, so deal with it quickly.
        if (hri.isMetaRegion()) {
            if (permRequest == Action.READ) {
                return AuthResult.allow(request, "All users allowed", user, permRequest, tableName, families);
            }
        }

        if (user == null) {
            return AuthResult.deny(request, "No user associated with request!", null, permRequest, tableName,
                    families);
        }

        // Users with CREATE/ADMIN rights need to modify hbase:meta and _acl_ table
        // e.g. When a new table is created a new entry in hbase:meta is added,
        // so the user need to be allowed to write on it.
        // e.g. When a table is removed an entry is removed from hbase:meta and _acl_
        // and the user need to be allowed to write on both tables.
        if (permRequest == Action.WRITE
                && (hri.isMetaRegion() || Bytes.equals(tableName.getName(), AccessControlLists.ACL_GLOBAL_NAME))
                && (authManager.authorize(user, Action.CREATE) || authManager.authorize(user, Action.ADMIN))) {
            return AuthResult.allow(request, "Table permission granted", user, permRequest, tableName, families);
        }

        // 2. check for the table-level, if successful we can short-circuit
        if (authManager.authorize(user, tableName, (byte[]) null, permRequest)) {
            return AuthResult.allow(request, "Table permission granted", user, permRequest, tableName, families);
        }

        // 3. check permissions against the requested families
        if (families != null && families.size() > 0) {
            // all families must pass
            for (Map.Entry<byte[], ? extends Collection<?>> family : families.entrySet()) {
                // a) check for family level access
                if (authManager.authorize(user, tableName, family.getKey(), permRequest)) {
                    continue; // family-level permission overrides per-qualifier
                }

                // b) qualifier level access can still succeed
                if ((family.getValue() != null) && (family.getValue().size() > 0)) {
                    if (family.getValue() instanceof Set) {
                        // for each qualifier of the family
                        Set<byte[]> familySet = (Set<byte[]>) family.getValue();
                        for (byte[] qualifier : familySet) {
                            if (!authManager.authorize(user, tableName, family.getKey(), qualifier, permRequest)) {
                                return AuthResult.deny(request, "Failed qualifier check", user, permRequest,
                                        tableName, makeFamilyMap(family.getKey(), qualifier));
                            }
                        }
                    } else if (family.getValue() instanceof List) { // List<KeyValue>
                        List<KeyValue> kvList = (List<KeyValue>) family.getValue();
                        for (KeyValue kv : kvList) {
                            if (!authManager.authorize(user, tableName, family.getKey(), kv.getQualifier(),
                                    permRequest)) {
                                return AuthResult.deny(request, "Failed qualifier check", user, permRequest,
                                        tableName, makeFamilyMap(family.getKey(), kv.getQualifier()));
                            }
                        }
                    }
                } else {
                    // no qualifiers and family-level check already failed
                    return AuthResult.deny(request, "Failed family check", user, permRequest, tableName,
                            makeFamilyMap(family.getKey(), null));
                }
            }

            // all family checks passed
            return AuthResult.allow(request, "All family checks passed", user, permRequest, tableName, families);
        }

        // 4. no families to check and table level access failed
        return AuthResult.deny(request, "No families to check and table permission failed", user, permRequest,
                tableName, families);
    }

    /**
     * Check the current user for authorization to perform a specific action
     * against the given set of row data.
     * @param opType the operation type
     * @param user the user
     * @param e the coprocessor environment
     * @param families the map of column families to qualifiers present in
     * the request
     * @param actions the desired actions
     * @return an authorization result
     */
    AuthResult permissionGranted(OpType opType, User user, RegionCoprocessorEnvironment e,
            Map<byte[], ? extends Collection<?>> families, Action... actions) {
        AuthResult result = null;
        for (Action action : actions) {
            result = permissionGranted(opType.toString(), user, action, e, families);
            if (!result.isAllowed()) {
                return result;
            }
        }
        return result;
    }

    private void logResult(AuthResult result) {
        if (AUDITLOG.isTraceEnabled()) {
            RequestContext ctx = RequestContext.get();
            InetAddress remoteAddr = null;
            if (ctx != null) {
                remoteAddr = ctx.getRemoteAddress();
            }
            AUDITLOG.trace("Access " + (result.isAllowed() ? "allowed" : "denied") + " for user "
                    + (result.getUser() != null ? result.getUser().getShortName() : "UNKNOWN") + "; reason: "
                    + result.getReason() + "; remote address: " + (remoteAddr != null ? remoteAddr : "")
                    + "; request: " + result.getRequest() + "; context: " + result.toContextString());
        }
    }

    /**
     * Returns the active user to which authorization checks should be applied.
     * If we are in the context of an RPC call, the remote user is used,
     * otherwise the currently logged in user is used.
     */
    private User getActiveUser() throws IOException {
        User user = RequestContext.getRequestUser();
        if (!RequestContext.isInRequestContext()) {
            // for non-rpc handling, fallback to system user
            user = userProvider.getCurrent();
        }
        return user;
    }

    /**
     * Authorizes that the current user has any of the given permissions for the
     * given table, column family and column qualifier.
     * @param tableName Table requested
     * @param family Column family requested
     * @param qualifier Column qualifier requested
     * @throws IOException if obtaining the current user fails
     * @throws AccessDeniedException if user has no authorization
     */
    private void requirePermission(String request, TableName tableName, byte[] family, byte[] qualifier,
            Action... permissions) throws IOException {
        User user = getActiveUser();
        AuthResult result = null;

        for (Action permission : permissions) {
            if (authManager.authorize(user, tableName, family, qualifier, permission)) {
                result = AuthResult.allow(request, "Table permission granted", user, permission, tableName, family,
                        qualifier);
                break;
            } else {
                // rest of the world
                result = AuthResult.deny(request, "Insufficient permissions", user, permission, tableName, family,
                        qualifier);
            }
        }
        logResult(result);
        if (!result.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + result.toContextString());
        }
    }

    /**
     * Authorizes that the current user has global privileges for the given action.
     * @param perm The action being requested
     * @throws IOException if obtaining the current user fails
     * @throws AccessDeniedException if authorization is denied
     */
    private void requirePermission(String request, Action perm) throws IOException {
        requireGlobalPermission(request, perm, null, null);
    }

    /**
     * Authorizes that the current user has permission to perform the given
     * action on the set of table column families.
     * @param perm Action that is required
     * @param env The current coprocessor environment
     * @param families The map of column families-qualifiers.
     * @throws AccessDeniedException if the authorization check failed
     */
    private void requirePermission(String request, Action perm, RegionCoprocessorEnvironment env,
            Map<byte[], ? extends Collection<?>> families) throws IOException {
        User user = getActiveUser();
        AuthResult result = permissionGranted(request, user, perm, env, families);
        logResult(result);

        if (!result.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions (table="
                    + env.getRegion().getTableDesc().getTableName()
                    + ((families != null && families.size() > 0) ? ", family: " + result.toFamilyString() : "")
                    + ", action=" + perm.toString() + ")");
        }
    }

    /**
     * Checks that the user has the given global permission. The generated
     * audit log message will contain context information for the operation
     * being authorized, based on the given parameters.
     * @param perm Action being requested
     * @param tableName Affected table name.
     * @param familyMap Affected column families.
     */
    private void requireGlobalPermission(String request, Action perm, TableName tableName,
            Map<byte[], ? extends Collection<byte[]>> familyMap) throws IOException {
        User user = getActiveUser();
        if (authManager.authorize(user, perm)) {
            logResult(AuthResult.allow(request, "Global check allowed", user, perm, tableName, familyMap));
        } else {
            logResult(AuthResult.deny(request, "Global check failed", user, perm, tableName, familyMap));
            throw new AccessDeniedException("Insufficient permissions for user '"
                    + (user != null ? user.getShortName() : "null") + "' (global, action=" + perm.toString() + ")");
        }
    }

    /**
     * Checks that the user has the given global permission. The generated
     * audit log message will contain context information for the operation
     * being authorized, based on the given parameters.
     * @param perm Action being requested
     * @param namespace
     */
    private void requireGlobalPermission(String request, Action perm, String namespace) throws IOException {
        User user = getActiveUser();
        if (authManager.authorize(user, perm)) {
            logResult(AuthResult.allow(request, "Global check allowed", user, perm, namespace));
        } else {
            logResult(AuthResult.deny(request, "Global check failed", user, perm, namespace));
            throw new AccessDeniedException("Insufficient permissions for user '"
                    + (user != null ? user.getShortName() : "null") + "' (global, action=" + perm.toString() + ")");
        }
    }

    /**
     * Returns <code>true</code> if the current user is allowed the given action
     * over at least one of the column qualifiers in the given column families.
     */
    private boolean hasFamilyQualifierPermission(User user, Action perm, RegionCoprocessorEnvironment env,
            Map<byte[], ? extends Collection<byte[]>> familyMap) throws IOException {
        HRegionInfo hri = env.getRegion().getRegionInfo();
        TableName tableName = hri.getTable();

        if (user == null) {
            return false;
        }

        if (familyMap != null && familyMap.size() > 0) {
            // at least one family must be allowed
            for (Map.Entry<byte[], ? extends Collection<byte[]>> family : familyMap.entrySet()) {
                if (family.getValue() != null && !family.getValue().isEmpty()) {
                    for (byte[] qualifier : family.getValue()) {
                        if (authManager.matchPermission(user, tableName, family.getKey(), qualifier, perm)) {
                            return true;
                        }
                    }
                } else {
                    if (authManager.matchPermission(user, tableName, family.getKey(), perm)) {
                        return true;
                    }
                }
            }
        } else if (LOG.isDebugEnabled()) {
            LOG.debug("Empty family map passed for permission check");
        }

        return false;
    }

    private enum OpType {
        GET_CLOSEST_ROW_BEFORE("getClosestRowBefore"), GET("get"), EXISTS("exists"), SCAN("scan"), PUT(
                "put"), DELETE("delete"), CHECK_AND_PUT("checkAndPut"), CHECK_AND_DELETE(
                        "checkAndDelete"), INCREMENT_COLUMN_VALUE(
                                "incrementColumnValue"), APPEND("append"), INCREMENT("increment");

        private String type;

        private OpType(String type) {
            this.type = type;
        }

        @Override
        public String toString() {
            return type;
        }
    }

    /**
     * Determine if cell ACLs covered by the operation grant access. This is expensive.
     * @return false if cell ACLs failed to grant access, true otherwise
     * @throws IOException
     */
    private boolean checkCoveringPermission(OpType request, RegionCoprocessorEnvironment e, byte[] row,
            Map<byte[], ? extends Collection<?>> familyMap, long opTs, Action... actions) throws IOException {
        if (!cellFeaturesEnabled) {
            return false;
        }
        long cellGrants = 0;
        User user = getActiveUser();
        long latestCellTs = 0;
        Get get = new Get(row);
        // Only in case of Put/Delete op, consider TS within cell (if set for individual cells).
        // When every cell, within a Mutation, can be linked with diff TS we can not rely on only one
        // version. We have to get every cell version and check its TS against the TS asked for in
        // Mutation and skip those Cells which is outside this Mutation TS.In case of Put, we have to
        // consider only one such passing cell. In case of Delete we have to consider all the cell
        // versions under this passing version. When Delete Mutation contains columns which are a
        // version delete just consider only one version for those column cells.
        boolean considerCellTs = (request == OpType.PUT || request == OpType.DELETE);
        if (considerCellTs) {
            get.setMaxVersions();
        } else {
            get.setMaxVersions(1);
        }
        boolean diffCellTsFromOpTs = false;
        for (Map.Entry<byte[], ? extends Collection<?>> entry : familyMap.entrySet()) {
            byte[] col = entry.getKey();
            // TODO: HBASE-7114 could possibly unify the collection type in family
            // maps so we would not need to do this
            if (entry.getValue() instanceof Set) {
                Set<byte[]> set = (Set<byte[]>) entry.getValue();
                if (set == null || set.isEmpty()) {
                    get.addFamily(col);
                } else {
                    for (byte[] qual : set) {
                        get.addColumn(col, qual);
                    }
                }
            } else if (entry.getValue() instanceof List) {
                List<Cell> list = (List<Cell>) entry.getValue();
                if (list == null || list.isEmpty()) {
                    get.addFamily(col);
                } else {
                    // In case of family delete, a Cell will be added into the list with Qualifier as null.
                    for (Cell cell : list) {
                        if (cell.getQualifierLength() == 0 && (cell.getTypeByte() == Type.DeleteFamily.getCode()
                                || cell.getTypeByte() == Type.DeleteFamilyVersion.getCode())) {
                            get.addFamily(col);
                        } else {
                            get.addColumn(col, CellUtil.cloneQualifier(cell));
                        }
                        if (considerCellTs) {
                            long cellTs = cell.getTimestamp();
                            latestCellTs = Math.max(latestCellTs, cellTs);
                            diffCellTsFromOpTs = diffCellTsFromOpTs || (opTs != cellTs);
                        }
                    }
                }
            } else {
                throw new RuntimeException("Unhandled collection type " + entry.getValue().getClass().getName());
            }
        }
        // We want to avoid looking into the future. So, if the cells of the
        // operation specify a timestamp, or the operation itself specifies a
        // timestamp, then we use the maximum ts found. Otherwise, we bound
        // the Get to the current server time. We add 1 to the timerange since
        // the upper bound of a timerange is exclusive yet we need to examine
        // any cells found there inclusively.
        long latestTs = Math.max(opTs, latestCellTs);
        if (latestTs == 0 || latestTs == HConstants.LATEST_TIMESTAMP) {
            latestTs = EnvironmentEdgeManager.currentTimeMillis();
        }
        get.setTimeRange(0, latestTs + 1);
        // In case of Put operation we set to read all versions. This was done to consider the case
        // where columns are added with TS other than the Mutation TS. But normally this wont be the
        // case with Put. There no need to get all versions but get latest version only.
        if (!diffCellTsFromOpTs && request == OpType.PUT) {
            get.setMaxVersions(1);
        }
        if (LOG.isTraceEnabled()) {
            LOG.trace("Scanning for cells with " + get);
        }
        // This Map is identical to familyMap. The key is a BR rather than byte[].
        // It will be easy to do gets over this new Map as we can create get keys over the Cell cf by
        // new SimpleByteRange(cell.familyArray, cell.familyOffset, cell.familyLen)
        Map<ByteRange, List<Cell>> familyMap1 = new HashMap<ByteRange, List<Cell>>();
        for (Entry<byte[], ? extends Collection<?>> entry : familyMap.entrySet()) {
            if (entry.getValue() instanceof List) {
                familyMap1.put(new SimpleByteRange(entry.getKey()), (List<Cell>) entry.getValue());
            }
        }
        RegionScanner scanner = getRegion(e).getScanner(new Scan(get));
        List<Cell> cells = Lists.newArrayList();
        Cell prevCell = null;
        ByteRange curFam = new SimpleByteRange();
        boolean curColAllVersions = (request == OpType.DELETE);
        long curColCheckTs = opTs;
        boolean foundColumn = false;
        try {
            boolean more = false;
            do {
                cells.clear();
                // scan with limit as 1 to hold down memory use on wide rows
                more = scanner.next(cells, 1);
                for (Cell cell : cells) {
                    if (LOG.isTraceEnabled()) {
                        LOG.trace("Found cell " + cell);
                    }
                    boolean colChange = prevCell == null || !CellUtil.matchingColumn(prevCell, cell);
                    if (colChange)
                        foundColumn = false;
                    prevCell = cell;
                    if (!curColAllVersions && foundColumn) {
                        continue;
                    }
                    if (colChange && considerCellTs) {
                        curFam.set(cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength());
                        List<Cell> cols = familyMap1.get(curFam);
                        for (Cell col : cols) {
                            // null/empty qualifier is used to denote a Family delete. The TS and delete type
                            // associated with this is applicable for all columns within the family. That is
                            // why the below (col.getQualifierLength() == 0) check.
                            if ((col.getQualifierLength() == 0 && request == OpType.DELETE)
                                    || CellUtil.matchingQualifier(cell, col)) {
                                byte type = col.getTypeByte();
                                if (considerCellTs) {
                                    curColCheckTs = col.getTimestamp();
                                }
                                // For a Delete op we pass allVersions as true. When a Delete Mutation contains
                                // a version delete for a column no need to check all the covering cells within
                                // that column. Check all versions when Type is DeleteColumn or DeleteFamily
                                // One version delete types are Delete/DeleteFamilyVersion
                                curColAllVersions = (KeyValue.Type.DeleteColumn.getCode() == type)
                                        || (KeyValue.Type.DeleteFamily.getCode() == type);
                                break;
                            }
                        }
                    }
                    if (cell.getTimestamp() > curColCheckTs) {
                        // Just ignore this cell. This is not a covering cell.
                        continue;
                    }
                    foundColumn = true;
                    for (Action action : actions) {
                        // Are there permissions for this user for the cell?
                        if (!authManager.authorize(user, getTableName(e), cell, action)) {
                            // We can stop if the cell ACL denies access
                            return false;
                        }
                    }
                    cellGrants++;
                }
            } while (more);
        } catch (AccessDeniedException ex) {
            throw ex;
        } catch (IOException ex) {
            LOG.error("Exception while getting cells to calculate covering permission", ex);
        } finally {
            scanner.close();
        }
        // We should not authorize unless we have found one or more cell ACLs that
        // grant access. This code is used to check for additional permissions
        // after no table or CF grants are found.
        return cellGrants > 0;
    }

    private void addCellPermissions(final byte[] perms, Map<byte[], List<Cell>> familyMap) {
        // Iterate over the entries in the familyMap, replacing the cells therein
        // with new cells including the ACL data
        for (Map.Entry<byte[], List<Cell>> e : familyMap.entrySet()) {
            List<Cell> newCells = Lists.newArrayList();
            for (Cell cell : e.getValue()) {
                List<Tag> tags = Lists.newArrayList(new Tag(AccessControlLists.ACL_TAG_TYPE, perms));
                byte[] tagBytes = CellUtil.getTagArray(cell);
                Iterator<Tag> tagIterator = CellUtil.tagsIterator(tagBytes, 0, tagBytes.length);
                while (tagIterator.hasNext()) {
                    tags.add(tagIterator.next());
                }
                // Ensure KeyValue so we can do a scatter gather copy. This is only a win if the
                // incoming cell type is actually KeyValue.
                KeyValue kv = KeyValueUtil.ensureKeyValue(cell);
                newCells.add(new KeyValue(kv.getRowArray(), kv.getRowOffset(), kv.getRowLength(),
                        kv.getFamilyArray(), kv.getFamilyOffset(), kv.getFamilyLength(), kv.getQualifierArray(),
                        kv.getQualifierOffset(), kv.getQualifierLength(), kv.getTimestamp(),
                        KeyValue.Type.codeToType(kv.getTypeByte()), kv.getValueArray(), kv.getValueOffset(),
                        kv.getValueLength(), tags));
            }
            // This is supposed to be safe, won't CME
            e.setValue(newCells);
        }
    }

    /* ---- MasterObserver implementation ---- */

    public void start(CoprocessorEnvironment env) throws IOException {
        CompoundConfiguration conf = new CompoundConfiguration();
        conf.add(env.getConfiguration());

        shouldCheckExecPermission = conf.getBoolean(AccessControlConstants.EXEC_PERMISSION_CHECKS_KEY,
                AccessControlConstants.DEFAULT_EXEC_PERMISSION_CHECKS);

        cellFeaturesEnabled = HFile.getFormatVersion(conf) >= HFile.MIN_FORMAT_VERSION_WITH_TAGS;
        if (!cellFeaturesEnabled) {
            LOG.info("A minimum HFile version of " + HFile.MIN_FORMAT_VERSION_WITH_TAGS
                    + " is required to persist cell ACLs. Consider setting " + HFile.FORMAT_VERSION_KEY
                    + " accordingly.");
        }

        ZooKeeperWatcher zk = null;
        if (env instanceof MasterCoprocessorEnvironment) {
            // if running on HMaster
            MasterCoprocessorEnvironment mEnv = (MasterCoprocessorEnvironment) env;
            zk = mEnv.getMasterServices().getZooKeeper();
        } else if (env instanceof RegionServerCoprocessorEnvironment) {
            RegionServerCoprocessorEnvironment rsEnv = (RegionServerCoprocessorEnvironment) env;
            zk = rsEnv.getRegionServerServices().getZooKeeper();
        } else if (env instanceof RegionCoprocessorEnvironment) {
            // if running at region
            regionEnv = (RegionCoprocessorEnvironment) env;
            conf.addStringMap(regionEnv.getRegion().getTableDesc().getConfiguration());
            zk = regionEnv.getRegionServerServices().getZooKeeper();
            compatibleEarlyTermination = conf.getBoolean(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT,
                    AccessControlConstants.DEFAULT_ATTRIBUTE_EARLY_OUT);
        }

        // set the user-provider.
        this.userProvider = UserProvider.instantiate(env.getConfiguration());

        // If zk is null or IOException while obtaining auth manager,
        // throw RuntimeException so that the coprocessor is unloaded.
        if (zk != null) {
            try {
                this.authManager = TableAuthManager.get(zk, env.getConfiguration());
            } catch (IOException ioe) {
                throw new RuntimeException("Error obtaining TableAuthManager", ioe);
            }
        } else {
            throw new RuntimeException("Error obtaining TableAuthManager, zk found null.");
        }
    }

    public void stop(CoprocessorEnvironment env) {

    }

    @Override
    public void preCreateTable(ObserverContext<MasterCoprocessorEnvironment> c, HTableDescriptor desc,
            HRegionInfo[] regions) throws IOException {
        Set<byte[]> families = desc.getFamiliesKeys();
        Map<byte[], Set<byte[]>> familyMap = new TreeMap<byte[], Set<byte[]>>(Bytes.BYTES_COMPARATOR);
        for (byte[] family : families) {
            familyMap.put(family, null);
        }
        requireGlobalPermission("createTable", Action.CREATE, desc.getTableName(), familyMap);
    }

    @Override
    public void preCreateTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, HTableDescriptor desc,
            HRegionInfo[] regions) throws IOException {
    }

    @Override
    public void postCreateTable(ObserverContext<MasterCoprocessorEnvironment> c, HTableDescriptor desc,
            HRegionInfo[] regions) throws IOException {
        if (!AccessControlLists.isAclTable(desc)) {
            String owner = desc.getOwnerString();
            // default the table owner to current user, if not specified.
            if (owner == null)
                owner = getActiveUser().getShortName();
            UserPermission userperm = new UserPermission(Bytes.toBytes(owner), desc.getTableName(), null,
                    Action.values());
            AccessControlLists.addUserPermission(c.getEnvironment().getConfiguration(), userperm);
        }
    }

    @Override
    public void postCreateTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, HTableDescriptor desc,
            HRegionInfo[] regions) throws IOException {
    }

    @Override
    public void preDeleteTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
        requirePermission("deleteTable", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void preDeleteTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void postDeleteTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
        AccessControlLists.removeTablePermissions(c.getEnvironment().getConfiguration(), tableName);
    }

    @Override
    public void postDeleteTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void preTruncateTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
        requirePermission("truncateTable", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void postTruncateTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void preTruncateTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void postTruncateTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void preModifyTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HTableDescriptor htd) throws IOException {
        requirePermission("modifyTable", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void preModifyTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HTableDescriptor htd) throws IOException {
    }

    @Override
    public void postModifyTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HTableDescriptor htd) throws IOException {
        String owner = htd.getOwnerString();
        // default the table owner to current user, if not specified.
        if (owner == null)
            owner = getActiveUser().getShortName();
        UserPermission userperm = new UserPermission(Bytes.toBytes(owner), htd.getTableName(), null,
                Action.values());
        AccessControlLists.addUserPermission(c.getEnvironment().getConfiguration(), userperm);
    }

    @Override
    public void postModifyTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HTableDescriptor htd) throws IOException {
    }

    @Override
    public void preAddColumn(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HColumnDescriptor column) throws IOException {
        requirePermission("addColumn", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void preAddColumnHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HColumnDescriptor column) throws IOException {
    }

    @Override
    public void postAddColumn(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HColumnDescriptor column) throws IOException {
    }

    @Override
    public void postAddColumnHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HColumnDescriptor column) throws IOException {
    }

    @Override
    public void preModifyColumn(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HColumnDescriptor descriptor) throws IOException {
        requirePermission("modifyColumn", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void preModifyColumnHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HColumnDescriptor descriptor) throws IOException {
    }

    @Override
    public void postModifyColumn(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HColumnDescriptor descriptor) throws IOException {
    }

    @Override
    public void postModifyColumnHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            HColumnDescriptor descriptor) throws IOException {
    }

    @Override
    public void preDeleteColumn(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName, byte[] col)
            throws IOException {
        requirePermission("deleteColumn", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void preDeleteColumnHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            byte[] col) throws IOException {
    }

    @Override
    public void postDeleteColumn(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName, byte[] col)
            throws IOException {
        AccessControlLists.removeTablePermissions(c.getEnvironment().getConfiguration(), tableName, col);
    }

    @Override
    public void postDeleteColumnHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName,
            byte[] col) throws IOException {
    }

    @Override
    public void preEnableTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
        requirePermission("enableTable", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void preEnableTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void postEnableTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void postEnableTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void preDisableTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
        if (Bytes.equals(tableName.getName(), AccessControlLists.ACL_GLOBAL_NAME)) {
            throw new AccessDeniedException(
                    "Not allowed to disable " + AccessControlLists.ACL_TABLE_NAME + " table.");
        }
        requirePermission("disableTable", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void preDisableTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void postDisableTable(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void postDisableTableHandler(ObserverContext<MasterCoprocessorEnvironment> c, TableName tableName)
            throws IOException {
    }

    @Override
    public void preMove(ObserverContext<MasterCoprocessorEnvironment> c, HRegionInfo region, ServerName srcServer,
            ServerName destServer) throws IOException {
        requirePermission("move", region.getTable(), null, null, Action.ADMIN);
    }

    @Override
    public void postMove(ObserverContext<MasterCoprocessorEnvironment> c, HRegionInfo region, ServerName srcServer,
            ServerName destServer) throws IOException {
    }

    @Override
    public void preAssign(ObserverContext<MasterCoprocessorEnvironment> c, HRegionInfo regionInfo)
            throws IOException {
        requirePermission("assign", regionInfo.getTable(), null, null, Action.ADMIN);
    }

    @Override
    public void postAssign(ObserverContext<MasterCoprocessorEnvironment> c, HRegionInfo regionInfo)
            throws IOException {
    }

    @Override
    public void preUnassign(ObserverContext<MasterCoprocessorEnvironment> c, HRegionInfo regionInfo, boolean force)
            throws IOException {
        requirePermission("unassign", regionInfo.getTable(), null, null, Action.ADMIN);
    }

    @Override
    public void postUnassign(ObserverContext<MasterCoprocessorEnvironment> c, HRegionInfo regionInfo, boolean force)
            throws IOException {
    }

    @Override
    public void preRegionOffline(ObserverContext<MasterCoprocessorEnvironment> c, HRegionInfo regionInfo)
            throws IOException {
        requirePermission("regionOffline", regionInfo.getTable(), null, null, Action.ADMIN);
    }

    @Override
    public void postRegionOffline(ObserverContext<MasterCoprocessorEnvironment> c, HRegionInfo regionInfo)
            throws IOException {
    }

    @Override
    public void preBalance(ObserverContext<MasterCoprocessorEnvironment> c) throws IOException {
        requirePermission("balance", Action.ADMIN);
    }

    @Override
    public void postBalance(ObserverContext<MasterCoprocessorEnvironment> c, List<RegionPlan> plans)
            throws IOException {
    }

    @Override
    public boolean preBalanceSwitch(ObserverContext<MasterCoprocessorEnvironment> c, boolean newValue)
            throws IOException {
        requirePermission("balanceSwitch", Action.ADMIN);
        return newValue;
    }

    @Override
    public void postBalanceSwitch(ObserverContext<MasterCoprocessorEnvironment> c, boolean oldValue,
            boolean newValue) throws IOException {
    }

    @Override
    public void preShutdown(ObserverContext<MasterCoprocessorEnvironment> c) throws IOException {
        requirePermission("shutdown", Action.ADMIN);
    }

    @Override
    public void preStopMaster(ObserverContext<MasterCoprocessorEnvironment> c) throws IOException {
        requirePermission("stopMaster", Action.ADMIN);
    }

    @Override
    public void postStartMaster(ObserverContext<MasterCoprocessorEnvironment> ctx) throws IOException {
        // initialize the ACL storage table
        AccessControlLists.init(ctx.getEnvironment().getMasterServices());
    }

    @Override
    public void preMasterInitialization(ObserverContext<MasterCoprocessorEnvironment> ctx) throws IOException {
    }

    @Override
    public void preSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
            final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) throws IOException {
        requirePermission("snapshot", Action.ADMIN);
    }

    @Override
    public void postSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
            final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) throws IOException {
    }

    @Override
    public void preCloneSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
            final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) throws IOException {
        requirePermission("clone", Action.ADMIN);
    }

    @Override
    public void postCloneSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
            final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) throws IOException {
    }

    @Override
    public void preRestoreSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
            final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) throws IOException {
        requirePermission("restore", Action.ADMIN);
    }

    @Override
    public void postRestoreSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
            final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) throws IOException {
    }

    @Override
    public void preDeleteSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
            final SnapshotDescription snapshot) throws IOException {
        requirePermission("deleteSnapshot", Action.ADMIN);
    }

    @Override
    public void postDeleteSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
            final SnapshotDescription snapshot) throws IOException {
    }

    @Override
    public void preCreateNamespace(ObserverContext<MasterCoprocessorEnvironment> ctx, NamespaceDescriptor ns)
            throws IOException {
        requireGlobalPermission("createNamespace", Action.ADMIN, ns.getName());
    }

    @Override
    public void postCreateNamespace(ObserverContext<MasterCoprocessorEnvironment> ctx, NamespaceDescriptor ns)
            throws IOException {
    }

    @Override
    public void preDeleteNamespace(ObserverContext<MasterCoprocessorEnvironment> ctx, String namespace)
            throws IOException {
        requireGlobalPermission("deleteNamespace", Action.ADMIN, namespace);
    }

    @Override
    public void postDeleteNamespace(ObserverContext<MasterCoprocessorEnvironment> ctx, String namespace)
            throws IOException {
        AccessControlLists.removeNamespacePermissions(ctx.getEnvironment().getConfiguration(), namespace);
        LOG.info(namespace + "entry deleted in " + AccessControlLists.ACL_TABLE_NAME + " table.");
    }

    @Override
    public void preModifyNamespace(ObserverContext<MasterCoprocessorEnvironment> ctx, NamespaceDescriptor ns)
            throws IOException {
        requireGlobalPermission("modifyNamespace", Action.ADMIN, ns.getName());
    }

    @Override
    public void postModifyNamespace(ObserverContext<MasterCoprocessorEnvironment> ctx, NamespaceDescriptor ns)
            throws IOException {
    }

    @Override
    public void preTableFlush(final ObserverContext<MasterCoprocessorEnvironment> ctx, final TableName tableName)
            throws IOException {
        requirePermission("flushTable", tableName, null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void postTableFlush(final ObserverContext<MasterCoprocessorEnvironment> ctx, final TableName tableName)
            throws IOException {
    }

    /* ---- RegionObserver implementation ---- */

    @Override
    public void preOpen(ObserverContext<RegionCoprocessorEnvironment> e) throws IOException {
        RegionCoprocessorEnvironment env = e.getEnvironment();
        final HRegion region = env.getRegion();
        if (region == null) {
            LOG.error("NULL region from RegionCoprocessorEnvironment in preOpen()");
        } else {
            HRegionInfo regionInfo = region.getRegionInfo();
            if (regionInfo.getTable().isSystemTable()) {
                isSystemOrSuperUser(regionEnv.getConfiguration());
            } else {
                requirePermission("preOpen", Action.ADMIN);
            }
        }
    }

    @Override
    public void postOpen(ObserverContext<RegionCoprocessorEnvironment> c) {
        RegionCoprocessorEnvironment env = c.getEnvironment();
        final HRegion region = env.getRegion();
        if (region == null) {
            LOG.error("NULL region from RegionCoprocessorEnvironment in postOpen()");
            return;
        }
        if (AccessControlLists.isAclRegion(region)) {
            aclRegion = true;
            // When this region is under recovering state, initialize will be handled by postLogReplay
            if (!region.isRecovering()) {
                try {
                    initialize(env);
                } catch (IOException ex) {
                    // if we can't obtain permissions, it's better to fail
                    // than perform checks incorrectly
                    throw new RuntimeException("Failed to initialize permissions cache", ex);
                }
            }
        } else {
            initialized = true;
        }
    }

    @Override
    public void postLogReplay(ObserverContext<RegionCoprocessorEnvironment> c) {
        if (aclRegion) {
            try {
                initialize(c.getEnvironment());
            } catch (IOException ex) {
                // if we can't obtain permissions, it's better to fail
                // than perform checks incorrectly
                throw new RuntimeException("Failed to initialize permissions cache", ex);
            }
        }
    }

    @Override
    public void preFlush(ObserverContext<RegionCoprocessorEnvironment> e) throws IOException {
        requirePermission("flush", getTableName(e.getEnvironment()), null, null, Action.ADMIN, Action.CREATE);
    }

    @Override
    public void preSplit(ObserverContext<RegionCoprocessorEnvironment> e) throws IOException {
        requirePermission("split", getTableName(e.getEnvironment()), null, null, Action.ADMIN);
    }

    @Override
    public void preSplit(ObserverContext<RegionCoprocessorEnvironment> e, byte[] splitRow) throws IOException {
        requirePermission("split", getTableName(e.getEnvironment()), null, null, Action.ADMIN);
    }

    @Override
    public InternalScanner preCompact(ObserverContext<RegionCoprocessorEnvironment> e, final Store store,
            final InternalScanner scanner, final ScanType scanType) throws IOException {
        requirePermission("compact", getTableName(e.getEnvironment()), null, null, Action.ADMIN, Action.CREATE);
        return scanner;
    }

    @Override
    public void preCompactSelection(final ObserverContext<RegionCoprocessorEnvironment> e, final Store store,
            final List<StoreFile> candidates) throws IOException {
        requirePermission("compact", getTableName(e.getEnvironment()), null, null, Action.ADMIN);
    }

    @Override
    public void preGetClosestRowBefore(final ObserverContext<RegionCoprocessorEnvironment> c, final byte[] row,
            final byte[] family, final Result result) throws IOException {
        assert family != null;
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<byte[]>> families = makeFamilyMap(family, null);
        User user = getActiveUser();
        AuthResult authResult = permissionGranted(OpType.GET_CLOSEST_ROW_BEFORE, user, env, families, Action.READ);
        if (!authResult.isAllowed() && cellFeaturesEnabled && !compatibleEarlyTermination) {
            authResult.setAllowed(checkCoveringPermission(OpType.GET_CLOSEST_ROW_BEFORE, env, row, families,
                    HConstants.LATEST_TIMESTAMP, Action.READ));
            authResult.setReason("Covering cell set");
        }
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
        }
    }

    private void internalPreRead(final ObserverContext<RegionCoprocessorEnvironment> c, final Query query,
            OpType opType) throws IOException {
        Filter filter = query.getFilter();
        // Don't wrap an AccessControlFilter
        if (filter != null && filter instanceof AccessControlFilter) {
            return;
        }
        User user = getActiveUser();
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<byte[]>> families = null;
        switch (opType) {
        case GET:
        case EXISTS:
            families = ((Get) query).getFamilyMap();
            break;
        case SCAN:
            families = ((Scan) query).getFamilyMap();
            break;
        default:
            throw new RuntimeException("Unhandled operation " + opType);
        }
        AuthResult authResult = permissionGranted(opType, user, env, families, Action.READ);
        HRegion region = getRegion(env);
        TableName table = getTableName(region);
        Map<ByteRange, Integer> cfVsMaxVersions = Maps.newHashMap();
        for (HColumnDescriptor hcd : region.getTableDesc().getFamilies()) {
            cfVsMaxVersions.put(new SimpleByteRange(hcd.getName()), hcd.getMaxVersions());
        }
        if (!authResult.isAllowed()) {
            if (!cellFeaturesEnabled || compatibleEarlyTermination) {
                // Old behavior: Scan with only qualifier checks if we have partial
                // permission. Backwards compatible behavior is to throw an
                // AccessDeniedException immediately if there are no grants for table
                // or CF or CF+qual. Only proceed with an injected filter if there are
                // grants for qualifiers. Otherwise we will fall through below and log
                // the result and throw an ADE. We may end up checking qualifier
                // grants three times (permissionGranted above, here, and in the
                // filter) but that's the price of backwards compatibility. 
                if (hasFamilyQualifierPermission(user, Action.READ, env, families)) {
                    Filter ourFilter = new AccessControlFilter(authManager, user, table,
                            query.getACLStrategy() ? AccessControlFilter.Strategy.CHECK_CELL_FIRST
                                    : AccessControlFilter.Strategy.CHECK_TABLE_AND_CF_ONLY,
                            cfVsMaxVersions);
                    // wrap any existing filter
                    if (filter != null) {
                        ourFilter = new FilterList(FilterList.Operator.MUST_PASS_ALL,
                                Lists.newArrayList(ourFilter, filter));
                    }
                    authResult.setAllowed(true);
                    ;
                    authResult.setReason("Access allowed with filter");
                    switch (opType) {
                    case GET:
                    case EXISTS:
                        ((Get) query).setFilter(ourFilter);
                        break;
                    case SCAN:
                        ((Scan) query).setFilter(ourFilter);
                        break;
                    default:
                        throw new RuntimeException("Unhandled operation " + opType);
                    }
                }
            } else {
                // New behavior: Any access we might be granted is more fine-grained
                // than whole table or CF. Simply inject a filter and return what is
                // allowed. We will not throw an AccessDeniedException. This is a
                // behavioral change since 0.96. 
                Filter ourFilter = new AccessControlFilter(authManager, user, table,
                        query.getACLStrategy() ? AccessControlFilter.Strategy.CHECK_CELL_FIRST
                                : AccessControlFilter.Strategy.CHECK_CELL_DEFAULT,
                        cfVsMaxVersions);
                // wrap any existing filter
                if (filter != null) {
                    ourFilter = new FilterList(FilterList.Operator.MUST_PASS_ALL,
                            Lists.newArrayList(ourFilter, filter));
                }
                authResult.setAllowed(true);
                ;
                authResult.setReason("Access allowed with filter");
                switch (opType) {
                case GET:
                case EXISTS:
                    ((Get) query).setFilter(ourFilter);
                    break;
                case SCAN:
                    ((Scan) query).setFilter(ourFilter);
                    break;
                default:
                    throw new RuntimeException("Unhandled operation " + opType);
                }
            }
        }

        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions (table=" + table + ", action=READ)");
        }
    }

    @Override
    public void preGetOp(final ObserverContext<RegionCoprocessorEnvironment> c, final Get get,
            final List<Cell> result) throws IOException {
        internalPreRead(c, get, OpType.GET);
    }

    @Override
    public boolean preExists(final ObserverContext<RegionCoprocessorEnvironment> c, final Get get,
            final boolean exists) throws IOException {
        internalPreRead(c, get, OpType.EXISTS);
        return exists;
    }

    @Override
    public void prePut(final ObserverContext<RegionCoprocessorEnvironment> c, final Put put, final WALEdit edit,
            final Durability durability) throws IOException {
        // Require WRITE permission to the table, CF, or top visible value, if any.
        // NOTE: We don't need to check the permissions for any earlier Puts
        // because we treat the ACLs in each Put as timestamped like any other
        // HBase value. A new ACL in a new Put applies to that Put. It doesn't
        // change the ACL of any previous Put. This allows simple evolution of
        // security policy over time without requiring expensive updates.
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<Cell>> families = put.getFamilyCellMap();
        User user = getActiveUser();
        AuthResult authResult = permissionGranted(OpType.PUT, user, env, families, Action.WRITE);
        if (!authResult.isAllowed() && cellFeaturesEnabled && !compatibleEarlyTermination) {
            authResult.setAllowed(checkCoveringPermission(OpType.PUT, env, put.getRow(), families,
                    put.getTimeStamp(), Action.WRITE));
            authResult.setReason("Covering cell set");
        }
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
        }
        byte[] bytes = put.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL);
        if (bytes != null) {
            if (cellFeaturesEnabled) {
                addCellPermissions(bytes, put.getFamilyCellMap());
            } else {
                throw new DoNotRetryIOException("Cell ACLs cannot be persisted");
            }
        }
    }

    @Override
    public void postPut(final ObserverContext<RegionCoprocessorEnvironment> c, final Put put, final WALEdit edit,
            final Durability durability) {
        if (aclRegion) {
            updateACL(c.getEnvironment(), put.getFamilyCellMap());
        }
    }

    @Override
    public void preDelete(final ObserverContext<RegionCoprocessorEnvironment> c, final Delete delete,
            final WALEdit edit, final Durability durability) throws IOException {
        // An ACL on a delete is useless, we shouldn't allow it
        if (delete.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL) != null) {
            throw new DoNotRetryIOException("ACL on delete has no effect: " + delete.toString());
        }
        // Require WRITE permissions on all cells covered by the delete. Unlike
        // for Puts we need to check all visible prior versions, because a major
        // compaction could remove them. If the user doesn't have permission to
        // overwrite any of the visible versions ('visible' defined as not covered
        // by a tombstone already) then we have to disallow this operation.
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<Cell>> families = delete.getFamilyCellMap();
        User user = getActiveUser();
        AuthResult authResult = permissionGranted(OpType.DELETE, user, env, families, Action.WRITE);
        if (!authResult.isAllowed() && cellFeaturesEnabled && !compatibleEarlyTermination) {
            authResult.setAllowed(checkCoveringPermission(OpType.DELETE, env, delete.getRow(), families,
                    delete.getTimeStamp(), Action.WRITE));
            authResult.setReason("Covering cell set");
        }
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
        }
    }

    @Override
    public void postDelete(final ObserverContext<RegionCoprocessorEnvironment> c, final Delete delete,
            final WALEdit edit, final Durability durability) throws IOException {
        if (aclRegion) {
            updateACL(c.getEnvironment(), delete.getFamilyCellMap());
        }
    }

    @Override
    public boolean preCheckAndPut(final ObserverContext<RegionCoprocessorEnvironment> c, final byte[] row,
            final byte[] family, final byte[] qualifier, final CompareFilter.CompareOp compareOp,
            final ByteArrayComparable comparator, final Put put, final boolean result) throws IOException {
        // Require READ and WRITE permissions on the table, CF, and KV to update
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<byte[]>> families = makeFamilyMap(family, qualifier);
        User user = getActiveUser();
        AuthResult authResult = permissionGranted(OpType.CHECK_AND_PUT, user, env, families, Action.READ,
                Action.WRITE);
        if (!authResult.isAllowed() && cellFeaturesEnabled && !compatibleEarlyTermination) {
            authResult.setAllowed(checkCoveringPermission(OpType.CHECK_AND_PUT, env, row, families,
                    HConstants.LATEST_TIMESTAMP, Action.READ));
            authResult.setReason("Covering cell set");
        }
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
        }
        byte[] bytes = put.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL);
        if (bytes != null) {
            if (cellFeaturesEnabled) {
                addCellPermissions(bytes, put.getFamilyCellMap());
            } else {
                throw new DoNotRetryIOException("Cell ACLs cannot be persisted");
            }
        }
        return result;
    }

    @Override
    public boolean preCheckAndDelete(final ObserverContext<RegionCoprocessorEnvironment> c, final byte[] row,
            final byte[] family, final byte[] qualifier, final CompareFilter.CompareOp compareOp,
            final ByteArrayComparable comparator, final Delete delete, final boolean result) throws IOException {
        // An ACL on a delete is useless, we shouldn't allow it
        if (delete.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL) != null) {
            throw new DoNotRetryIOException("ACL on checkAndDelete has no effect: " + delete.toString());
        }
        // Require READ and WRITE permissions on the table, CF, and the KV covered
        // by the delete
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<byte[]>> families = makeFamilyMap(family, qualifier);
        User user = getActiveUser();
        AuthResult authResult = permissionGranted(OpType.CHECK_AND_DELETE, user, env, families, Action.READ,
                Action.WRITE);
        if (!authResult.isAllowed() && cellFeaturesEnabled && !compatibleEarlyTermination) {
            authResult.setAllowed(checkCoveringPermission(OpType.CHECK_AND_DELETE, env, row, families,
                    HConstants.LATEST_TIMESTAMP, Action.READ));
            authResult.setReason("Covering cell set");
        }
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
        }
        return result;
    }

    @Override
    public long preIncrementColumnValue(final ObserverContext<RegionCoprocessorEnvironment> c, final byte[] row,
            final byte[] family, final byte[] qualifier, final long amount, final boolean writeToWAL)
            throws IOException {
        // Require WRITE permission to the table, CF, and the KV to be replaced by the
        // incremented value
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<byte[]>> families = makeFamilyMap(family, qualifier);
        User user = getActiveUser();
        AuthResult authResult = permissionGranted(OpType.INCREMENT_COLUMN_VALUE, user, env, families, Action.WRITE);
        if (!authResult.isAllowed() && cellFeaturesEnabled && !compatibleEarlyTermination) {
            authResult.setAllowed(checkCoveringPermission(OpType.INCREMENT_COLUMN_VALUE, env, row, families,
                    HConstants.LATEST_TIMESTAMP, Action.WRITE));
            authResult.setReason("Covering cell set");
        }
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
        }
        return -1;
    }

    @Override
    public Result preAppend(ObserverContext<RegionCoprocessorEnvironment> c, Append append) throws IOException {
        // Require WRITE permission to the table, CF, and the KV to be appended
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<Cell>> families = append.getFamilyCellMap();
        User user = getActiveUser();
        AuthResult authResult = permissionGranted(OpType.APPEND, user, env, families, Action.WRITE);
        if (!authResult.isAllowed() && cellFeaturesEnabled && !compatibleEarlyTermination) {
            authResult.setAllowed(checkCoveringPermission(OpType.APPEND, env, append.getRow(), families,
                    HConstants.LATEST_TIMESTAMP, Action.WRITE));
            authResult.setReason("Covering cell set");
        }
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
        }
        byte[] bytes = append.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL);
        if (bytes != null) {
            if (cellFeaturesEnabled) {
                addCellPermissions(bytes, append.getFamilyCellMap());
            } else {
                throw new DoNotRetryIOException("Cell ACLs cannot be persisted");
            }
        }
        return null;
    }

    @Override
    public Result preIncrement(final ObserverContext<RegionCoprocessorEnvironment> c, final Increment increment)
            throws IOException {
        // Require WRITE permission to the table, CF, and the KV to be replaced by
        // the incremented value
        RegionCoprocessorEnvironment env = c.getEnvironment();
        Map<byte[], ? extends Collection<Cell>> families = increment.getFamilyCellMap();
        User user = getActiveUser();
        AuthResult authResult = permissionGranted(OpType.INCREMENT, user, env, families, Action.WRITE);
        if (!authResult.isAllowed() && cellFeaturesEnabled && !compatibleEarlyTermination) {
            authResult.setAllowed(checkCoveringPermission(OpType.INCREMENT, env, increment.getRow(), families,
                    increment.getTimeRange().getMax(), Action.WRITE));
            authResult.setReason("Covering cell set");
        }
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
        }
        byte[] bytes = increment.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL);
        if (bytes != null) {
            if (cellFeaturesEnabled) {
                addCellPermissions(bytes, increment.getFamilyCellMap());
            } else {
                throw new DoNotRetryIOException("Cell ACLs cannot be persisted");
            }
        }
        return null;
    }

    @Override
    public Cell postMutationBeforeWAL(ObserverContext<RegionCoprocessorEnvironment> ctx, MutationType opType,
            Mutation mutation, Cell oldCell, Cell newCell) throws IOException {
        // If the HFile version is insufficient to persist tags, we won't have any
        // work to do here
        if (!cellFeaturesEnabled) {
            return newCell;
        }

        // Collect any ACLs from the old cell
        List<Tag> tags = Lists.newArrayList();
        ListMultimap<String, Permission> perms = ArrayListMultimap.create();
        if (oldCell != null) {
            byte[] tagBytes = CellUtil.getTagArray(oldCell);
            Iterator<Tag> tagIterator = CellUtil.tagsIterator(tagBytes, 0, tagBytes.length);
            while (tagIterator.hasNext()) {
                Tag tag = tagIterator.next();
                if (tag.getType() != AccessControlLists.ACL_TAG_TYPE) {
                    if (LOG.isTraceEnabled()) {
                        LOG.trace("Carrying forward tag from " + oldCell + ": type " + tag.getType() + " length "
                                + tag.getValue().length);
                    }
                    tags.add(tag);
                } else {
                    ListMultimap<String, Permission> kvPerms = ProtobufUtil
                            .toUsersAndPermissions(AccessControlProtos.UsersAndPermissions.newBuilder()
                                    .mergeFrom(tag.getBuffer(), tag.getTagOffset(), tag.getTagLength()).build());
                    perms.putAll(kvPerms);
                }
            }
        }

        // Do we have an ACL on the operation?
        byte[] aclBytes = mutation.getACL();
        if (aclBytes != null) {
            // Yes, use it
            tags.add(new Tag(AccessControlLists.ACL_TAG_TYPE, aclBytes));
        } else {
            // No, use what we carried forward
            if (perms != null) {
                // TODO: If we collected ACLs from more than one tag we may have a
                // List<Permission> of size > 1, this can be collapsed into a single
                // Permission
                if (LOG.isTraceEnabled()) {
                    LOG.trace("Carrying forward ACLs from " + oldCell + ": " + perms);
                }
                tags.add(new Tag(AccessControlLists.ACL_TAG_TYPE,
                        ProtobufUtil.toUsersAndPermissions(perms).toByteArray()));
            }
        }

        // If we have no tags to add, just return
        if (tags.isEmpty()) {
            return newCell;
        }

        // We need to create another KV, unfortunately, because the current new KV
        // has no space for tags
        KeyValue newKv = KeyValueUtil.ensureKeyValue(newCell);
        KeyValue rewriteKv = new KeyValue(newKv.getRowArray(), newKv.getRowOffset(), newKv.getRowLength(),
                newKv.getFamilyArray(), newKv.getFamilyOffset(), newKv.getFamilyLength(), newKv.getQualifierArray(),
                newKv.getQualifierOffset(), newKv.getQualifierLength(), newKv.getTimestamp(),
                KeyValue.Type.codeToType(newKv.getTypeByte()), newKv.getValueArray(), newKv.getValueOffset(),
                newKv.getValueLength(), tags);
        // Preserve mvcc data
        rewriteKv.setMvccVersion(newKv.getMvccVersion());
        return rewriteKv;
    }

    @Override
    public RegionScanner preScannerOpen(final ObserverContext<RegionCoprocessorEnvironment> c, final Scan scan,
            final RegionScanner s) throws IOException {
        internalPreRead(c, scan, OpType.SCAN);
        return s;
    }

    @Override
    public RegionScanner postScannerOpen(final ObserverContext<RegionCoprocessorEnvironment> c, final Scan scan,
            final RegionScanner s) throws IOException {
        User user = getActiveUser();
        if (user != null && user.getShortName() != null) { // store reference to scanner owner for later checks
            scannerOwners.put(s, user.getShortName());
        }
        return s;
    }

    @Override
    public boolean preScannerNext(final ObserverContext<RegionCoprocessorEnvironment> c, final InternalScanner s,
            final List<Result> result, final int limit, final boolean hasNext) throws IOException {
        requireScannerOwner(s);
        return hasNext;
    }

    @Override
    public void preScannerClose(final ObserverContext<RegionCoprocessorEnvironment> c, final InternalScanner s)
            throws IOException {
        requireScannerOwner(s);
    }

    @Override
    public void postScannerClose(final ObserverContext<RegionCoprocessorEnvironment> c, final InternalScanner s)
            throws IOException {
        // clean up any associated owner mapping
        scannerOwners.remove(s);
    }

    /**
     * Verify, when servicing an RPC, that the caller is the scanner owner.
     * If so, we assume that access control is correctly enforced based on
     * the checks performed in preScannerOpen()
     */
    private void requireScannerOwner(InternalScanner s) throws AccessDeniedException {
        if (RequestContext.isInRequestContext()) {
            String requestUserName = RequestContext.getRequestUserName();
            String owner = scannerOwners.get(s);
            if (owner != null && !owner.equals(requestUserName)) {
                throw new AccessDeniedException("User '" + requestUserName + "' is not the scanner owner!");
            }
        }
    }

    /**
     * Verifies user has WRITE privileges on
     * the Column Families involved in the bulkLoadHFile
     * request. Specific Column Write privileges are presently
     * ignored.
     */
    @Override
    public void preBulkLoadHFile(ObserverContext<RegionCoprocessorEnvironment> ctx,
            List<Pair<byte[], String>> familyPaths) throws IOException {
        for (Pair<byte[], String> el : familyPaths) {
            requirePermission("preBulkLoadHFile", ctx.getEnvironment().getRegion().getTableDesc().getTableName(),
                    el.getFirst(), null, Action.CREATE);
        }
    }

    private AuthResult hasSomeAccess(RegionCoprocessorEnvironment e, String method, Action action)
            throws IOException {
        User requestUser = getActiveUser();
        TableName tableName = e.getRegion().getTableDesc().getTableName();
        AuthResult authResult = permissionGranted(method, requestUser, action, e, Collections.EMPTY_MAP);
        if (!authResult.isAllowed()) {
            for (UserPermission userPerm : AccessControlLists.getUserTablePermissions(regionEnv.getConfiguration(),
                    tableName)) {
                for (Action userAction : userPerm.getActions()) {
                    if (userAction.equals(action)) {
                        return AuthResult.allow(method, "Access allowed", requestUser, action, tableName, null,
                                null);
                    }
                }
            }
        }
        return authResult;
    }

    /**
     * Authorization check for
     * SecureBulkLoadProtocol.prepareBulkLoad()
     * @param e
     * @throws IOException
     */
    //TODO this should end up as a coprocessor hook
    public void prePrepareBulkLoad(RegionCoprocessorEnvironment e) throws IOException {
        AuthResult authResult = hasSomeAccess(e, "prePrepareBulkLoad", Action.WRITE);
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions (table="
                    + e.getRegion().getTableDesc().getTableName() + ", action=WRITE)");
        }
    }

    /**
     * Authorization security check for
     * SecureBulkLoadProtocol.cleanupBulkLoad()
     * @param e
     * @throws IOException
     */
    //TODO this should end up as a coprocessor hook
    public void preCleanupBulkLoad(RegionCoprocessorEnvironment e) throws IOException {
        AuthResult authResult = hasSomeAccess(e, "preCleanupBulkLoad", Action.WRITE);
        logResult(authResult);
        if (!authResult.isAllowed()) {
            throw new AccessDeniedException("Insufficient permissions (table="
                    + e.getRegion().getTableDesc().getTableName() + ", action=WRITE)");
        }
    }

    /* ---- EndpointObserver implementation ---- */

    @Override
    public Message preEndpointInvocation(ObserverContext<RegionCoprocessorEnvironment> ctx, Service service,
            String methodName, Message request) throws IOException {
        // Don't intercept calls to our own AccessControlService, we check for
        // appropriate permissions in the service handlers
        if (shouldCheckExecPermission && !(service instanceof AccessControlService)) {
            requirePermission("invoke(" + service.getDescriptorForType().getName() + "." + methodName + ")",
                    getTableName(ctx.getEnvironment()), null, null, Action.EXEC);
        }
        return request;
    }

    @Override
    public void postEndpointInvocation(ObserverContext<RegionCoprocessorEnvironment> ctx, Service service,
            String methodName, Message request, Message.Builder responseBuilder) throws IOException {
    }

    /* ---- Protobuf AccessControlService implementation ---- */

    @Override
    public void grant(RpcController controller, AccessControlProtos.GrantRequest request,
            RpcCallback<AccessControlProtos.GrantResponse> done) {
        UserPermission perm = ProtobufUtil.toUserPermission(request.getUserPermission());
        AccessControlProtos.GrantResponse response = null;
        try {
            // verify it's only running at .acl.
            if (aclRegion) {
                if (!initialized) {
                    throw new CoprocessorException("AccessController not yet initialized");
                }
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Received request to grant access permission " + perm.toString());
                }

                switch (request.getUserPermission().getPermission().getType()) {
                case Global:
                case Table:
                    requirePermission("grant", perm.getTableName(), perm.getFamily(), perm.getQualifier(),
                            Action.ADMIN);
                    break;
                case Namespace:
                    requireGlobalPermission("grant", Action.ADMIN, perm.getNamespace());
                }

                AccessControlLists.addUserPermission(regionEnv.getConfiguration(), perm);
                if (AUDITLOG.isTraceEnabled()) {
                    // audit log should store permission changes in addition to auth results
                    AUDITLOG.trace("Granted permission " + perm.toString());
                }
            } else {
                throw new CoprocessorException(AccessController.class,
                        "This method " + "can only execute at " + AccessControlLists.ACL_TABLE_NAME + " table.");
            }
            response = AccessControlProtos.GrantResponse.getDefaultInstance();
        } catch (IOException ioe) {
            // pass exception back up
            ResponseConverter.setControllerException(controller, ioe);
        }
        done.run(response);
    }

    @Override
    public void revoke(RpcController controller, AccessControlProtos.RevokeRequest request,
            RpcCallback<AccessControlProtos.RevokeResponse> done) {
        UserPermission perm = ProtobufUtil.toUserPermission(request.getUserPermission());
        AccessControlProtos.RevokeResponse response = null;
        try {
            // only allowed to be called on _acl_ region
            if (aclRegion) {
                if (!initialized) {
                    throw new CoprocessorException("AccessController not yet initialized");
                }
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Received request to revoke access permission " + perm.toString());
                }

                switch (request.getUserPermission().getPermission().getType()) {
                case Global:
                case Table:
                    requirePermission("revoke", perm.getTableName(), perm.getFamily(), perm.getQualifier(),
                            Action.ADMIN);
                    break;
                case Namespace:
                    requireGlobalPermission("revoke", Action.ADMIN, perm.getNamespace());
                }

                AccessControlLists.removeUserPermission(regionEnv.getConfiguration(), perm);
                if (AUDITLOG.isTraceEnabled()) {
                    // audit log should record all permission changes
                    AUDITLOG.trace("Revoked permission " + perm.toString());
                }
            } else {
                throw new CoprocessorException(AccessController.class,
                        "This method " + "can only execute at " + AccessControlLists.ACL_TABLE_NAME + " table.");
            }
            response = AccessControlProtos.RevokeResponse.getDefaultInstance();
        } catch (IOException ioe) {
            // pass exception back up
            ResponseConverter.setControllerException(controller, ioe);
        }
        done.run(response);
    }

    @Override
    public void getUserPermissions(RpcController controller, AccessControlProtos.GetUserPermissionsRequest request,
            RpcCallback<AccessControlProtos.GetUserPermissionsResponse> done) {
        AccessControlProtos.GetUserPermissionsResponse response = null;
        try {
            // only allowed to be called on _acl_ region
            if (aclRegion) {
                if (!initialized) {
                    throw new CoprocessorException("AccessController not yet initialized");
                }
                List<UserPermission> perms = null;
                if (request.getType() == AccessControlProtos.Permission.Type.Table) {
                    TableName table = null;
                    if (request.hasTableName()) {
                        table = ProtobufUtil.toTableName(request.getTableName());
                    }
                    requirePermission("userPermissions", table, null, null, Action.ADMIN);

                    perms = AccessControlLists.getUserTablePermissions(regionEnv.getConfiguration(), table);
                } else if (request.getType() == AccessControlProtos.Permission.Type.Namespace) {
                    perms = AccessControlLists.getUserNamespacePermissions(regionEnv.getConfiguration(),
                            request.getNamespaceName().toStringUtf8());
                } else {
                    perms = AccessControlLists.getUserPermissions(regionEnv.getConfiguration(), null);
                }
                response = ResponseConverter.buildGetUserPermissionsResponse(perms);
            } else {
                throw new CoprocessorException(AccessController.class,
                        "This method " + "can only execute at " + AccessControlLists.ACL_TABLE_NAME + " table.");
            }
        } catch (IOException ioe) {
            // pass exception back up
            ResponseConverter.setControllerException(controller, ioe);
        }
        done.run(response);
    }

    @Override
    public void checkPermissions(RpcController controller, AccessControlProtos.CheckPermissionsRequest request,
            RpcCallback<AccessControlProtos.CheckPermissionsResponse> done) {
        Permission[] permissions = new Permission[request.getPermissionCount()];
        for (int i = 0; i < request.getPermissionCount(); i++) {
            permissions[i] = ProtobufUtil.toPermission(request.getPermission(i));
        }
        AccessControlProtos.CheckPermissionsResponse response = null;
        try {
            TableName tableName = regionEnv.getRegion().getTableDesc().getTableName();
            for (Permission permission : permissions) {
                if (permission instanceof TablePermission) {
                    TablePermission tperm = (TablePermission) permission;
                    for (Action action : permission.getActions()) {
                        if (!tperm.getTableName().equals(tableName)) {
                            throw new CoprocessorException(AccessController.class, String.format(
                                    "This method " + "can only execute at the table specified in TablePermission. "
                                            + "Table of the region:%s , requested table:%s",
                                    tableName, tperm.getTableName()));
                        }

                        Map<byte[], Set<byte[]>> familyMap = new TreeMap<byte[], Set<byte[]>>(
                                Bytes.BYTES_COMPARATOR);
                        if (tperm.getFamily() != null) {
                            if (tperm.getQualifier() != null) {
                                Set<byte[]> qualifiers = Sets.newTreeSet(Bytes.BYTES_COMPARATOR);
                                qualifiers.add(tperm.getQualifier());
                                familyMap.put(tperm.getFamily(), qualifiers);
                            } else {
                                familyMap.put(tperm.getFamily(), null);
                            }
                        }

                        requirePermission("checkPermissions", action, regionEnv, familyMap);
                    }

                } else {
                    for (Action action : permission.getActions()) {
                        requirePermission("checkPermissions", action);
                    }
                }
            }
            response = AccessControlProtos.CheckPermissionsResponse.getDefaultInstance();
        } catch (IOException ioe) {
            ResponseConverter.setControllerException(controller, ioe);
        }
        done.run(response);
    }

    @Override
    public Service getService() {
        return AccessControlProtos.AccessControlService.newReflectiveService(this);
    }

    private HRegion getRegion(RegionCoprocessorEnvironment e) {
        return e.getRegion();
    }

    private TableName getTableName(RegionCoprocessorEnvironment e) {
        HRegion region = e.getRegion();
        if (region != null) {
            return getTableName(region);
        }
        return null;
    }

    private TableName getTableName(HRegion region) {
        HRegionInfo regionInfo = region.getRegionInfo();
        if (regionInfo != null) {
            return regionInfo.getTable();
        }
        return null;
    }

    @Override
    public void preClose(ObserverContext<RegionCoprocessorEnvironment> e, boolean abortRequested)
            throws IOException {
        requirePermission("preClose", Action.ADMIN);
    }

    private void isSystemOrSuperUser(Configuration conf) throws IOException {
        User user = userProvider.getCurrent();
        if (user == null) {
            throw new IOException("Unable to obtain the current user, "
                    + "authorization checks for internal operations will not work correctly!");
        }

        String currentUser = user.getShortName();
        List<String> superusers = Lists.asList(currentUser,
                conf.getStrings(AccessControlLists.SUPERUSER_CONF_KEY, new String[0]));

        User activeUser = getActiveUser();
        if (!(superusers.contains(activeUser.getShortName()))) {
            throw new AccessDeniedException(
                    "User '" + (user != null ? user.getShortName() : "null") + "is not system or super user.");
        }
    }

    @Override
    public void preStopRegionServer(ObserverContext<RegionServerCoprocessorEnvironment> env) throws IOException {
        requirePermission("preStopRegionServer", Action.ADMIN);
    }

    private Map<byte[], ? extends Collection<byte[]>> makeFamilyMap(byte[] family, byte[] qualifier) {
        if (family == null) {
            return null;
        }

        Map<byte[], Collection<byte[]>> familyMap = new TreeMap<byte[], Collection<byte[]>>(Bytes.BYTES_COMPARATOR);
        familyMap.put(family, qualifier != null ? ImmutableSet.of(qualifier) : null);
        return familyMap;
    }

    @Override
    public void preGetTableDescriptors(ObserverContext<MasterCoprocessorEnvironment> ctx,
            List<TableName> tableNamesList, List<HTableDescriptor> descriptors) throws IOException {
        // If the list is empty, this is a request for all table descriptors and requires GLOBAL
        // ADMIN privs.
        if (tableNamesList == null || tableNamesList.isEmpty()) {
            requireGlobalPermission("getTableDescriptors", Action.ADMIN, null, null);
        }
        // Otherwise, if the requestor has ADMIN or CREATE privs for all listed tables, the
        // request can be granted.
        else {
            MasterServices masterServices = ctx.getEnvironment().getMasterServices();
            for (TableName tableName : tableNamesList) {
                // Do not deny if the table does not exist
                try {
                    masterServices.checkTableModifiable(tableName);
                } catch (TableNotFoundException ex) {
                    // Skip checks for a table that does not exist
                    continue;
                } catch (TableNotDisabledException ex) {
                    // We don't care about this
                }
                requirePermission("getTableDescriptors", tableName, null, null, Action.ADMIN, Action.CREATE);
            }
        }
    }

    @Override
    public void postGetTableDescriptors(ObserverContext<MasterCoprocessorEnvironment> ctx,
            List<HTableDescriptor> descriptors) throws IOException {
    }

    @Override
    public void preMerge(ObserverContext<RegionServerCoprocessorEnvironment> ctx, HRegion regionA, HRegion regionB)
            throws IOException {
        requirePermission("mergeRegions", regionA.getTableDesc().getTableName(), null, null, Action.ADMIN);
    }

    @Override
    public void postMerge(ObserverContext<RegionServerCoprocessorEnvironment> c, HRegion regionA, HRegion regionB,
            HRegion mergedRegion) throws IOException {
    }

    @Override
    public void preMergeCommit(ObserverContext<RegionServerCoprocessorEnvironment> ctx, HRegion regionA,
            HRegion regionB, List<Mutation> metaEntries) throws IOException {
    }

    @Override
    public void postMergeCommit(ObserverContext<RegionServerCoprocessorEnvironment> ctx, HRegion regionA,
            HRegion regionB, HRegion mergedRegion) throws IOException {
    }

    @Override
    public void preRollBackMerge(ObserverContext<RegionServerCoprocessorEnvironment> ctx, HRegion regionA,
            HRegion regionB) throws IOException {
    }

    @Override
    public void postRollBackMerge(ObserverContext<RegionServerCoprocessorEnvironment> ctx, HRegion regionA,
            HRegion regionB) throws IOException {
    }
}