HBASE-11077 [AccessController] Restore compatible early-out access denial

git-svn-id: https://svn.apache.org/repos/asf/hbase/trunk@1591524 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andrew Kyle Purtell 2014-05-01 01:04:36 +00:00
parent 9047981a0c
commit 86464360f8
11 changed files with 1086 additions and 396 deletions

View File

@ -25,6 +25,22 @@ import org.apache.hadoop.classification.InterfaceStability;
@InterfaceStability.Evolving @InterfaceStability.Evolving
public interface AccessControlConstants { public interface AccessControlConstants {
/**
* Configuration option that toggles whether EXEC permission checking is
* performed during coprocessor endpoint invocations.
*/
public static final String EXEC_PERMISSION_CHECKS_KEY = "hbase.security.exec.permission.checks";
/** Default setting for hbase.security.exec.permission.checks; false */
public static final boolean DEFAULT_EXEC_PERMISSION_CHECKS = false;
/**
* Configuration or CF schema option for early termination of access checks
* if table or CF permissions grant access. Pre-0.98 compatible behavior
*/
public static final String CF_ATTRIBUTE_EARLY_OUT = "hbase.security.access.early_out";
/** Default setting for hbase.security.access.early_out */
public static final boolean DEFAULT_ATTRIBUTE_EARLY_OUT = true;
// Operation attributes for cell level security // Operation attributes for cell level security
/** Cell level ACL */ /** Cell level ACL */

View File

@ -22,10 +22,10 @@ import java.io.IOException;
import java.util.Map; import java.util.Map;
import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.exceptions.DeserializationException; import org.apache.hadoop.hbase.exceptions.DeserializationException;
import org.apache.hadoop.hbase.filter.FilterBase; import org.apache.hadoop.hbase.filter.FilterBase;
import org.apache.hadoop.hbase.filter.Filter.ReturnCode;
import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.util.ByteRange; import org.apache.hadoop.hbase.util.ByteRange;
import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.Bytes;
@ -48,11 +48,20 @@ import org.apache.hadoop.hbase.util.SimpleByteRange;
*/ */
class AccessControlFilter extends FilterBase { class AccessControlFilter extends FilterBase {
public static enum Strategy {
/** Filter only by checking the table or CF permissions */
CHECK_TABLE_AND_CF_ONLY,
/** Cell permissions can override table or CF permissions */
CHECK_CELL_DEFAULT,
/** Cell permissions must authorize */
CHECK_CELL_FIRST,
};
private TableAuthManager authManager; private TableAuthManager authManager;
private TableName table; private TableName table;
private User user; private User user;
private boolean isSystemTable; private boolean isSystemTable;
private boolean cellFirstStrategy; private Strategy strategy;
private Map<ByteRange, Integer> cfVsMaxVersions; private Map<ByteRange, Integer> cfVsMaxVersions;
private int familyMaxVersions; private int familyMaxVersions;
private int currentVersions; private int currentVersions;
@ -66,12 +75,12 @@ class AccessControlFilter extends FilterBase {
} }
AccessControlFilter(TableAuthManager mgr, User ugi, TableName tableName, AccessControlFilter(TableAuthManager mgr, User ugi, TableName tableName,
boolean cellFirstStrategy, Map<ByteRange, Integer> cfVsMaxVersions) { Strategy strategy, Map<ByteRange, Integer> cfVsMaxVersions) {
authManager = mgr; authManager = mgr;
table = tableName; table = tableName;
user = ugi; user = ugi;
isSystemTable = tableName.isSystemTable(); isSystemTable = tableName.isSystemTable();
this.cellFirstStrategy = cellFirstStrategy; this.strategy = strategy;
this.cfVsMaxVersions = cfVsMaxVersions; this.cfVsMaxVersions = cfVsMaxVersions;
this.prevFam = new SimpleByteRange(); this.prevFam = new SimpleByteRange();
this.prevQual = new SimpleByteRange(); this.prevQual = new SimpleByteRange();
@ -103,12 +112,37 @@ class AccessControlFilter extends FilterBase {
if (currentVersions > familyMaxVersions) { if (currentVersions > familyMaxVersions) {
return ReturnCode.SKIP; return ReturnCode.SKIP;
} }
if (authManager.authorize(user, table, cell, cellFirstStrategy, Permission.Action.READ)) { // XXX: Compare in place, don't clone
byte[] family = CellUtil.cloneFamily(cell);
byte[] qualifier = CellUtil.cloneQualifier(cell);
switch (strategy) {
// Filter only by checking the table or CF permissions
case CHECK_TABLE_AND_CF_ONLY: {
if (authManager.authorize(user, table, family, qualifier, Permission.Action.READ)) {
return ReturnCode.INCLUDE; return ReturnCode.INCLUDE;
} }
// Before per cell ACLs we used to return the NEXT_COL hint, but we can }
// no longer do that since, given the possibility of per cell ACLs break;
// anywhere, we now need to examine all KVs with this filter. // Cell permissions can override table or CF permissions
case CHECK_CELL_DEFAULT: {
if (authManager.authorize(user, table, family, qualifier, Permission.Action.READ) ||
authManager.authorize(user, table, cell, Permission.Action.READ)) {
return ReturnCode.INCLUDE;
}
}
break;
// Cell permissions must authorize
case CHECK_CELL_FIRST: {
if (authManager.authorize(user, table, cell, Permission.Action.READ) &&
authManager.authorize(user, table, family, qualifier, Permission.Action.READ)) {
return ReturnCode.INCLUDE;
}
}
break;
default:
throw new RuntimeException("Unhandled strategy " + strategy);
}
return ReturnCode.SKIP; return ReturnCode.SKIP;
} }

View File

@ -32,6 +32,7 @@ import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil; import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.CompoundConfiguration;
import org.apache.hadoop.hbase.CoprocessorEnvironment; import org.apache.hadoop.hbase.CoprocessorEnvironment;
import org.apache.hadoop.hbase.DoNotRetryIOException; import org.apache.hadoop.hbase.DoNotRetryIOException;
import org.apache.hadoop.hbase.HColumnDescriptor; import org.apache.hadoop.hbase.HColumnDescriptor;
@ -103,6 +104,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker; import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.protobuf.Message; import com.google.protobuf.Message;
import com.google.protobuf.RpcCallback; import com.google.protobuf.RpcCallback;
@ -149,9 +151,6 @@ public class AccessController extends BaseRegionObserver
private static final Log AUDITLOG = private static final Log AUDITLOG =
LogFactory.getLog("SecurityLogger."+AccessController.class.getName()); LogFactory.getLog("SecurityLogger."+AccessController.class.getName());
static final String EXEC_PERMISSION_CHECKS_KEY = "hbase.security.exec.permission.checks";
static final boolean DEFAULT_EXEC_PERMISSION_CHECKS = false;
TableAuthManager authManager = null; TableAuthManager authManager = null;
// flags if we are running on a region of the _acl_ table // flags if we are running on a region of the _acl_ table
@ -167,11 +166,15 @@ public class AccessController extends BaseRegionObserver
private UserProvider userProvider; private UserProvider userProvider;
// flags if we are able to support cell ACLs // if we are able to support cell ACLs
boolean canPersistCellACLs; boolean cellFeaturesEnabled;
// flags if we should check EXEC permissions // if we should check EXEC permissions
boolean shouldCheckExecPermissions; 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; private volatile boolean initialized = false;
@ -185,6 +188,7 @@ public class AccessController extends BaseRegionObserver
void initialize(RegionCoprocessorEnvironment e) throws IOException { void initialize(RegionCoprocessorEnvironment e) throws IOException {
final HRegion region = e.getRegion(); final HRegion region = e.getRegion();
Configuration conf = e.getConfiguration();
Map<byte[], ListMultimap<String,TablePermission>> tables = Map<byte[], ListMultimap<String,TablePermission>> tables =
AccessControlLists.loadAll(region); AccessControlLists.loadAll(region);
// For each table, write out the table's permissions to the respective // For each table, write out the table's permissions to the respective
@ -193,11 +197,9 @@ public class AccessController extends BaseRegionObserver
tables.entrySet()) { tables.entrySet()) {
byte[] entry = t.getKey(); byte[] entry = t.getKey();
ListMultimap<String,TablePermission> perms = t.getValue(); ListMultimap<String,TablePermission> perms = t.getValue();
byte[] serialized = AccessControlLists.writePermissionsAsBytes(perms, e.getConfiguration()); byte[] serialized = AccessControlLists.writePermissionsAsBytes(perms, conf);
this.authManager.getZKPermissionWatcher().writeToZookeeper(entry, serialized); this.authManager.getZKPermissionWatcher().writeToZookeeper(entry, serialized);
} }
shouldCheckExecPermissions = e.getConfiguration().getBoolean(EXEC_PERMISSION_CHECKS_KEY,
DEFAULT_EXEC_PERMISSION_CHECKS);
initialized = true; initialized = true;
} }
@ -250,7 +252,7 @@ public class AccessController extends BaseRegionObserver
* the request * the request
* @return an authorization result * @return an authorization result
*/ */
AuthResult permissionGranted(String request, User user, Permission.Action permRequest, AuthResult permissionGranted(String request, User user, Action permRequest,
RegionCoprocessorEnvironment e, RegionCoprocessorEnvironment e,
Map<byte [], ? extends Collection<?>> families) { Map<byte [], ? extends Collection<?>> families) {
HRegionInfo hri = e.getRegion().getRegionInfo(); HRegionInfo hri = e.getRegion().getRegionInfo();
@ -259,7 +261,7 @@ public class AccessController extends BaseRegionObserver
// 1. All users need read access to hbase:meta table. // 1. All users need read access to hbase:meta table.
// this is a very common operation, so deal with it quickly. // this is a very common operation, so deal with it quickly.
if (hri.isMetaRegion()) { if (hri.isMetaRegion()) {
if (permRequest == Permission.Action.READ) { if (permRequest == Action.READ) {
return AuthResult.allow(request, "All users allowed", user, return AuthResult.allow(request, "All users allowed", user,
permRequest, tableName, families); permRequest, tableName, families);
} }
@ -275,11 +277,11 @@ public class AccessController extends BaseRegionObserver
// so the user need to be allowed to write on it. // 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_ // 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. // and the user need to be allowed to write on both tables.
if (permRequest == Permission.Action.WRITE && if (permRequest == Action.WRITE &&
(hri.isMetaRegion() || (hri.isMetaRegion() ||
Bytes.equals(tableName.getName(), AccessControlLists.ACL_GLOBAL_NAME)) && Bytes.equals(tableName.getName(), AccessControlLists.ACL_GLOBAL_NAME)) &&
(authManager.authorize(user, Permission.Action.CREATE) || (authManager.authorize(user, Action.CREATE) ||
authManager.authorize(user, Permission.Action.ADMIN))) authManager.authorize(user, Action.ADMIN)))
{ {
return AuthResult.allow(request, "Table permission granted", user, return AuthResult.allow(request, "Table permission granted", user,
permRequest, tableName, families); permRequest, tableName, families);
@ -340,6 +342,29 @@ public class AccessController extends BaseRegionObserver
user, permRequest, tableName, families); 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) { private void logResult(AuthResult result) {
if (AUDITLOG.isTraceEnabled()) { if (AUDITLOG.isTraceEnabled()) {
RequestContext ctx = RequestContext.get(); RequestContext ctx = RequestContext.get();
@ -407,7 +432,7 @@ public class AccessController extends BaseRegionObserver
* @throws IOException if obtaining the current user fails * @throws IOException if obtaining the current user fails
* @throws AccessDeniedException if authorization is denied * @throws AccessDeniedException if authorization is denied
*/ */
private void requirePermission(String request, Permission.Action perm) throws IOException { private void requirePermission(String request, Action perm) throws IOException {
requireGlobalPermission(request, perm, null, null); requireGlobalPermission(request, perm, null, null);
} }
@ -419,7 +444,7 @@ public class AccessController extends BaseRegionObserver
* @param families The map of column families-qualifiers. * @param families The map of column families-qualifiers.
* @throws AccessDeniedException if the authorization check failed * @throws AccessDeniedException if the authorization check failed
*/ */
private void requirePermission(String request, Permission.Action perm, private void requirePermission(String request, Action perm,
RegionCoprocessorEnvironment env, RegionCoprocessorEnvironment env,
Map<byte[], ? extends Collection<?>> families) Map<byte[], ? extends Collection<?>> families)
throws IOException { throws IOException {
@ -444,7 +469,7 @@ public class AccessController extends BaseRegionObserver
* @param tableName Affected table name. * @param tableName Affected table name.
* @param familyMap Affected column families. * @param familyMap Affected column families.
*/ */
private void requireGlobalPermission(String request, Permission.Action perm, TableName tableName, private void requireGlobalPermission(String request, Action perm, TableName tableName,
Map<byte[], ? extends Collection<byte[]>> familyMap) throws IOException { Map<byte[], ? extends Collection<byte[]>> familyMap) throws IOException {
User user = getActiveUser(); User user = getActiveUser();
if (authManager.authorize(user, perm)) { if (authManager.authorize(user, perm)) {
@ -464,7 +489,7 @@ public class AccessController extends BaseRegionObserver
* @param perm Action being requested * @param perm Action being requested
* @param namespace * @param namespace
*/ */
private void requireGlobalPermission(String request, Permission.Action perm, private void requireGlobalPermission(String request, Action perm,
String namespace) throws IOException { String namespace) throws IOException {
User user = getActiveUser(); User user = getActiveUser();
if (authManager.authorize(user, perm)) { if (authManager.authorize(user, perm)) {
@ -477,8 +502,52 @@ public class AccessController extends BaseRegionObserver
} }
} }
/**
* 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 { private enum OpType {
GET_CLOSEST_ROW_BEFORE("getClosestRowBefore"), GET_CLOSEST_ROW_BEFORE("getClosestRowBefore"),
GET("get"),
EXISTS("exists"),
SCAN("scan"),
PUT("put"), PUT("put"),
DELETE("delete"), DELETE("delete"),
CHECK_AND_PUT("checkAndPut"), CHECK_AND_PUT("checkAndPut"),
@ -499,44 +568,20 @@ public class AccessController extends BaseRegionObserver
} }
} }
private void requireCoveringPermission(OpType request, RegionCoprocessorEnvironment e, /**
* 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) byte[] row, Map<byte[], ? extends Collection<?>> familyMap, long opTs, Action... actions)
throws IOException { throws IOException {
if (!cellFeaturesEnabled) {
return false;
}
long cellGrants = 0;
User user = getActiveUser(); User user = getActiveUser();
// First check table or CF level permissions, if they grant access we can
// early out before needing to enumerate over per KV perms.
List<Action> cellCheckActions = Lists.newArrayList();
// TODO: permissionGranted should support checking multiple actions or
// we should convert actions into a bitmap and pass that around. See
// HBASE-7123.
AuthResult results[] = new AuthResult[actions.length];
for (int i = 0; i < actions.length; i++) {
results[i] = permissionGranted(request.type, user, actions[i], e, familyMap);
if (!results[i].isAllowed()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Got " + results[i] + ", added to cellCheckActions");
}
cellCheckActions.add(actions[i]);
}
}
// If all permissions checks passed, we can early out
if (cellCheckActions.isEmpty()) {
if (LOG.isTraceEnabled()) {
LOG.trace("All permissions checks passed, we can early out");
}
for (int i = 0; i < results.length; i++) {
logResult(results[i]);
}
return;
}
// Table or CF permissions do not allow, enumerate the covered KVs. We
// can stop at the first which does not grant access.
int cellsChecked = 0;
long latestCellTs = 0; long latestCellTs = 0;
if (canPersistCellACLs) {
Get get = new Get(row); Get get = new Get(row);
// Only in case of Put/Delete op, consider TS within cell (if set for individual cells). // 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 // When every cell, within a Mutation, can be linked with diff TS we can not rely on only one
@ -653,8 +698,9 @@ public class AccessController extends BaseRegionObserver
if ((col.getQualifierLength() == 0 && request == OpType.DELETE) if ((col.getQualifierLength() == 0 && request == OpType.DELETE)
|| CellUtil.matchingQualifier(cell, col)) { || CellUtil.matchingQualifier(cell, col)) {
byte type = col.getTypeByte(); byte type = col.getTypeByte();
if (considerCellTs) if (considerCellTs) {
curColCheckTs = col.getTimestamp(); curColCheckTs = col.getTimestamp();
}
// For a Delete op we pass allVersions as true. When a Delete Mutation contains // 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 // 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 // that column. Check all versions when Type is DeleteColumn or DeleteFamily
@ -670,18 +716,14 @@ public class AccessController extends BaseRegionObserver
continue; continue;
} }
foundColumn = true; foundColumn = true;
for (Action action: cellCheckActions) { for (Action action: actions) {
// Are there permissions for this user for the cell? // Are there permissions for this user for the cell?
if (!authManager.authorize(user, getTableName(e), cell, false, action)) { if (!authManager.authorize(user, getTableName(e), cell, action)) {
AuthResult authResult = AuthResult.deny(request.type, "Insufficient permissions", // We can stop if the cell ACL denies access
user, action, getTableName(e), CellUtil.cloneFamily(cell), return false;
CellUtil.cloneQualifier(cell));
logResult(authResult);
throw new AccessDeniedException("Insufficient permissions " +
authResult.toContextString());
} }
} }
cellsChecked++; cellGrants++;
} }
} while (more); } while (more);
} catch (AccessDeniedException ex) { } catch (AccessDeniedException ex) {
@ -691,28 +733,10 @@ public class AccessController extends BaseRegionObserver
} finally { } finally {
scanner.close(); 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
// If there were no cells to check, throw the ADE // after no table or CF grants are found.
if (cellsChecked < 1) { return cellGrants > 0;
if (LOG.isTraceEnabled()) {
LOG.trace("No cells found with scan");
}
AuthResult authResult = AuthResult.deny(request.type, "Insufficient permissions",
user, cellCheckActions.get(0), getTableName(e), familyMap);
logResult(authResult);
throw new AccessDeniedException("Insufficient permissions " +
authResult.toContextString());
}
// Log that authentication succeeded. We need to trade off logging maybe
// thousands of fine grained decisions with providing detail.
for (byte[] family: familyMap.keySet()) {
for (Action action: actions) {
logResult(AuthResult.allow(request.type, "Permission granted", user, action,
getTableName(e), family, null));
}
}
} }
private void addCellPermissions(final byte[] perms, Map<byte[], List<Cell>> familyMap) { private void addCellPermissions(final byte[] perms, Map<byte[], List<Cell>> familyMap) {
@ -743,42 +767,22 @@ public class AccessController extends BaseRegionObserver
} }
} }
private void internalPreRead(final ObserverContext<RegionCoprocessorEnvironment> c,
final Query query) throws IOException {
TableName tableName = getTableName(c.getEnvironment());
User activeUser = getActiveUser();
Filter filter = query.getFilter();
boolean cellFirstStrategy = query.getACLStrategy();
// Don't wrap an AccessControlFilter
if (filter != null && filter instanceof AccessControlFilter) {
return;
}
Map<ByteRange, Integer> cfVsMaxVersions = new HashMap<ByteRange, Integer>();
HRegion region = c.getEnvironment().getRegion();
for (HColumnDescriptor hcd : region.getTableDesc().getFamilies()) {
cfVsMaxVersions.put(new SimpleByteRange(hcd.getName()), hcd.getMaxVersions());
}
Filter newFilter = (filter != null)
? new FilterList(FilterList.Operator.MUST_PASS_ALL,
Lists.newArrayList(
new AccessControlFilter(authManager, activeUser, tableName,
cellFirstStrategy, cfVsMaxVersions),
filter))
: new AccessControlFilter(authManager, activeUser, tableName,
cellFirstStrategy, cfVsMaxVersions);
query.setFilter(newFilter);
}
/* ---- MasterObserver implementation ---- */ /* ---- MasterObserver implementation ---- */
public void start(CoprocessorEnvironment env) throws IOException { public void start(CoprocessorEnvironment env) throws IOException {
canPersistCellACLs = HFile.getFormatVersion(env.getConfiguration()) >= CompoundConfiguration conf = new CompoundConfiguration();
HFile.MIN_FORMAT_VERSION_WITH_TAGS; conf.add(env.getConfiguration());
if (!canPersistCellACLs) {
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 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 + " is required to persist cell ACLs. Consider setting " + HFile.FORMAT_VERSION_KEY
+ " accordingly."); + " accordingly.");
} }
ZooKeeperWatcher zk = null; ZooKeeperWatcher zk = null;
if (env instanceof MasterCoprocessorEnvironment) { if (env instanceof MasterCoprocessorEnvironment) {
// if running on HMaster // if running on HMaster
@ -790,7 +794,10 @@ public class AccessController extends BaseRegionObserver
} else if (env instanceof RegionCoprocessorEnvironment) { } else if (env instanceof RegionCoprocessorEnvironment) {
// if running at region // if running at region
regionEnv = (RegionCoprocessorEnvironment) env; regionEnv = (RegionCoprocessorEnvironment) env;
conf.addStringMap(regionEnv.getRegion().getTableDesc().getConfiguration());
zk = regionEnv.getRegionServerServices().getZooKeeper(); zk = regionEnv.getRegionServerServices().getZooKeeper();
compatibleEarlyTermination = conf.getBoolean(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT,
AccessControlConstants.DEFAULT_ATTRIBUTE_EARLY_OUT);
} }
// set the user-provider. // set the user-provider.
@ -821,7 +828,7 @@ public class AccessController extends BaseRegionObserver
for (byte[] family: families) { for (byte[] family: families) {
familyMap.put(family, null); familyMap.put(family, null);
} }
requireGlobalPermission("createTable", Permission.Action.CREATE, desc.getTableName(), familyMap); requireGlobalPermission("createTable", Action.CREATE, desc.getTableName(), familyMap);
} }
@Override @Override
@ -940,8 +947,7 @@ public class AccessController extends BaseRegionObserver
@Override @Override
public void postDeleteColumn(ObserverContext<MasterCoprocessorEnvironment> c, public void postDeleteColumn(ObserverContext<MasterCoprocessorEnvironment> c,
TableName tableName, byte[] col) throws IOException { TableName tableName, byte[] col) throws IOException {
AccessControlLists.removeTablePermissions(c.getEnvironment().getConfiguration(), AccessControlLists.removeTablePermissions(c.getEnvironment().getConfiguration(), tableName, col);
tableName, col);
} }
@Override @Override
@ -1033,8 +1039,9 @@ public class AccessController extends BaseRegionObserver
@Override @Override
public void preBalance(ObserverContext<MasterCoprocessorEnvironment> c) public void preBalance(ObserverContext<MasterCoprocessorEnvironment> c)
throws IOException { throws IOException {
requirePermission("balance", Permission.Action.ADMIN); requirePermission("balance", Action.ADMIN);
} }
@Override @Override
public void postBalance(ObserverContext<MasterCoprocessorEnvironment> c, List<RegionPlan> plans) public void postBalance(ObserverContext<MasterCoprocessorEnvironment> c, List<RegionPlan> plans)
throws IOException {} throws IOException {}
@ -1042,7 +1049,7 @@ public class AccessController extends BaseRegionObserver
@Override @Override
public boolean preBalanceSwitch(ObserverContext<MasterCoprocessorEnvironment> c, public boolean preBalanceSwitch(ObserverContext<MasterCoprocessorEnvironment> c,
boolean newValue) throws IOException { boolean newValue) throws IOException {
requirePermission("balanceSwitch", Permission.Action.ADMIN); requirePermission("balanceSwitch", Action.ADMIN);
return newValue; return newValue;
} }
@ -1053,13 +1060,13 @@ public class AccessController extends BaseRegionObserver
@Override @Override
public void preShutdown(ObserverContext<MasterCoprocessorEnvironment> c) public void preShutdown(ObserverContext<MasterCoprocessorEnvironment> c)
throws IOException { throws IOException {
requirePermission("shutdown", Permission.Action.ADMIN); requirePermission("shutdown", Action.ADMIN);
} }
@Override @Override
public void preStopMaster(ObserverContext<MasterCoprocessorEnvironment> c) public void preStopMaster(ObserverContext<MasterCoprocessorEnvironment> c)
throws IOException { throws IOException {
requirePermission("stopMaster", Permission.Action.ADMIN); requirePermission("stopMaster", Action.ADMIN);
} }
@Override @Override
@ -1078,7 +1085,7 @@ public class AccessController extends BaseRegionObserver
public void preSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx, public void preSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor)
throws IOException { throws IOException {
requirePermission("snapshot", Permission.Action.ADMIN); requirePermission("snapshot", Action.ADMIN);
} }
@Override @Override
@ -1091,7 +1098,7 @@ public class AccessController extends BaseRegionObserver
public void preCloneSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx, public void preCloneSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor)
throws IOException { throws IOException {
requirePermission("clone", Permission.Action.ADMIN); requirePermission("clone", Action.ADMIN);
} }
@Override @Override
@ -1104,7 +1111,7 @@ public class AccessController extends BaseRegionObserver
public void preRestoreSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx, public void preRestoreSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor) final SnapshotDescription snapshot, final HTableDescriptor hTableDescriptor)
throws IOException { throws IOException {
requirePermission("restore", Permission.Action.ADMIN); requirePermission("restore", Action.ADMIN);
} }
@Override @Override
@ -1116,7 +1123,7 @@ public class AccessController extends BaseRegionObserver
@Override @Override
public void preDeleteSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx, public void preDeleteSnapshot(final ObserverContext<MasterCoprocessorEnvironment> ctx,
final SnapshotDescription snapshot) throws IOException { final SnapshotDescription snapshot) throws IOException {
requirePermission("deleteSnapshot", Permission.Action.ADMIN); requirePermission("deleteSnapshot", Action.ADMIN);
} }
@Override @Override
@ -1254,20 +1261,131 @@ public class AccessController extends BaseRegionObserver
final byte [] row, final byte [] family, final Result result) final byte [] row, final byte [] family, final Result result)
throws IOException { throws IOException {
assert family != null; assert family != null;
requireCoveringPermission(OpType.GET_CLOSEST_ROW_BEFORE, c.getEnvironment(), row, RegionCoprocessorEnvironment env = c.getEnvironment();
makeFamilyMap(family, null), HConstants.LATEST_TIMESTAMP, Permission.Action.READ); 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 @Override
public void preGetOp(final ObserverContext<RegionCoprocessorEnvironment> c, public void preGetOp(final ObserverContext<RegionCoprocessorEnvironment> c,
final Get get, final List<Cell> result) throws IOException { final Get get, final List<Cell> result) throws IOException {
internalPreRead(c, get); internalPreRead(c, get, OpType.GET);
} }
@Override @Override
public boolean preExists(final ObserverContext<RegionCoprocessorEnvironment> c, public boolean preExists(final ObserverContext<RegionCoprocessorEnvironment> c,
final Get get, final boolean exists) throws IOException { final Get get, final boolean exists) throws IOException {
internalPreRead(c, get); internalPreRead(c, get, OpType.EXISTS);
return exists; return exists;
} }
@ -1281,11 +1399,22 @@ public class AccessController extends BaseRegionObserver
// HBase value. A new ACL in a new Put applies to that Put. It doesn't // 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 // change the ACL of any previous Put. This allows simple evolution of
// security policy over time without requiring expensive updates. // security policy over time without requiring expensive updates.
requireCoveringPermission(OpType.PUT, c.getEnvironment(), put.getRow(), RegionCoprocessorEnvironment env = c.getEnvironment();
put.getFamilyCellMap(), put.getTimeStamp(), Permission.Action.WRITE); 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); byte[] bytes = put.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL);
if (bytes != null) { if (bytes != null) {
if (canPersistCellACLs) { if (cellFeaturesEnabled) {
addCellPermissions(bytes, put.getFamilyCellMap()); addCellPermissions(bytes, put.getFamilyCellMap());
} else { } else {
throw new DoNotRetryIOException("Cell ACLs cannot be persisted"); throw new DoNotRetryIOException("Cell ACLs cannot be persisted");
@ -1314,8 +1443,19 @@ public class AccessController extends BaseRegionObserver
// compaction could remove them. If the user doesn't have permission to // compaction could remove them. If the user doesn't have permission to
// overwrite any of the visible versions ('visible' defined as not covered // overwrite any of the visible versions ('visible' defined as not covered
// by a tombstone already) then we have to disallow this operation. // by a tombstone already) then we have to disallow this operation.
requireCoveringPermission(OpType.DELETE, c.getEnvironment(), delete.getRow(), RegionCoprocessorEnvironment env = c.getEnvironment();
delete.getFamilyCellMap(), delete.getTimeStamp(), Action.WRITE); 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 @Override
@ -1334,11 +1474,23 @@ public class AccessController extends BaseRegionObserver
final ByteArrayComparable comparator, final Put put, final ByteArrayComparable comparator, final Put put,
final boolean result) throws IOException { final boolean result) throws IOException {
// Require READ and WRITE permissions on the table, CF, and KV to update // Require READ and WRITE permissions on the table, CF, and KV to update
requireCoveringPermission(OpType.CHECK_AND_PUT, c.getEnvironment(), row, RegionCoprocessorEnvironment env = c.getEnvironment();
makeFamilyMap(family, qualifier), HConstants.LATEST_TIMESTAMP, Action.READ, Action.WRITE); 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, 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); byte[] bytes = put.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL);
if (bytes != null) { if (bytes != null) {
if (canPersistCellACLs) { if (cellFeaturesEnabled) {
addCellPermissions(bytes, put.getFamilyCellMap()); addCellPermissions(bytes, put.getFamilyCellMap());
} else { } else {
throw new DoNotRetryIOException("Cell ACLs cannot be persisted"); throw new DoNotRetryIOException("Cell ACLs cannot be persisted");
@ -1360,8 +1512,20 @@ public class AccessController extends BaseRegionObserver
} }
// Require READ and WRITE permissions on the table, CF, and the KV covered // Require READ and WRITE permissions on the table, CF, and the KV covered
// by the delete // by the delete
requireCoveringPermission(OpType.CHECK_AND_DELETE, c.getEnvironment(), row, RegionCoprocessorEnvironment env = c.getEnvironment();
makeFamilyMap(family, qualifier), HConstants.LATEST_TIMESTAMP, Action.READ, Action.WRITE); 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, Action.WRITE));
authResult.setReason("Covering cell set");
}
logResult(authResult);
if (!authResult.isAllowed()) {
throw new AccessDeniedException("Insufficient permissions " + authResult.toContextString());
}
return result; return result;
} }
@ -1372,8 +1536,20 @@ public class AccessController extends BaseRegionObserver
throws IOException { throws IOException {
// Require WRITE permission to the table, CF, and the KV to be replaced by the // Require WRITE permission to the table, CF, and the KV to be replaced by the
// incremented value // incremented value
requireCoveringPermission(OpType.INCREMENT_COLUMN_VALUE, c.getEnvironment(), row, RegionCoprocessorEnvironment env = c.getEnvironment();
makeFamilyMap(family, qualifier), HConstants.LATEST_TIMESTAMP, Action.WRITE); 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; return -1;
} }
@ -1381,11 +1557,22 @@ public class AccessController extends BaseRegionObserver
public Result preAppend(ObserverContext<RegionCoprocessorEnvironment> c, Append append) public Result preAppend(ObserverContext<RegionCoprocessorEnvironment> c, Append append)
throws IOException { throws IOException {
// Require WRITE permission to the table, CF, and the KV to be appended // Require WRITE permission to the table, CF, and the KV to be appended
requireCoveringPermission(OpType.APPEND, c.getEnvironment(), append.getRow(), RegionCoprocessorEnvironment env = c.getEnvironment();
append.getFamilyCellMap(), HConstants.LATEST_TIMESTAMP, Action.WRITE); 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); byte[] bytes = append.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL);
if (bytes != null) { if (bytes != null) {
if (canPersistCellACLs) { if (cellFeaturesEnabled) {
addCellPermissions(bytes, append.getFamilyCellMap()); addCellPermissions(bytes, append.getFamilyCellMap());
} else { } else {
throw new DoNotRetryIOException("Cell ACLs cannot be persisted"); throw new DoNotRetryIOException("Cell ACLs cannot be persisted");
@ -1400,11 +1587,23 @@ public class AccessController extends BaseRegionObserver
throws IOException { throws IOException {
// Require WRITE permission to the table, CF, and the KV to be replaced by // Require WRITE permission to the table, CF, and the KV to be replaced by
// the incremented value // the incremented value
requireCoveringPermission(OpType.INCREMENT, c.getEnvironment(), increment.getRow(), RegionCoprocessorEnvironment env = c.getEnvironment();
increment.getFamilyCellMap(), increment.getTimeRange().getMax(), Action.WRITE); 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.APPEND, 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); byte[] bytes = increment.getAttribute(AccessControlConstants.OP_ATTRIBUTE_ACL);
if (bytes != null) { if (bytes != null) {
if (canPersistCellACLs) { if (cellFeaturesEnabled) {
addCellPermissions(bytes, increment.getFamilyCellMap()); addCellPermissions(bytes, increment.getFamilyCellMap());
} else { } else {
throw new DoNotRetryIOException("Cell ACLs cannot be persisted"); throw new DoNotRetryIOException("Cell ACLs cannot be persisted");
@ -1418,7 +1617,7 @@ public class AccessController extends BaseRegionObserver
MutationType opType, Mutation mutation, Cell oldCell, Cell newCell) throws IOException { MutationType opType, Mutation mutation, Cell oldCell, Cell newCell) throws IOException {
// If the HFile version is insufficient to persist tags, we won't have any // If the HFile version is insufficient to persist tags, we won't have any
// work to do here // work to do here
if (!canPersistCellACLs) { if (!cellFeaturesEnabled) {
return newCell; return newCell;
} }
@ -1486,7 +1685,7 @@ public class AccessController extends BaseRegionObserver
@Override @Override
public RegionScanner preScannerOpen(final ObserverContext<RegionCoprocessorEnvironment> c, public RegionScanner preScannerOpen(final ObserverContext<RegionCoprocessorEnvironment> c,
final Scan scan, final RegionScanner s) throws IOException { final Scan scan, final RegionScanner s) throws IOException {
internalPreRead(c, scan); internalPreRead(c, scan, OpType.SCAN);
return s; return s;
} }
@ -1563,7 +1762,7 @@ public class AccessController extends BaseRegionObserver
if (!authResult.isAllowed()) { if (!authResult.isAllowed()) {
for(UserPermission userPerm: for(UserPermission userPerm:
AccessControlLists.getUserTablePermissions(regionEnv.getConfiguration(), tableName)) { AccessControlLists.getUserTablePermissions(regionEnv.getConfiguration(), tableName)) {
for(Permission.Action userAction: userPerm.getActions()) { for(Action userAction: userPerm.getActions()) {
if(userAction.equals(action)) { if(userAction.equals(action)) {
return AuthResult.allow(method, "Access allowed", requestUser, return AuthResult.allow(method, "Access allowed", requestUser,
action, tableName, null, null); action, tableName, null, null);
@ -1613,7 +1812,7 @@ public class AccessController extends BaseRegionObserver
Service service, String methodName, Message request) throws IOException { Service service, String methodName, Message request) throws IOException {
// Don't intercept calls to our own AccessControlService, we check for // Don't intercept calls to our own AccessControlService, we check for
// appropriate permissions in the service handlers // appropriate permissions in the service handlers
if (shouldCheckExecPermissions && !(service instanceof AccessControlService)) { if (shouldCheckExecPermission && !(service instanceof AccessControlService)) {
requirePermission("invoke(" + service.getDescriptorForType().getName() + "." + requirePermission("invoke(" + service.getDescriptorForType().getName() + "." +
methodName + ")", methodName + ")",
getTableName(ctx.getEnvironment()), null, null, getTableName(ctx.getEnvironment()), null, null,
@ -1769,7 +1968,7 @@ public class AccessController extends BaseRegionObserver
for (Permission permission : permissions) { for (Permission permission : permissions) {
if (permission instanceof TablePermission) { if (permission instanceof TablePermission) {
TablePermission tperm = (TablePermission) permission; TablePermission tperm = (TablePermission) permission;
for (Permission.Action action : permission.getActions()) { for (Action action : permission.getActions()) {
if (!tperm.getTableName().equals(tableName)) { if (!tperm.getTableName().equals(tableName)) {
throw new CoprocessorException(AccessController.class, String.format("This method " throw new CoprocessorException(AccessController.class, String.format("This method "
+ "can only execute at the table specified in TablePermission. " + + "can only execute at the table specified in TablePermission. " +
@ -1792,7 +1991,7 @@ public class AccessController extends BaseRegionObserver
} }
} else { } else {
for (Permission.Action action : permission.getActions()) { for (Action action : permission.getActions()) {
requirePermission("checkPermissions", action); requirePermission("checkPermissions", action);
} }
} }
@ -1815,17 +2014,19 @@ public class AccessController extends BaseRegionObserver
private TableName getTableName(RegionCoprocessorEnvironment e) { private TableName getTableName(RegionCoprocessorEnvironment e) {
HRegion region = e.getRegion(); HRegion region = e.getRegion();
TableName tableName = null;
if (region != null) { if (region != null) {
return getTableName(region);
}
return null;
}
private TableName getTableName(HRegion region) {
HRegionInfo regionInfo = region.getRegionInfo(); HRegionInfo regionInfo = region.getRegionInfo();
if (regionInfo != null) { if (regionInfo != null) {
tableName = regionInfo.getTable(); return regionInfo.getTable();
} }
return null;
} }
return tableName;
}
@Override @Override
public void preClose(ObserverContext<RegionCoprocessorEnvironment> e, boolean abortRequested) public void preClose(ObserverContext<RegionCoprocessorEnvironment> e, boolean abortRequested)
@ -1855,7 +2056,7 @@ public class AccessController extends BaseRegionObserver
public void preStopRegionServer( public void preStopRegionServer(
ObserverContext<RegionServerCoprocessorEnvironment> env) ObserverContext<RegionServerCoprocessorEnvironment> env)
throws IOException { throws IOException {
requirePermission("preStopRegionServer", Permission.Action.ADMIN); requirePermission("preStopRegionServer", Action.ADMIN);
} }
private Map<byte[], ? extends Collection<byte[]>> makeFamilyMap(byte[] family, private Map<byte[], ? extends Collection<byte[]>> makeFamilyMap(byte[] family,
@ -1876,7 +2077,7 @@ public class AccessController extends BaseRegionObserver
// If the list is empty, this is a request for all table descriptors and requires GLOBAL // If the list is empty, this is a request for all table descriptors and requires GLOBAL
// ADMIN privs. // ADMIN privs.
if (tableNamesList == null || tableNamesList.isEmpty()) { if (tableNamesList == null || tableNamesList.isEmpty()) {
requireGlobalPermission("getTableDescriptors", Permission.Action.ADMIN, null, null); requireGlobalPermission("getTableDescriptors", Action.ADMIN, null, null);
} }
// Otherwise, if the requestor has ADMIN or CREATE privs for all listed tables, the // Otherwise, if the requestor has ADMIN or CREATE privs for all listed tables, the
// request can be granted. // request can be granted.
@ -1893,7 +2094,7 @@ public class AccessController extends BaseRegionObserver
// We don't care about this // We don't care about this
} }
requirePermission("getTableDescriptors", tableName, null, null, requirePermission("getTableDescriptors", tableName, null, null,
Permission.Action.ADMIN, Permission.Action.CREATE); Action.ADMIN, Action.CREATE);
} }
} }
} }

View File

@ -33,12 +33,12 @@ import org.apache.hadoop.hbase.util.Bytes;
*/ */
@InterfaceAudience.Private @InterfaceAudience.Private
public class AuthResult { public class AuthResult {
private final boolean allowed; private boolean allowed;
private final String namespace; private final String namespace;
private final TableName table; private final TableName table;
private final Permission.Action action; private final Permission.Action action;
private final String request; private final String request;
private final String reason; private String reason;
private final User user; private final User user;
// "family" and "qualifier" should only be used if "families" is null. // "family" and "qualifier" should only be used if "families" is null.
@ -121,6 +121,14 @@ public class AuthResult {
return request; return request;
} }
public void setAllowed(boolean allowed) {
this.allowed = allowed;
}
public void setReason(String reason) {
this.reason = reason;
}
String toFamilyString() { String toFamilyString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (families != null) { if (families != null) {

View File

@ -28,7 +28,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.exceptions.DeserializationException; import org.apache.hadoop.hbase.exceptions.DeserializationException;
import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.User;
@ -348,12 +347,14 @@ public class TableAuthManager {
return false; return false;
} }
private boolean checkCellPermissions(User user, Cell cell, Permission.Action action) { /**
* Authorize a user for a given KV. This is called from AccessControlFilter.
*/
public boolean authorize(User user, TableName table, Cell cell, Permission.Action action) {
try { try {
List<Permission> perms = AccessControlLists.getCellPermissionsForUser(user, cell); List<Permission> perms = AccessControlLists.getCellPermissionsForUser(user, cell);
if (LOG.isTraceEnabled()) { if (LOG.isTraceEnabled()) {
LOG.trace("Perms for user " + user.getShortName() + " in cell " + LOG.trace("Perms for user " + user.getShortName() + " in cell " + cell + ": " + perms);
cell + ": " + perms);
} }
for (Permission p: perms) { for (Permission p: perms) {
if (p.implies(action)) { if (p.implies(action)) {
@ -369,46 +370,6 @@ public class TableAuthManager {
return false; return false;
} }
private boolean checkTableColumnPermissions(User user, TableName table, Cell cell,
Permission.Action action) {
// TODO: Do not clone here
byte[] family = CellUtil.cloneFamily(cell);
byte[] qualifier = CellUtil.cloneQualifier(cell);
// User is authorized at table or CF level
if (authorizeUser(user, table, family, qualifier, action)) {
return true;
}
String groupNames[] = user.getGroupNames();
if (groupNames != null) {
for (String group: groupNames) {
// TODO: authorizeGroup should check qualifier too?
// Group is authorized at table or CF level
if (authorizeGroup(group, table, family, action)) {
return true;
}
}
}
return false;
}
/**
* Authorize a user for a given KV. This is called from AccessControlFilter.
*/
public boolean authorize(User user, TableName table, Cell cell, boolean cellFirstStrategy,
Permission.Action action) {
if (cellFirstStrategy) {
if (checkCellPermissions(user, cell, action)) {
return true;
}
return checkTableColumnPermissions(user, table, cell, action);
} else {
if (checkTableColumnPermissions(user, table, cell, action)) {
return true;
}
return checkCellPermissions(user, cell, action);
}
}
public boolean authorize(User user, String namespace, Permission.Action action) { public boolean authorize(User user, String namespace, Permission.Action action) {
// Global authorizations supercede namespace level // Global authorizations supercede namespace level
if (authorizeUser(user, action)) { if (authorizeUser(user, action)) {

View File

@ -41,6 +41,7 @@ import org.apache.hadoop.hbase.Waiter.Predicate;
import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException; import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
import org.apache.hadoop.hbase.io.hfile.HFile;
import org.apache.hadoop.hbase.protobuf.ProtobufUtil; import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.AccessControlService; import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.AccessControlService;
import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.CheckPermissionsRequest; import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.CheckPermissionsRequest;
@ -84,7 +85,7 @@ public class SecureTestUtil {
} }
conf.set("hbase.superuser", sb.toString()); conf.set("hbase.superuser", sb.toString());
// Need HFile V3 for tags for security features // Need HFile V3 for tags for security features
conf.setInt("hfile.format.version", 3); conf.setInt(HFile.FORMAT_VERSION_KEY, 3);
} }
public static void verifyConfiguration(Configuration conf) { public static void verifyConfiguration(Configuration conf) {
@ -96,9 +97,12 @@ public class SecureTestUtil {
AccessController.class.getName()))) { AccessController.class.getName()))) {
throw new RuntimeException("AccessController is missing from a system coprocessor list"); throw new RuntimeException("AccessController is missing from a system coprocessor list");
} }
if (conf.getInt(HFile.FORMAT_VERSION_KEY, 2) < HFile.MIN_FORMAT_VERSION_WITH_TAGS) {
throw new RuntimeException("Post 0.96 security features require HFile version >= 3");
}
} }
public void checkTablePerms(Configuration conf, byte[] table, byte[] family, byte[] column, public static void checkTablePerms(Configuration conf, byte[] table, byte[] family, byte[] column,
Permission.Action... actions) throws IOException { Permission.Action... actions) throws IOException {
Permission[] perms = new Permission[actions.length]; Permission[] perms = new Permission[actions.length];
for (int i = 0; i < actions.length; i++) { for (int i = 0; i < actions.length; i++) {
@ -108,7 +112,7 @@ public class SecureTestUtil {
checkTablePerms(conf, table, perms); checkTablePerms(conf, table, perms);
} }
public void checkTablePerms(Configuration conf, byte[] table, Permission... perms) throws IOException { public static void checkTablePerms(Configuration conf, byte[] table, Permission... perms) throws IOException {
CheckPermissionsRequest.Builder request = CheckPermissionsRequest.newBuilder(); CheckPermissionsRequest.Builder request = CheckPermissionsRequest.newBuilder();
for (Permission p : perms) { for (Permission p : perms) {
request.addPermission(ProtobufUtil.toPermission(p)); request.addPermission(ProtobufUtil.toPermission(p));
@ -139,7 +143,7 @@ public class SecureTestUtil {
*/ */
static interface AccessTestAction extends PrivilegedExceptionAction<Object> { } static interface AccessTestAction extends PrivilegedExceptionAction<Object> { }
public void verifyAllowed(User user, AccessTestAction... actions) throws Exception { public static void verifyAllowed(User user, AccessTestAction... actions) throws Exception {
for (AccessTestAction action : actions) { for (AccessTestAction action : actions) {
try { try {
Object obj = user.runAs(action); Object obj = user.runAs(action);
@ -155,13 +159,13 @@ public class SecureTestUtil {
} }
} }
public void verifyAllowed(AccessTestAction action, User... users) throws Exception { public static void verifyAllowed(AccessTestAction action, User... users) throws Exception {
for (User user : users) { for (User user : users) {
verifyAllowed(user, action); verifyAllowed(user, action);
} }
} }
public void verifyAllowed(User user, AccessTestAction action, int count) throws Exception { public static void verifyAllowed(User user, AccessTestAction action, int count) throws Exception {
try { try {
Object obj = user.runAs(action); Object obj = user.runAs(action);
if (obj != null && obj instanceof List<?>) { if (obj != null && obj instanceof List<?>) {
@ -176,14 +180,34 @@ public class SecureTestUtil {
} }
} }
public void verifyDenied(User user, AccessTestAction... actions) throws Exception { public static void verifyDeniedWithException(User user, AccessTestAction... actions)
throws Exception {
verifyDenied(user, true, actions);
}
public static void verifyDeniedWithException(AccessTestAction action, User... users)
throws Exception {
for (User user : users) {
verifyDenied(user, true, action);
}
}
public static void verifyDenied(User user, AccessTestAction... actions) throws Exception {
verifyDenied(user, false, actions);
}
public static void verifyDenied(User user, boolean requireException,
AccessTestAction... actions) throws Exception {
for (AccessTestAction action : actions) { for (AccessTestAction action : actions) {
try { try {
Object obj = user.runAs(action); Object obj = user.runAs(action);
if (requireException) {
fail("Expected exception was not thrown for user '" + user.getShortName() + "'");
}
if (obj != null && obj instanceof List<?>) { if (obj != null && obj instanceof List<?>) {
List<?> results = (List<?>) obj; List<?> results = (List<?>) obj;
if (results != null && !results.isEmpty()) { if (results != null && !results.isEmpty()) {
fail("Expected no results for user '" + user.getShortName() + "'"); fail("Unexpected results for user '" + user.getShortName() + "'");
} }
} }
} catch (IOException e) { } catch (IOException e) {
@ -211,7 +235,7 @@ public class SecureTestUtil {
} while((ex = ex.getCause()) != null); } while((ex = ex.getCause()) != null);
} }
if (!isAccessDeniedException) { if (!isAccessDeniedException) {
fail("Not receiving AccessDeniedException for user '" + user.getShortName() + "'"); fail("Expected exception was not thrown for user '" + user.getShortName() + "'");
} }
} catch (UndeclaredThrowableException ute) { } catch (UndeclaredThrowableException ute) {
// TODO why we get a PrivilegedActionException, which is unexpected? // TODO why we get a PrivilegedActionException, which is unexpected?
@ -226,12 +250,12 @@ public class SecureTestUtil {
return; return;
} }
} }
fail("Not receiving AccessDeniedException for user '" + user.getShortName() + "'"); fail("Expected exception was not thrown for user '" + user.getShortName() + "'");
} }
} }
} }
public void verifyDenied(AccessTestAction action, User... users) throws Exception { public static void verifyDenied(AccessTestAction action, User... users) throws Exception {
for (User user : users) { for (User user : users) {
verifyDenied(user, action); verifyDenied(user, action);
} }

View File

@ -47,7 +47,7 @@ import org.junit.experimental.categories.Category;
import org.junit.rules.TestName; import org.junit.rules.TestName;
@Category(LargeTests.class) @Category(LargeTests.class)
public class TestAccessControlFilter { public class TestAccessControlFilter extends SecureTestUtil {
@Rule public TestName name = new TestName(); @Rule public TestName name = new TestName();
private static HBaseTestingUtility TEST_UTIL; private static HBaseTestingUtility TEST_UTIL;
@ -69,12 +69,14 @@ public class TestAccessControlFilter {
public static void setupBeforeClass() throws Exception { public static void setupBeforeClass() throws Exception {
TEST_UTIL = new HBaseTestingUtility(); TEST_UTIL = new HBaseTestingUtility();
Configuration conf = TEST_UTIL.getConfiguration(); Configuration conf = TEST_UTIL.getConfiguration();
SecureTestUtil.enableSecurity(conf); enableSecurity(conf);
String baseuser = User.getCurrent().getShortName(); verifyConfiguration(conf);
conf.set("hbase.superuser", conf.get("hbase.superuser", "") +
String.format(",%s.hfs.0,%s.hfs.1,%s.hfs.2", baseuser, baseuser, baseuser)); // We expect 0.98 scanning semantics
conf.setBoolean(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT, false);
TEST_UTIL.startMiniCluster(); TEST_UTIL.startMiniCluster();
TEST_UTIL.waitTableEnabled(AccessControlLists.ACL_TABLE_NAME.getName()); TEST_UTIL.waitTableEnabled(AccessControlLists.ACL_TABLE_NAME.getName(), 50000);
READER = User.createUserForTesting(conf, "reader", new String[0]); READER = User.createUserForTesting(conf, "reader", new String[0]);
LIMITED = User.createUserForTesting(conf, "limited", new String[0]); LIMITED = User.createUserForTesting(conf, "limited", new String[0]);

View File

@ -170,7 +170,7 @@ public class TestAccessController extends SecureTestUtil {
verifyConfiguration(conf); verifyConfiguration(conf);
// Enable EXEC permission checking // Enable EXEC permission checking
conf.setBoolean(AccessController.EXEC_PERMISSION_CHECKS_KEY, true); conf.setBoolean(AccessControlConstants.EXEC_PERMISSION_CHECKS_KEY, true);
TEST_UTIL.startMiniCluster(); TEST_UTIL.startMiniCluster();
MasterCoprocessorHost cpHost = MasterCoprocessorHost cpHost =

View File

@ -45,10 +45,8 @@ import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.hbase.util.TestTableName; import org.apache.hadoop.hbase.util.TestTableName;
import org.apache.log4j.Level; import org.apache.log4j.Level;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
@ -89,18 +87,13 @@ public class TestCellACLWithMultipleVersions extends SecureTestUtil {
public static void setupBeforeClass() throws Exception { public static void setupBeforeClass() throws Exception {
// setup configuration // setup configuration
conf = TEST_UTIL.getConfiguration(); conf = TEST_UTIL.getConfiguration();
conf.set("hbase.master.hfilecleaner.plugins",
"org.apache.hadoop.hbase.master.cleaner.HFileLinkCleaner,"
+ "org.apache.hadoop.hbase.master.snapshot.SnapshotHFileCleaner");
conf.set("hbase.master.logcleaner.plugins",
"org.apache.hadoop.hbase.master.snapshot.SnapshotLogCleaner");
// Enable security // Enable security
enableSecurity(conf); enableSecurity(conf);
// Verify enableSecurity sets up what we require // Verify enableSecurity sets up what we require
verifyConfiguration(conf); verifyConfiguration(conf);
// Enable EXEC permission checking // We expect 0.98 cell ACL semantics
conf.setBoolean(AccessController.EXEC_PERMISSION_CHECKS_KEY, true); conf.setBoolean(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT, false);
TEST_UTIL.startMiniCluster(); TEST_UTIL.startMiniCluster();
MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster().getMaster() MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster().getMaster()

View File

@ -43,12 +43,11 @@ import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.master.MasterCoprocessorHost; import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost; import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost;
import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.access.Permission.Action;
import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.TestTableName; import org.apache.hadoop.hbase.util.TestTableName;
import org.apache.log4j.Level; import org.apache.log4j.Level;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
@ -79,6 +78,7 @@ public class TestCellACLs extends SecureTestUtil {
private static final byte[] TEST_Q3 = Bytes.toBytes("q3"); private static final byte[] TEST_Q3 = Bytes.toBytes("q3");
private static final byte[] TEST_Q4 = Bytes.toBytes("q4"); private static final byte[] TEST_Q4 = Bytes.toBytes("q4");
private static final byte[] ZERO = Bytes.toBytes(0L); private static final byte[] ZERO = Bytes.toBytes(0L);
private static final byte[] ONE = Bytes.toBytes(1L);
private static Configuration conf; private static Configuration conf;
@ -89,18 +89,13 @@ public class TestCellACLs extends SecureTestUtil {
public static void setupBeforeClass() throws Exception { public static void setupBeforeClass() throws Exception {
// setup configuration // setup configuration
conf = TEST_UTIL.getConfiguration(); conf = TEST_UTIL.getConfiguration();
conf.set("hbase.master.hfilecleaner.plugins",
"org.apache.hadoop.hbase.master.cleaner.HFileLinkCleaner,"
+ "org.apache.hadoop.hbase.master.snapshot.SnapshotHFileCleaner");
conf.set("hbase.master.logcleaner.plugins",
"org.apache.hadoop.hbase.master.snapshot.SnapshotLogCleaner");
// Enable security // Enable security
enableSecurity(conf); enableSecurity(conf);
// Verify enableSecurity sets up what we require // Verify enableSecurity sets up what we require
verifyConfiguration(conf); verifyConfiguration(conf);
// Enable EXEC permission checking // We expect 0.98 cell ACL semantics
conf.setBoolean(AccessController.EXEC_PERMISSION_CHECKS_KEY, true); conf.setBoolean(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT, false);
TEST_UTIL.startMiniCluster(); TEST_UTIL.startMiniCluster();
MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster().getMaster() MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster().getMaster()
@ -150,12 +145,11 @@ public class TestCellACLs extends SecureTestUtil {
Put p; Put p;
// with ro ACL // with ro ACL
p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q1, ZERO); p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q1, ZERO);
p.setACL(USER_OTHER.getShortName(), new Permission(Permission.Action.READ)); p.setACL(USER_OTHER.getShortName(), new Permission(Action.READ));
t.put(p); t.put(p);
// with rw ACL // with rw ACL
p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q2, ZERO); p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q2, ZERO);
p.setACL(USER_OTHER.getShortName(), new Permission(Permission.Action.READ, p.setACL(USER_OTHER.getShortName(), new Permission(Action.READ, Action.WRITE));
Permission.Action.WRITE));
t.put(p); t.put(p);
// no ACL // no ACL
p = new Put(TEST_ROW) p = new Put(TEST_ROW)
@ -308,7 +302,7 @@ public class TestCellACLs extends SecureTestUtil {
public Object run() throws Exception { public Object run() throws Exception {
Increment i = new Increment(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q2, 1L); Increment i = new Increment(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q2, 1L);
// Tag this increment with an ACL that denies write permissions to USER_OTHER // Tag this increment with an ACL that denies write permissions to USER_OTHER
i.setACL(USER_OTHER.getShortName(), new Permission(Permission.Action.READ)); i.setACL(USER_OTHER.getShortName(), new Permission(Action.READ));
HTable t = new HTable(conf, TEST_TABLE.getTableName()); HTable t = new HTable(conf, TEST_TABLE.getTableName());
try { try {
t.increment(i); t.increment(i);
@ -379,6 +373,164 @@ public class TestCellACLs extends SecureTestUtil {
verifyAllowed(deleteQ1, USER_OWNER); verifyAllowed(deleteQ1, USER_OWNER);
} }
/**
* Insure we are not granting access in the absence of any cells found
* when scanning for covered cells.
*/
@Test
public void testCoveringCheck() throws Exception {
// Grant read access to USER_OTHER
grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(),
TEST_FAMILY, null, Action.READ);
// A write by USER_OTHER should be denied.
// This is where we could have a big problem if there is an error in the
// covering check logic.
verifyDenied(new AccessTestAction() {
@Override
public Object run() throws Exception {
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Put p;
p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q1, ZERO);
t.put(p);
} finally {
t.close();
}
return null;
}
}, USER_OTHER);
// Add the cell
verifyAllowed(new AccessTestAction() {
@Override
public Object run() throws Exception {
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Put p;
p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q1, ZERO);
t.put(p);
} finally {
t.close();
}
return null;
}
}, USER_OWNER);
// A write by USER_OTHER should still be denied, just to make sure
verifyDenied(new AccessTestAction() {
@Override
public Object run() throws Exception {
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Put p;
p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q1, ONE);
t.put(p);
} finally {
t.close();
}
return null;
}
}, USER_OTHER);
// A read by USER_OTHER should be allowed, just to make sure
verifyAllowed(new AccessTestAction() {
@Override
public Object run() throws Exception {
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
return t.get(new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1));
} finally {
t.close();
}
}
}, USER_OTHER);
}
@Test
public void testCellStrategy() throws Exception {
// Set up our test actions
AccessTestAction readQ1Default = new AccessTestAction() {
@Override
public Object run() throws Exception {
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
return t.get(new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1));
} finally {
t.close();
}
}
};
AccessTestAction readQ2Default = new AccessTestAction() {
@Override
public Object run() throws Exception {
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
return t.get(new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q2));
} finally {
t.close();
}
}
};
AccessTestAction readQ1CellFirst = new AccessTestAction() {
@Override
public Object run() throws Exception {
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Get get = new Get(TEST_ROW).addColumn(TEST_FAMILY, TEST_Q1);
get.setACLStrategy(true);
return t.get(get);
} finally {
t.close();
}
}
};
// Add test data
verifyAllowed(new AccessTestAction() {
@Override
public Object run() throws Exception {
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Put p;
// The empty permission set on Q1
p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q1, ZERO);
p.setACL(USER_OTHER.getShortName(), new Permission());
t.put(p);
// Read permissions on Q2
p = new Put(TEST_ROW).add(TEST_FAMILY, TEST_Q2, ZERO);
p.setACL(USER_OTHER.getShortName(), new Permission(Action.READ));
t.put(p);
} finally {
t.close();
}
return null;
}
}, USER_OWNER);
// A read by USER_OTHER will be denied with the default cell strategy as
// there is no visibility without a grant and a cell ACL giving
// explicit permission
verifyDenied(readQ1Default, USER_OTHER);
// A read will be allowed by the default cell strategy if there is a cell
// ACL giving explicit permission.
verifyAllowed(readQ2Default, USER_OTHER);
// Grant read access to USER_OTHER
grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(),
TEST_FAMILY, null, Action.READ);
// A read by USER_OTHER will now be allowed with the default cell strategy
// because we have a CF level grant and we take the union of permissions.
verifyAllowed(readQ1Default, USER_OTHER);
// A read by USER_OTHER will be denied with the cell first strategy
// because the empty perm set for USER_OTHER in the cell ACL there
// revokes access.
verifyDenied(readQ1CellFirst, USER_OTHER);
}
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
// Clean the _acl_ table // Clean the _acl_ table

View File

@ -0,0 +1,299 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.security.access;
import static org.junit.Assert.*;
import java.util.UUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Coprocessor;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.MediumTests;
import org.apache.hadoop.hbase.TableNotFoundException;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.access.Permission.Action;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.TestTableName;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
@Category(MediumTests.class)
public class TestScanEarlyTermination extends SecureTestUtil {
private static final Log LOG = LogFactory.getLog(TestScanEarlyTermination.class);
static {
Logger.getLogger(AccessController.class).setLevel(Level.TRACE);
Logger.getLogger(AccessControlFilter.class).setLevel(Level.TRACE);
Logger.getLogger(TableAuthManager.class).setLevel(Level.TRACE);
}
@Rule
public TestTableName TEST_TABLE = new TestTableName();
private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
private static final byte[] TEST_FAMILY1 = Bytes.toBytes("f1");
private static final byte[] TEST_FAMILY2 = Bytes.toBytes("f2");
private static final byte[] TEST_ROW = Bytes.toBytes("testrow");
private static final byte[] TEST_Q1 = Bytes.toBytes("q1");
private static final byte[] TEST_Q2 = Bytes.toBytes("q2");
private static final byte[] ZERO = Bytes.toBytes(0L);
private static Configuration conf;
private static User USER_OWNER;
private static User USER_OTHER;
@BeforeClass
public static void setupBeforeClass() throws Exception {
// setup configuration
conf = TEST_UTIL.getConfiguration();
// Enable security
enableSecurity(conf);
// Verify enableSecurity sets up what we require
verifyConfiguration(conf);
TEST_UTIL.startMiniCluster();
MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster().getMaster()
.getMasterCoprocessorHost();
cpHost.load(AccessController.class, Coprocessor.PRIORITY_HIGHEST, conf);
AccessController ac = (AccessController)
cpHost.findCoprocessor(AccessController.class.getName());
cpHost.createEnvironment(AccessController.class, ac, Coprocessor.PRIORITY_HIGHEST, 1, conf);
RegionServerCoprocessorHost rsHost = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0)
.getRegionServerCoprocessorHost();
rsHost.createEnvironment(AccessController.class, ac, Coprocessor.PRIORITY_HIGHEST, 1, conf);
// Wait for the ACL table to become available
TEST_UTIL.waitTableEnabled(AccessControlLists.ACL_TABLE_NAME.getName());
// create a set of test users
USER_OWNER = User.createUserForTesting(conf, "owner", new String[0]);
USER_OTHER = User.createUserForTesting(conf, "other", new String[0]);
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
TEST_UTIL.shutdownMiniCluster();
}
@Before
public void setUp() throws Exception {
HBaseAdmin admin = TEST_UTIL.getHBaseAdmin();
HTableDescriptor htd = new HTableDescriptor(TEST_TABLE.getTableName());
htd.setOwner(USER_OWNER);
HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY1);
hcd.setMaxVersions(10);
htd.addFamily(hcd);
hcd = new HColumnDescriptor(TEST_FAMILY2);
hcd.setMaxVersions(10);
htd.addFamily(hcd);
// Enable backwards compatible early termination behavior in the HTD. We
// want to confirm that the per-table configuration is properly picked up.
htd.setConfiguration(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT, "true");
admin.createTable(htd);
TEST_UTIL.waitTableEnabled(TEST_TABLE.getTableName().getName());
}
@After
public void tearDown() throws Exception {
// Clean the _acl_ table
try {
TEST_UTIL.deleteTable(TEST_TABLE.getTableName());
} catch (TableNotFoundException ex) {
// Test deleted the table, no problem
LOG.info("Test deleted table " + TEST_TABLE.getTableName());
}
assertEquals(0, AccessControlLists.getTablePermissions(conf, TEST_TABLE.getTableName()).size());
}
@Test
public void testEarlyScanTermination() throws Exception {
// Grant USER_OTHER access to TEST_FAMILY1 only
grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(), TEST_FAMILY1,
null, Action.READ);
// Set up test data
verifyAllowed(new AccessTestAction() {
@Override
public Object run() throws Exception {
// force a new RS connection
conf.set("testkey", UUID.randomUUID().toString());
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Put put = new Put(TEST_ROW).add(TEST_FAMILY1, TEST_Q1, ZERO);
t.put(put);
// Set a READ cell ACL for USER_OTHER on this value in FAMILY2
put = new Put(TEST_ROW).add(TEST_FAMILY2, TEST_Q1, ZERO);
put.setACL(USER_OTHER.getShortName(), new Permission(Action.READ));
t.put(put);
// Set an empty cell ACL for USER_OTHER on this other value in FAMILY2
put = new Put(TEST_ROW).add(TEST_FAMILY2, TEST_Q2, ZERO);
put.setACL(USER_OTHER.getShortName(), new Permission());
t.put(put);
} finally {
t.close();
}
return null;
}
}, USER_OWNER);
// A scan of FAMILY1 will be allowed
verifyAllowed(new AccessTestAction() {
@Override
public Object run() throws Exception {
// force a new RS connection
conf.set("testkey", UUID.randomUUID().toString());
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Scan scan = new Scan().addFamily(TEST_FAMILY1);
Result result = t.getScanner(scan).next();
if (result != null) {
assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1));
assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1));
return result.listCells();
}
return null;
} finally {
t.close();
}
}
}, USER_OTHER);
// A scan of FAMILY1 and FAMILY2 will produce results for FAMILY1 without
// throwing an exception, however no cells from FAMILY2 will be returned
// because we early out checks at the CF level.
verifyAllowed(new AccessTestAction() {
@Override
public Object run() throws Exception {
// force a new RS connection
conf.set("testkey", UUID.randomUUID().toString());
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Scan scan = new Scan();
Result result = t.getScanner(scan).next();
if (result != null) {
assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1));
assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1));
return result.listCells();
}
return null;
} finally {
t.close();
}
}
}, USER_OTHER);
// A scan of FAMILY2 will throw an AccessDeniedException
verifyDeniedWithException(new AccessTestAction() {
@Override
public Object run() throws Exception {
// force a new RS connection
conf.set("testkey", UUID.randomUUID().toString());
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Scan scan = new Scan().addFamily(TEST_FAMILY2);
Result result = t.getScanner(scan).next();
if (result != null) {
return result.listCells();
}
return null;
} finally {
t.close();
}
}
}, USER_OTHER);
// Now grant USER_OTHER access to TEST_FAMILY2:TEST_Q2
grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(), TEST_FAMILY2,
TEST_Q2, Action.READ);
// A scan of FAMILY1 and FAMILY2 will produce combined results. In FAMILY2
// we have access granted to Q2 at the CF level. Because we early out
// checks at the CF level the cell ACL on Q1 also granting access is ignored.
verifyAllowed(new AccessTestAction() {
@Override
public Object run() throws Exception {
// force a new RS connection
conf.set("testkey", UUID.randomUUID().toString());
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Scan scan = new Scan();
Result result = t.getScanner(scan).next();
if (result != null) {
assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1));
assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1));
assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY2, TEST_Q2));
return result.listCells();
}
return null;
} finally {
t.close();
}
}
}, USER_OTHER);
// A scan of FAMILY1 and FAMILY2 will produce combined results. If we use
// a cell first strategy then cell ACLs come into effect. In FAMILY2, that
// cell ACL on Q1 now grants access and the empty permission set on Q2 now
// denies access.
verifyAllowed(new AccessTestAction() {
@Override
public Object run() throws Exception {
// force a new RS connection
conf.set("testkey", UUID.randomUUID().toString());
HTable t = new HTable(conf, TEST_TABLE.getTableName());
try {
Scan scan = new Scan();
scan.setACLStrategy(true);
Result result = t.getScanner(scan).next();
if (result != null) {
assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1));
assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1));
assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q2));
return result.listCells();
}
return null;
} finally {
t.close();
}
}
}, USER_OTHER);
}
}