HBASE-3025 Security: coprocessor based access control
git-svn-id: https://svn.apache.org/repos/asf/hbase/trunk@1203909 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
a2de91f85b
commit
cc7e469310
@ -802,6 +802,7 @@ Release 0.92.0 - Unreleased
|
|||||||
(Jonathan Hsieh)
|
(Jonathan Hsieh)
|
||||||
HBASE-4298 Support to drain RS nodes through ZK (Aravind Gottipati)
|
HBASE-4298 Support to drain RS nodes through ZK (Aravind Gottipati)
|
||||||
HBASE-2742 Provide strong authentication with a secure RPC engine
|
HBASE-2742 Provide strong authentication with a secure RPC engine
|
||||||
|
HBASE-3025 Coprocessor based access control
|
||||||
|
|
||||||
Release 0.90.5 - Unreleased
|
Release 0.90.5 - Unreleased
|
||||||
|
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.io.DataInput;
|
||||||
|
import java.io.DataOutput;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.hadoop.hbase.KeyValue;
|
||||||
|
import org.apache.hadoop.hbase.filter.FilterBase;
|
||||||
|
import org.apache.hadoop.hbase.security.User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <strong>NOTE: for internal use only by AccessController implementation</strong>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* TODO: There is room for further performance optimization here.
|
||||||
|
* Calling TableAuthManager.authorize() per KeyValue imposes a fair amount of
|
||||||
|
* overhead. A more optimized solution might look at the qualifiers where
|
||||||
|
* permissions are actually granted and explicitly limit the scan to those.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* We should aim to use this _only_ when access to the requested column families
|
||||||
|
* is not granted at the column family levels. If table or column family
|
||||||
|
* access succeeds, then there is no need to impose the overhead of this filter.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
class AccessControlFilter extends FilterBase {
|
||||||
|
|
||||||
|
private TableAuthManager authManager;
|
||||||
|
private byte[] table;
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For Writable
|
||||||
|
*/
|
||||||
|
AccessControlFilter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessControlFilter(TableAuthManager mgr, User ugi,
|
||||||
|
byte[] tableName) {
|
||||||
|
authManager = mgr;
|
||||||
|
table = tableName;
|
||||||
|
user = ugi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReturnCode filterKeyValue(KeyValue kv) {
|
||||||
|
if (authManager.authorize(user, table, kv, TablePermission.Action.READ)) {
|
||||||
|
return ReturnCode.INCLUDE;
|
||||||
|
}
|
||||||
|
return ReturnCode.NEXT_COL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(DataOutput dataOutput) throws IOException {
|
||||||
|
// no implementation, server-side use only
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"Serialization not supported. Intended for server-side use only.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFields(DataInput dataInput) throws IOException {
|
||||||
|
// no implementation, server-side use only
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"Serialization not supported. Intended for server-side use only.");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,514 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.common.collect.ArrayListMultimap;
|
||||||
|
import com.google.common.collect.ListMultimap;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.hbase.HColumnDescriptor;
|
||||||
|
import org.apache.hadoop.hbase.HConstants;
|
||||||
|
import org.apache.hadoop.hbase.HTableDescriptor;
|
||||||
|
import org.apache.hadoop.hbase.KeyValue;
|
||||||
|
import org.apache.hadoop.hbase.catalog.MetaReader;
|
||||||
|
import org.apache.hadoop.hbase.client.Delete;
|
||||||
|
import org.apache.hadoop.hbase.client.Get;
|
||||||
|
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.ResultScanner;
|
||||||
|
import org.apache.hadoop.hbase.client.Scan;
|
||||||
|
import org.apache.hadoop.hbase.io.HbaseObjectWritable;
|
||||||
|
import org.apache.hadoop.hbase.io.hfile.Compression;
|
||||||
|
import org.apache.hadoop.hbase.master.MasterServices;
|
||||||
|
import org.apache.hadoop.hbase.regionserver.HRegion;
|
||||||
|
import org.apache.hadoop.hbase.regionserver.InternalScanner;
|
||||||
|
import org.apache.hadoop.hbase.regionserver.StoreFile;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
import org.apache.hadoop.hbase.util.Pair;
|
||||||
|
import org.apache.hadoop.io.Text;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataInput;
|
||||||
|
import java.io.DataOutput;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains lists of permission grants to users and groups to allow for
|
||||||
|
* authorization checks by {@link AccessController}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Access control lists are stored in an "internal" metadata table named
|
||||||
|
* {@code _acl_}. Each table's permission grants are stored as a separate row,
|
||||||
|
* keyed by the table name. KeyValues for permissions assignments are stored
|
||||||
|
* in one of the formats:
|
||||||
|
* <pre>
|
||||||
|
* Key Desc
|
||||||
|
* -------- --------
|
||||||
|
* user table level permissions for a user [R=read, W=write]
|
||||||
|
* @group table level permissions for a group
|
||||||
|
* user,family column family level permissions for a user
|
||||||
|
* @group,family column family level permissions for a group
|
||||||
|
* user,family,qualifier column qualifier level permissions for a user
|
||||||
|
* @group,family,qualifier column qualifier level permissions for a group
|
||||||
|
* </pre>
|
||||||
|
* All values are encoded as byte arrays containing the codes from the
|
||||||
|
* {@link org.apache.hadoop.hbase.security.access.TablePermission.Action} enum.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class AccessControlLists {
|
||||||
|
/** Internal storage table for access control lists */
|
||||||
|
public static final String ACL_TABLE_NAME_STR = "_acl_";
|
||||||
|
public static final byte[] ACL_TABLE_NAME = Bytes.toBytes(ACL_TABLE_NAME_STR);
|
||||||
|
/** Column family used to store ACL grants */
|
||||||
|
public static final String ACL_LIST_FAMILY_STR = "l";
|
||||||
|
public static final byte[] ACL_LIST_FAMILY = Bytes.toBytes(ACL_LIST_FAMILY_STR);
|
||||||
|
|
||||||
|
/** Table descriptor for ACL internal table */
|
||||||
|
public static final HTableDescriptor ACL_TABLEDESC = new HTableDescriptor(
|
||||||
|
ACL_TABLE_NAME);
|
||||||
|
static {
|
||||||
|
ACL_TABLEDESC.addFamily(
|
||||||
|
new HColumnDescriptor(ACL_LIST_FAMILY,
|
||||||
|
10, // Ten is arbitrary number. Keep versions to help debugging.
|
||||||
|
Compression.Algorithm.NONE.getName(), true, true, 8 * 1024,
|
||||||
|
HConstants.FOREVER, StoreFile.BloomType.NONE.toString(),
|
||||||
|
HConstants.REPLICATION_SCOPE_LOCAL));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delimiter to separate user, column family, and qualifier in
|
||||||
|
* _acl_ table info: column keys */
|
||||||
|
public static final char ACL_KEY_DELIMITER = ',';
|
||||||
|
/** Prefix character to denote group names */
|
||||||
|
public static final String GROUP_PREFIX = "@";
|
||||||
|
/** Configuration key for superusers */
|
||||||
|
public static final String SUPERUSER_CONF_KEY = "hbase.superuser";
|
||||||
|
|
||||||
|
private static Log LOG = LogFactory.getLog(AccessControlLists.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for existence of {@code _acl_} table and create it if it does not exist
|
||||||
|
* @param master reference to HMaster
|
||||||
|
*/
|
||||||
|
static void init(MasterServices master) throws IOException {
|
||||||
|
if (!MetaReader.tableExists(master.getCatalogTracker(), ACL_TABLE_NAME_STR)) {
|
||||||
|
master.createTable(ACL_TABLEDESC, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a new table permission grant in the access control lists table.
|
||||||
|
* @param conf the configuration
|
||||||
|
* @param tableName the table to which access is being granted
|
||||||
|
* @param username the user or group being granted the permission
|
||||||
|
* @param perm the details of the permission being granted
|
||||||
|
* @throws IOException in the case of an error accessing the metadata table
|
||||||
|
*/
|
||||||
|
static void addTablePermission(Configuration conf,
|
||||||
|
byte[] tableName, String username, TablePermission perm)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
Put p = new Put(tableName);
|
||||||
|
byte[] key = Bytes.toBytes(username);
|
||||||
|
if (perm.getFamily() != null && perm.getFamily().length > 0) {
|
||||||
|
key = Bytes.add(key,
|
||||||
|
Bytes.add(new byte[]{ACL_KEY_DELIMITER}, perm.getFamily()));
|
||||||
|
if (perm.getQualifier() != null && perm.getQualifier().length > 0) {
|
||||||
|
key = Bytes.add(key,
|
||||||
|
Bytes.add(new byte[]{ACL_KEY_DELIMITER}, perm.getQualifier()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TablePermission.Action[] actions = perm.getActions();
|
||||||
|
if ((actions == null) || (actions.length == 0)) {
|
||||||
|
LOG.warn("No actions associated with user '"+username+"'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] value = new byte[actions.length];
|
||||||
|
for (int i = 0; i < actions.length; i++) {
|
||||||
|
value[i] = actions[i].code();
|
||||||
|
}
|
||||||
|
p.add(ACL_LIST_FAMILY, key, value);
|
||||||
|
if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("Writing permission for table "+
|
||||||
|
Bytes.toString(tableName)+" "+
|
||||||
|
Bytes.toString(key)+": "+Bytes.toStringBinary(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
HTable acls = null;
|
||||||
|
try {
|
||||||
|
acls = new HTable(conf, ACL_TABLE_NAME);
|
||||||
|
acls.put(p);
|
||||||
|
} finally {
|
||||||
|
if (acls != null) acls.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a previously granted permission from the stored access control
|
||||||
|
* lists. The {@link TablePermission} being removed must exactly match what
|
||||||
|
* is stored -- no wildcard matching is attempted. Ie, if user "bob" has
|
||||||
|
* been granted "READ" access to the "data" table, but only to column family
|
||||||
|
* plus qualifier "info:colA", then trying to call this method with only
|
||||||
|
* user "bob" and the table name "data" (but without specifying the
|
||||||
|
* column qualifier "info:colA") will have no effect.
|
||||||
|
*
|
||||||
|
* @param conf the configuration
|
||||||
|
* @param tableName the table of the current permission grant
|
||||||
|
* @param userName the user or group currently granted the permission
|
||||||
|
* @param perm the details of the permission to be revoked
|
||||||
|
* @throws IOException if there is an error accessing the metadata table
|
||||||
|
*/
|
||||||
|
static void removeTablePermission(Configuration conf,
|
||||||
|
byte[] tableName, String userName, TablePermission perm)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
Delete d = new Delete(tableName);
|
||||||
|
byte[] key = null;
|
||||||
|
if (perm.getFamily() != null && perm.getFamily().length > 0) {
|
||||||
|
key = Bytes.toBytes(userName + ACL_KEY_DELIMITER +
|
||||||
|
Bytes.toString(perm.getFamily()));
|
||||||
|
if (perm.getQualifier() != null && perm.getQualifier().length > 0) {
|
||||||
|
key = Bytes.toBytes(userName + ACL_KEY_DELIMITER +
|
||||||
|
Bytes.toString(perm.getFamily()) + ACL_KEY_DELIMITER +
|
||||||
|
Bytes.toString(perm.getQualifier()));
|
||||||
|
} else {
|
||||||
|
key = Bytes.toBytes(userName + ACL_KEY_DELIMITER +
|
||||||
|
Bytes.toString(perm.getFamily()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
key = Bytes.toBytes(userName);
|
||||||
|
}
|
||||||
|
if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("Removing permission for user '" + userName+ "': "+
|
||||||
|
perm.toString());
|
||||||
|
}
|
||||||
|
d.deleteColumns(ACL_LIST_FAMILY, key);
|
||||||
|
HTable acls = null;
|
||||||
|
try {
|
||||||
|
acls = new HTable(conf, ACL_TABLE_NAME);
|
||||||
|
acls.delete(d);
|
||||||
|
} finally {
|
||||||
|
if (acls != null) acls.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the given region is part of the {@code _acl_}
|
||||||
|
* metadata table.
|
||||||
|
*/
|
||||||
|
static boolean isAclRegion(HRegion region) {
|
||||||
|
return Bytes.equals(ACL_TABLE_NAME, region.getTableDesc().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all of the permission grants stored in a region of the {@code _acl_}
|
||||||
|
* table.
|
||||||
|
*
|
||||||
|
* @param aclRegion
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
static Map<byte[],ListMultimap<String,TablePermission>> loadAll(
|
||||||
|
HRegion aclRegion)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
if (!isAclRegion(aclRegion)) {
|
||||||
|
throw new IOException("Can only load permissions from "+ACL_TABLE_NAME_STR);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<byte[],ListMultimap<String,TablePermission>> allPerms =
|
||||||
|
new TreeMap<byte[],ListMultimap<String,TablePermission>>(Bytes.BYTES_COMPARATOR);
|
||||||
|
|
||||||
|
// do a full scan of _acl_ table
|
||||||
|
|
||||||
|
Scan scan = new Scan();
|
||||||
|
scan.addFamily(ACL_LIST_FAMILY);
|
||||||
|
|
||||||
|
InternalScanner iScanner = null;
|
||||||
|
try {
|
||||||
|
iScanner = aclRegion.getScanner(scan);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
List<KeyValue> row = new ArrayList<KeyValue>();
|
||||||
|
|
||||||
|
boolean hasNext = iScanner.next(row);
|
||||||
|
ListMultimap<String,TablePermission> perms = ArrayListMultimap.create();
|
||||||
|
byte[] table = null;
|
||||||
|
for (KeyValue kv : row) {
|
||||||
|
if (table == null) {
|
||||||
|
table = kv.getRow();
|
||||||
|
}
|
||||||
|
Pair<String,TablePermission> permissionsOfUserOnTable =
|
||||||
|
parseTablePermissionRecord(table, kv);
|
||||||
|
if (permissionsOfUserOnTable != null) {
|
||||||
|
String username = permissionsOfUserOnTable.getFirst();
|
||||||
|
TablePermission permissions = permissionsOfUserOnTable.getSecond();
|
||||||
|
perms.put(username, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (table != null) {
|
||||||
|
allPerms.put(table, perms);
|
||||||
|
}
|
||||||
|
if (!hasNext) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (iScanner != null) {
|
||||||
|
iScanner.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allPerms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all permissions from the region server holding {@code _acl_},
|
||||||
|
* primarily intended for testing purposes.
|
||||||
|
*/
|
||||||
|
static Map<byte[],ListMultimap<String,TablePermission>> loadAll(
|
||||||
|
Configuration conf) throws IOException {
|
||||||
|
Map<byte[],ListMultimap<String,TablePermission>> allPerms =
|
||||||
|
new TreeMap<byte[],ListMultimap<String,TablePermission>>(Bytes.BYTES_COMPARATOR);
|
||||||
|
|
||||||
|
// do a full scan of _acl_, filtering on only first table region rows
|
||||||
|
|
||||||
|
Scan scan = new Scan();
|
||||||
|
scan.addFamily(ACL_LIST_FAMILY);
|
||||||
|
|
||||||
|
HTable acls = null;
|
||||||
|
ResultScanner scanner = null;
|
||||||
|
try {
|
||||||
|
acls = new HTable(conf, ACL_TABLE_NAME);
|
||||||
|
scanner = acls.getScanner(scan);
|
||||||
|
for (Result row : scanner) {
|
||||||
|
ListMultimap<String,TablePermission> resultPerms =
|
||||||
|
parseTablePermissions(row.getRow(), row);
|
||||||
|
allPerms.put(row.getRow(), resultPerms);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (scanner != null) scanner.close();
|
||||||
|
if (acls != null) acls.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return allPerms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads user permission assignments stored in the <code>l:</code> column
|
||||||
|
* family of the first table row in <code>_acl_</code>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* See {@link AccessControlLists class documentation} for the key structure
|
||||||
|
* used for storage.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
static ListMultimap<String,TablePermission> getTablePermissions(
|
||||||
|
Configuration conf, byte[] tableName)
|
||||||
|
throws IOException {
|
||||||
|
/* TODO: -ROOT- and .META. cannot easily be handled because they must be
|
||||||
|
* online before _acl_ table. Can anything be done here?
|
||||||
|
*/
|
||||||
|
if (Bytes.equals(tableName, HConstants.ROOT_TABLE_NAME) ||
|
||||||
|
Bytes.equals(tableName, HConstants.META_TABLE_NAME) ||
|
||||||
|
Bytes.equals(tableName, AccessControlLists.ACL_TABLE_NAME)) {
|
||||||
|
return ArrayListMultimap.create(0,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for normal user tables, we just read the table row from _acl_
|
||||||
|
ListMultimap<String,TablePermission> perms = ArrayListMultimap.create();
|
||||||
|
HTable acls = null;
|
||||||
|
try {
|
||||||
|
acls = new HTable(conf, ACL_TABLE_NAME);
|
||||||
|
Get get = new Get(tableName);
|
||||||
|
get.addFamily(ACL_LIST_FAMILY);
|
||||||
|
Result row = acls.get(get);
|
||||||
|
if (!row.isEmpty()) {
|
||||||
|
perms = parseTablePermissions(tableName, row);
|
||||||
|
} else {
|
||||||
|
LOG.info("No permissions found in "+ACL_TABLE_NAME_STR+
|
||||||
|
" for table "+Bytes.toString(tableName));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (acls != null) acls.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently granted permissions for a given table as a list of
|
||||||
|
* user plus associated permissions.
|
||||||
|
*/
|
||||||
|
static List<UserPermission> getUserPermissions(
|
||||||
|
Configuration conf, byte[] tableName)
|
||||||
|
throws IOException {
|
||||||
|
ListMultimap<String,TablePermission> allPerms = getTablePermissions(
|
||||||
|
conf, tableName);
|
||||||
|
|
||||||
|
List<UserPermission> perms = new ArrayList<UserPermission>();
|
||||||
|
|
||||||
|
for (Map.Entry<String, TablePermission> entry : allPerms.entries()) {
|
||||||
|
UserPermission up = new UserPermission(Bytes.toBytes(entry.getKey()),
|
||||||
|
entry.getValue().getTable(), entry.getValue().getFamily(),
|
||||||
|
entry.getValue().getQualifier(), entry.getValue().getActions());
|
||||||
|
perms.add(up);
|
||||||
|
}
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ListMultimap<String,TablePermission> parseTablePermissions(
|
||||||
|
byte[] table, Result result) {
|
||||||
|
ListMultimap<String,TablePermission> perms = ArrayListMultimap.create();
|
||||||
|
if (result != null && result.size() > 0) {
|
||||||
|
for (KeyValue kv : result.raw()) {
|
||||||
|
|
||||||
|
Pair<String,TablePermission> permissionsOfUserOnTable =
|
||||||
|
parseTablePermissionRecord(table, kv);
|
||||||
|
|
||||||
|
if (permissionsOfUserOnTable != null) {
|
||||||
|
String username = permissionsOfUserOnTable.getFirst();
|
||||||
|
TablePermission permissions = permissionsOfUserOnTable.getSecond();
|
||||||
|
perms.put(username, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pair<String,TablePermission> parseTablePermissionRecord(
|
||||||
|
byte[] table, KeyValue kv) {
|
||||||
|
// return X given a set of permissions encoded in the permissionRecord kv.
|
||||||
|
byte[] family = kv.getFamily();
|
||||||
|
|
||||||
|
if (!Bytes.equals(family, ACL_LIST_FAMILY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] key = kv.getQualifier();
|
||||||
|
byte[] value = kv.getValue();
|
||||||
|
if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("Read acl: kv ["+
|
||||||
|
Bytes.toStringBinary(key)+": "+
|
||||||
|
Bytes.toStringBinary(value)+"]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for a column family appended to the key
|
||||||
|
// TODO: avoid the string conversion to make this more efficient
|
||||||
|
String username = Bytes.toString(key);
|
||||||
|
int idx = username.indexOf(ACL_KEY_DELIMITER);
|
||||||
|
byte[] permFamily = null;
|
||||||
|
byte[] permQualifier = null;
|
||||||
|
if (idx > 0 && idx < username.length()-1) {
|
||||||
|
String remainder = username.substring(idx+1);
|
||||||
|
username = username.substring(0, idx);
|
||||||
|
idx = remainder.indexOf(ACL_KEY_DELIMITER);
|
||||||
|
if (idx > 0 && idx < remainder.length()-1) {
|
||||||
|
permFamily = Bytes.toBytes(remainder.substring(0, idx));
|
||||||
|
permQualifier = Bytes.toBytes(remainder.substring(idx+1));
|
||||||
|
} else {
|
||||||
|
permFamily = Bytes.toBytes(remainder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Pair<String,TablePermission>(
|
||||||
|
username, new TablePermission(table, permFamily, permQualifier, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a set of permissions as {@link org.apache.hadoop.io.Writable} instances
|
||||||
|
* to the given output stream.
|
||||||
|
* @param out
|
||||||
|
* @param perms
|
||||||
|
* @param conf
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static void writePermissions(DataOutput out,
|
||||||
|
ListMultimap<String,? extends Permission> perms, Configuration conf)
|
||||||
|
throws IOException {
|
||||||
|
Set<String> keys = perms.keySet();
|
||||||
|
out.writeInt(keys.size());
|
||||||
|
for (String key : keys) {
|
||||||
|
Text.writeString(out, key);
|
||||||
|
HbaseObjectWritable.writeObject(out, perms.get(key), List.class, conf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a set of permissions as {@link org.apache.hadoop.io.Writable} instances
|
||||||
|
* and returns the resulting byte array.
|
||||||
|
*/
|
||||||
|
public static byte[] writePermissionsAsBytes(
|
||||||
|
ListMultimap<String,? extends Permission> perms, Configuration conf) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
writePermissions(new DataOutputStream(bos), perms, conf);
|
||||||
|
return bos.toByteArray();
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
// shouldn't happen here
|
||||||
|
LOG.error("Error serializing permissions", ioe);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a set of permissions as {@link org.apache.hadoop.io.Writable} instances
|
||||||
|
* from the input stream.
|
||||||
|
*/
|
||||||
|
public static <T extends Permission> ListMultimap<String,T> readPermissions(
|
||||||
|
DataInput in, Configuration conf) throws IOException {
|
||||||
|
ListMultimap<String,T> perms = ArrayListMultimap.create();
|
||||||
|
int length = in.readInt();
|
||||||
|
for (int i=0; i<length; i++) {
|
||||||
|
String user = Text.readString(in);
|
||||||
|
List<T> userPerms =
|
||||||
|
(List)HbaseObjectWritable.readObject(in, conf);
|
||||||
|
perms.putAll(user, userPerms);
|
||||||
|
}
|
||||||
|
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the given name should be interpreted as a group
|
||||||
|
* principal. Currently this simply checks if the name starts with the
|
||||||
|
* special group prefix character ("@").
|
||||||
|
*/
|
||||||
|
public static boolean isGroupPrincipal(String name) {
|
||||||
|
return name != null && name.startsWith(GROUP_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the actual name for a group principal (stripped of the
|
||||||
|
* group prefix).
|
||||||
|
*/
|
||||||
|
public static String getGroupName(String aclKey) {
|
||||||
|
if (!isGroupPrincipal(aclKey)) {
|
||||||
|
return aclKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return aclKey.substring(GROUP_PREFIX.length());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,987 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.common.collect.ListMultimap;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.MapMaker;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.hbase.CoprocessorEnvironment;
|
||||||
|
import org.apache.hadoop.hbase.HColumnDescriptor;
|
||||||
|
import org.apache.hadoop.hbase.HConstants;
|
||||||
|
import org.apache.hadoop.hbase.HTableDescriptor;
|
||||||
|
import org.apache.hadoop.hbase.KeyValue;
|
||||||
|
import org.apache.hadoop.hbase.HRegionInfo;
|
||||||
|
import org.apache.hadoop.hbase.ServerName;
|
||||||
|
import org.apache.hadoop.hbase.client.Delete;
|
||||||
|
import org.apache.hadoop.hbase.client.Get;
|
||||||
|
import org.apache.hadoop.hbase.client.Increment;
|
||||||
|
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.coprocessor.BaseRegionObserver;
|
||||||
|
import org.apache.hadoop.hbase.coprocessor.CoprocessorException;
|
||||||
|
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.filter.CompareFilter;
|
||||||
|
import org.apache.hadoop.hbase.filter.FilterList;
|
||||||
|
import org.apache.hadoop.hbase.filter.WritableByteArrayComparable;
|
||||||
|
import org.apache.hadoop.hbase.ipc.HBaseRPC;
|
||||||
|
import org.apache.hadoop.hbase.ipc.ProtocolSignature;
|
||||||
|
import org.apache.hadoop.hbase.ipc.RequestContext;
|
||||||
|
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.wal.WALEdit;
|
||||||
|
import org.apache.hadoop.hbase.security.AccessDeniedException;
|
||||||
|
import org.apache.hadoop.hbase.security.User;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* {@link org.apache.hadoop.hbase.ipc.SecureRpcEngine} 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 AccessControllerProtocol} implementation, and the associated
|
||||||
|
* {@code grant}, {@code revoke}, and {@code user_permission} HBase shell
|
||||||
|
* commands.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class AccessController extends BaseRegionObserver
|
||||||
|
implements MasterObserver, AccessControllerProtocol {
|
||||||
|
/**
|
||||||
|
* Represents the result of an authorization check for logging and error
|
||||||
|
* reporting.
|
||||||
|
*/
|
||||||
|
private static class AuthResult {
|
||||||
|
private final boolean allowed;
|
||||||
|
private final byte[] table;
|
||||||
|
private final byte[] family;
|
||||||
|
private final byte[] qualifier;
|
||||||
|
private final Permission.Action action;
|
||||||
|
private final String reason;
|
||||||
|
private final User user;
|
||||||
|
|
||||||
|
public AuthResult(boolean allowed, String reason, User user,
|
||||||
|
Permission.Action action, byte[] table, byte[] family, byte[] qualifier) {
|
||||||
|
this.allowed = allowed;
|
||||||
|
this.reason = reason;
|
||||||
|
this.user = user;
|
||||||
|
this.table = table;
|
||||||
|
this.family = family;
|
||||||
|
this.qualifier = qualifier;
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowed() { return allowed; }
|
||||||
|
|
||||||
|
public User getUser() { return user; }
|
||||||
|
|
||||||
|
public String getReason() { return reason; }
|
||||||
|
|
||||||
|
public String toContextString() {
|
||||||
|
return "(user=" + (user != null ? user.getName() : "UNKNOWN") + ", " +
|
||||||
|
"scope=" + (table == null ? "GLOBAL" : Bytes.toString(table)) + ", " +
|
||||||
|
"family=" + (family != null ? Bytes.toString(family) : "") + ", " +
|
||||||
|
"qualifer=" + (qualifier != null ? Bytes.toString(qualifier) : "") + ", " +
|
||||||
|
"action=" + (action != null ? action.toString() : "") + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return new StringBuilder("AuthResult")
|
||||||
|
.append(toContextString()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthResult allow(String reason, User user,
|
||||||
|
Permission.Action action, byte[] table) {
|
||||||
|
return new AuthResult(true, reason, user, action, table, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthResult deny(String reason, User user,
|
||||||
|
Permission.Action action, byte[] table) {
|
||||||
|
return new AuthResult(false, reason, user, action, table, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthResult deny(String reason, User user,
|
||||||
|
Permission.Action action, byte[] table, byte[] family, byte[] qualifier) {
|
||||||
|
return new AuthResult(false, reason, user, action, table, family, qualifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Log LOG = LogFactory.getLog(AccessController.class);
|
||||||
|
|
||||||
|
private static final Log AUDITLOG =
|
||||||
|
LogFactory.getLog("SecurityLogger."+AccessController.class.getName());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version number for AccessControllerProtocol
|
||||||
|
*/
|
||||||
|
private static final long PROTOCOL_VERSION = 1L;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
void initialize(RegionCoprocessorEnvironment e) throws IOException {
|
||||||
|
final HRegion region = e.getRegion();
|
||||||
|
|
||||||
|
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[] table = t.getKey();
|
||||||
|
String tableName = Bytes.toString(table);
|
||||||
|
ListMultimap<String,TablePermission> perms = t.getValue();
|
||||||
|
byte[] serialized = AccessControlLists.writePermissionsAsBytes(perms,
|
||||||
|
e.getRegion().getConf());
|
||||||
|
this.authManager.getZKPermissionWatcher().writeToZookeeper(tableName,
|
||||||
|
serialized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<KeyValue>> familyMap) {
|
||||||
|
Set<String> tableSet = new HashSet<String>();
|
||||||
|
for (Map.Entry<byte[], List<KeyValue>> f : familyMap.entrySet()) {
|
||||||
|
List<KeyValue> kvs = f.getValue();
|
||||||
|
for (KeyValue kv: kvs) {
|
||||||
|
if (Bytes.compareTo(kv.getBuffer(), kv.getFamilyOffset(),
|
||||||
|
kv.getFamilyLength(), AccessControlLists.ACL_LIST_FAMILY, 0,
|
||||||
|
AccessControlLists.ACL_LIST_FAMILY.length) == 0) {
|
||||||
|
String tableName = Bytes.toString(kv.getRow());
|
||||||
|
tableSet.add(tableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String tableName: tableSet) {
|
||||||
|
try {
|
||||||
|
ListMultimap<String,TablePermission> perms =
|
||||||
|
AccessControlLists.getTablePermissions(regionEnv.getConfiguration(),
|
||||||
|
Bytes.toBytes(tableName));
|
||||||
|
byte[] serialized = AccessControlLists.writePermissionsAsBytes(
|
||||||
|
perms, e.getRegion().getConf());
|
||||||
|
this.authManager.getZKPermissionWatcher().writeToZookeeper(tableName,
|
||||||
|
serialized);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("Failed updating permissions mirror for '" + tableName +
|
||||||
|
"'", 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
|
||||||
|
*/
|
||||||
|
AuthResult permissionGranted(User user, TablePermission.Action permRequest,
|
||||||
|
RegionCoprocessorEnvironment e,
|
||||||
|
Map<byte [], ? extends Collection<?>> families) {
|
||||||
|
HRegionInfo hri = e.getRegion().getRegionInfo();
|
||||||
|
HTableDescriptor htd = e.getRegion().getTableDesc();
|
||||||
|
byte[] tableName = hri.getTableName();
|
||||||
|
|
||||||
|
// 1. All users need read access to .META. and -ROOT- tables.
|
||||||
|
// this is a very common operation, so deal with it quickly.
|
||||||
|
if ((hri.isRootRegion() || hri.isMetaRegion()) &&
|
||||||
|
(permRequest == TablePermission.Action.READ)) {
|
||||||
|
return AuthResult.allow("All users allowed", user, permRequest,
|
||||||
|
hri.getTableName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return AuthResult.deny("No user associated with request!", null,
|
||||||
|
permRequest, hri.getTableName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. The table owner has full privileges
|
||||||
|
String owner = htd.getOwnerString();
|
||||||
|
if (user.getShortName().equals(owner)) {
|
||||||
|
// owner of the table has full access
|
||||||
|
return AuthResult.allow("User is table owner", user, permRequest,
|
||||||
|
hri.getTableName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. check for the table-level, if successful we can short-circuit
|
||||||
|
if (authManager.authorize(user, tableName, (byte[])null, permRequest)) {
|
||||||
|
return AuthResult.allow("Table permission granted", user,
|
||||||
|
permRequest, tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 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("Failed qualifier check", user,
|
||||||
|
permRequest, tableName, 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("Failed qualifier check", user,
|
||||||
|
permRequest, tableName, family.getKey(), kv.getQualifier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no qualifiers and family-level check already failed
|
||||||
|
return AuthResult.deny("Failed family check", user, permRequest,
|
||||||
|
tableName, family.getKey(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// all family checks passed
|
||||||
|
return AuthResult.allow("All family checks passed", user, permRequest,
|
||||||
|
tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. no families to check and table level access failed
|
||||||
|
return AuthResult.deny("No families to check and table permission failed",
|
||||||
|
user, permRequest, tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logResult(AuthResult result) {
|
||||||
|
if (AUDITLOG.isTraceEnabled()) {
|
||||||
|
AUDITLOG.trace("Access " + (result.isAllowed() ? "allowed" : "denied") +
|
||||||
|
" for user " + (result.getUser() != null ? result.getUser().getShortName() : "UNKNOWN") +
|
||||||
|
"; reason: " + result.getReason() +
|
||||||
|
"; 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 = User.getCurrent();
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(Permission.Action perm) throws IOException {
|
||||||
|
User user = getActiveUser();
|
||||||
|
if (authManager.authorize(user, perm)) {
|
||||||
|
logResult(AuthResult.allow("Global check allowed", user, perm, null));
|
||||||
|
} else {
|
||||||
|
logResult(AuthResult.deny("Global check failed", user, perm, null));
|
||||||
|
throw new AccessDeniedException("Insufficient permissions for user '" +
|
||||||
|
(user != null ? user.getShortName() : "null") +"' (global, action=" +
|
||||||
|
perm.toString() + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 set of column families present/required in the request
|
||||||
|
* @throws AccessDeniedException if the authorization check failed
|
||||||
|
*/
|
||||||
|
private void requirePermission(Permission.Action perm,
|
||||||
|
RegionCoprocessorEnvironment env, Collection<byte[]> families)
|
||||||
|
throws IOException {
|
||||||
|
// create a map of family-qualifier
|
||||||
|
HashMap<byte[], Set<byte[]>> familyMap = new HashMap<byte[], Set<byte[]>>();
|
||||||
|
for (byte[] family : families) {
|
||||||
|
familyMap.put(family, null);
|
||||||
|
}
|
||||||
|
requirePermission(perm, env, familyMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(Permission.Action perm,
|
||||||
|
RegionCoprocessorEnvironment env,
|
||||||
|
Map<byte[], ? extends Collection<?>> families)
|
||||||
|
throws IOException {
|
||||||
|
User user = getActiveUser();
|
||||||
|
AuthResult result = permissionGranted(user, perm, env, families);
|
||||||
|
logResult(result);
|
||||||
|
|
||||||
|
if (!result.isAllowed()) {
|
||||||
|
StringBuffer sb = new StringBuffer("");
|
||||||
|
if ((families != null && families.size() > 0)) {
|
||||||
|
for (byte[] familyName : families.keySet()) {
|
||||||
|
if (sb.length() != 0) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
sb.append(Bytes.toString(familyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AccessDeniedException("Insufficient permissions (table=" +
|
||||||
|
env.getRegion().getTableDesc().getNameAsString()+
|
||||||
|
((families != null && families.size() > 0) ? ", family: " +
|
||||||
|
sb.toString() : "") + ", 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,
|
||||||
|
TablePermission.Action perm,
|
||||||
|
RegionCoprocessorEnvironment env,
|
||||||
|
Map<byte[], ? extends Set<byte[]>> familyMap)
|
||||||
|
throws IOException {
|
||||||
|
HRegionInfo hri = env.getRegion().getRegionInfo();
|
||||||
|
byte[] tableName = hri.getTableName();
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (familyMap != null && familyMap.size() > 0) {
|
||||||
|
// at least one family must be allowed
|
||||||
|
for (Map.Entry<byte[], ? extends Set<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- MasterObserver implementation ---- */
|
||||||
|
public void start(CoprocessorEnvironment env) throws IOException {
|
||||||
|
// if running on HMaster
|
||||||
|
if (env instanceof MasterCoprocessorEnvironment) {
|
||||||
|
MasterCoprocessorEnvironment e = (MasterCoprocessorEnvironment)env;
|
||||||
|
this.authManager = TableAuthManager.get(
|
||||||
|
e.getMasterServices().getZooKeeper(),
|
||||||
|
e.getConfiguration());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if running at region
|
||||||
|
if (env instanceof RegionCoprocessorEnvironment) {
|
||||||
|
regionEnv = (RegionCoprocessorEnvironment)env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop(CoprocessorEnvironment env) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preCreateTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
HTableDescriptor desc, HRegionInfo[] regions) throws IOException {
|
||||||
|
requirePermission(Permission.Action.CREATE);
|
||||||
|
|
||||||
|
// default the table owner if not specified
|
||||||
|
User owner = getActiveUser();
|
||||||
|
if (desc.getOwnerString() == null ||
|
||||||
|
desc.getOwnerString().equals("")) {
|
||||||
|
desc.setOwner(owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postCreateTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
HTableDescriptor desc, HRegionInfo[] regions) throws IOException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preDeleteTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName) throws IOException {
|
||||||
|
requirePermission(Permission.Action.CREATE);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postDeleteTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName) throws IOException {}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preModifyTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName, HTableDescriptor htd) throws IOException {
|
||||||
|
requirePermission(Permission.Action.CREATE);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postModifyTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName, HTableDescriptor htd) throws IOException {}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preAddColumn(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName, HColumnDescriptor column) throws IOException {
|
||||||
|
requirePermission(Permission.Action.CREATE);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postAddColumn(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName, HColumnDescriptor column) throws IOException {}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preModifyColumn(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName, HColumnDescriptor descriptor) throws IOException {
|
||||||
|
requirePermission(Permission.Action.CREATE);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postModifyColumn(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName, HColumnDescriptor descriptor) throws IOException {}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preDeleteColumn(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName, byte[] col) throws IOException {
|
||||||
|
requirePermission(Permission.Action.CREATE);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postDeleteColumn(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName, byte[] col) throws IOException {}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preEnableTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName) throws IOException {
|
||||||
|
/* TODO: Allow for users with global CREATE permission and the table owner */
|
||||||
|
requirePermission(Permission.Action.ADMIN);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postEnableTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName) throws IOException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preDisableTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName) throws IOException {
|
||||||
|
/* TODO: Allow for users with global CREATE permission and the table owner */
|
||||||
|
requirePermission(Permission.Action.ADMIN);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postDisableTable(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
byte[] tableName) throws IOException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preMove(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
HRegionInfo region, ServerName srcServer, ServerName destServer)
|
||||||
|
throws IOException {
|
||||||
|
requirePermission(Permission.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(Permission.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(Permission.Action.ADMIN);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postUnassign(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
HRegionInfo regionInfo, boolean force) throws IOException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preBalance(ObserverContext<MasterCoprocessorEnvironment> c)
|
||||||
|
throws IOException {
|
||||||
|
requirePermission(Permission.Action.ADMIN);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void postBalance(ObserverContext<MasterCoprocessorEnvironment> c)
|
||||||
|
throws IOException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preBalanceSwitch(ObserverContext<MasterCoprocessorEnvironment> c,
|
||||||
|
boolean newValue) throws IOException {
|
||||||
|
requirePermission(Permission.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(Permission.Action.ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preStopMaster(ObserverContext<MasterCoprocessorEnvironment> c)
|
||||||
|
throws IOException {
|
||||||
|
requirePermission(Permission.Action.ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postStartMaster(ObserverContext<MasterCoprocessorEnvironment> ctx)
|
||||||
|
throws IOException {
|
||||||
|
// initialize the ACL storage table
|
||||||
|
AccessControlLists.init(ctx.getEnvironment().getMasterServices());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ---- RegionObserver implementation ---- */
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postOpen(ObserverContext<RegionCoprocessorEnvironment> c) {
|
||||||
|
RegionCoprocessorEnvironment e = c.getEnvironment();
|
||||||
|
final HRegion region = e.getRegion();
|
||||||
|
if (region == null) {
|
||||||
|
LOG.error("NULL region from RegionCoprocessorEnvironment in postOpen()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.authManager = TableAuthManager.get(
|
||||||
|
e.getRegionServerServices().getZooKeeper(),
|
||||||
|
e.getRegion().getConf());
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
// pass along as a RuntimeException, so that the coprocessor is unloaded
|
||||||
|
throw new RuntimeException("Error obtaining TableAuthManager", ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AccessControlLists.isAclRegion(region)) {
|
||||||
|
aclRegion = true;
|
||||||
|
try {
|
||||||
|
initialize(e);
|
||||||
|
} 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 preGetClosestRowBefore(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final byte [] row, final byte [] family, final Result result)
|
||||||
|
throws IOException {
|
||||||
|
requirePermission(TablePermission.Action.READ, c.getEnvironment(),
|
||||||
|
(family != null ? Lists.newArrayList(family) : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preGet(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final Get get, final List<KeyValue> result) throws IOException {
|
||||||
|
/*
|
||||||
|
if column family level checks fail, check for a qualifier level permission
|
||||||
|
in one of the families. If it is present, then continue with the AccessControlFilter.
|
||||||
|
*/
|
||||||
|
RegionCoprocessorEnvironment e = c.getEnvironment();
|
||||||
|
User requestUser = getActiveUser();
|
||||||
|
AuthResult authResult = permissionGranted(requestUser,
|
||||||
|
TablePermission.Action.READ, e, get.getFamilyMap());
|
||||||
|
if (!authResult.isAllowed()) {
|
||||||
|
if (hasFamilyQualifierPermission(requestUser,
|
||||||
|
TablePermission.Action.READ, e, get.getFamilyMap())) {
|
||||||
|
byte[] table = getTableName(e);
|
||||||
|
AccessControlFilter filter = new AccessControlFilter(authManager,
|
||||||
|
requestUser, table);
|
||||||
|
|
||||||
|
// wrap any existing filter
|
||||||
|
if (get.getFilter() != null) {
|
||||||
|
FilterList wrapper = new FilterList(FilterList.Operator.MUST_PASS_ALL,
|
||||||
|
Lists.newArrayList(filter, get.getFilter()));
|
||||||
|
get.setFilter(wrapper);
|
||||||
|
} else {
|
||||||
|
get.setFilter(filter);
|
||||||
|
}
|
||||||
|
logResult(AuthResult.allow("Access allowed with filter", requestUser,
|
||||||
|
TablePermission.Action.READ, authResult.table));
|
||||||
|
} else {
|
||||||
|
logResult(authResult);
|
||||||
|
throw new AccessDeniedException("Insufficient permissions (table=" +
|
||||||
|
e.getRegion().getTableDesc().getNameAsString() + ", action=READ)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// log auth success
|
||||||
|
logResult(authResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preExists(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final Get get, final boolean exists) throws IOException {
|
||||||
|
requirePermission(TablePermission.Action.READ, c.getEnvironment(),
|
||||||
|
get.familySet());
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prePut(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final Put put, final WALEdit edit, final boolean writeToWAL)
|
||||||
|
throws IOException {
|
||||||
|
requirePermission(TablePermission.Action.WRITE, c.getEnvironment(),
|
||||||
|
put.getFamilyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postPut(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final Put put, final WALEdit edit, final boolean writeToWAL) {
|
||||||
|
if (aclRegion) {
|
||||||
|
updateACL(c.getEnvironment(), put.getFamilyMap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preDelete(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final Delete delete, final WALEdit edit, final boolean writeToWAL)
|
||||||
|
throws IOException {
|
||||||
|
requirePermission(TablePermission.Action.WRITE, c.getEnvironment(),
|
||||||
|
delete.getFamilyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postDelete(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final Delete delete, final WALEdit edit, final boolean writeToWAL)
|
||||||
|
throws IOException {
|
||||||
|
if (aclRegion) {
|
||||||
|
updateACL(c.getEnvironment(), delete.getFamilyMap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preCheckAndPut(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final byte [] row, final byte [] family, final byte [] qualifier,
|
||||||
|
final CompareFilter.CompareOp compareOp,
|
||||||
|
final WritableByteArrayComparable comparator, final Put put,
|
||||||
|
final boolean result) throws IOException {
|
||||||
|
requirePermission(TablePermission.Action.READ, c.getEnvironment(),
|
||||||
|
Arrays.asList(new byte[][]{family}));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preCheckAndDelete(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final byte [] row, final byte [] family, final byte [] qualifier,
|
||||||
|
final CompareFilter.CompareOp compareOp,
|
||||||
|
final WritableByteArrayComparable comparator, final Delete delete,
|
||||||
|
final boolean result) throws IOException {
|
||||||
|
requirePermission(TablePermission.Action.READ, c.getEnvironment(),
|
||||||
|
Arrays.asList( new byte[][] {family}));
|
||||||
|
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 {
|
||||||
|
requirePermission(TablePermission.Action.WRITE, c.getEnvironment(),
|
||||||
|
Arrays.asList(new byte[][]{family}));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preIncrement(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final Increment increment, final Result result)
|
||||||
|
throws IOException {
|
||||||
|
requirePermission(TablePermission.Action.WRITE, c.getEnvironment(),
|
||||||
|
increment.getFamilyMap().keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RegionScanner preScannerOpen(final ObserverContext<RegionCoprocessorEnvironment> c,
|
||||||
|
final Scan scan, final RegionScanner s) throws IOException {
|
||||||
|
/*
|
||||||
|
if column family level checks fail, check for a qualifier level permission
|
||||||
|
in one of the families. If it is present, then continue with the AccessControlFilter.
|
||||||
|
*/
|
||||||
|
RegionCoprocessorEnvironment e = c.getEnvironment();
|
||||||
|
User user = getActiveUser();
|
||||||
|
AuthResult authResult = permissionGranted(user, TablePermission.Action.READ, e,
|
||||||
|
scan.getFamilyMap());
|
||||||
|
if (!authResult.isAllowed()) {
|
||||||
|
if (hasFamilyQualifierPermission(user, TablePermission.Action.READ, e,
|
||||||
|
scan.getFamilyMap())) {
|
||||||
|
byte[] table = getTableName(e);
|
||||||
|
AccessControlFilter filter = new AccessControlFilter(authManager,
|
||||||
|
user, table);
|
||||||
|
|
||||||
|
// wrap any existing filter
|
||||||
|
if (scan.hasFilter()) {
|
||||||
|
FilterList wrapper = new FilterList(FilterList.Operator.MUST_PASS_ALL,
|
||||||
|
Lists.newArrayList(filter, scan.getFilter()));
|
||||||
|
scan.setFilter(wrapper);
|
||||||
|
} else {
|
||||||
|
scan.setFilter(filter);
|
||||||
|
}
|
||||||
|
logResult(AuthResult.allow("Access allowed with filter", user,
|
||||||
|
TablePermission.Action.READ, authResult.table));
|
||||||
|
} else {
|
||||||
|
// no table/family level perms and no qualifier level perms, reject
|
||||||
|
logResult(authResult);
|
||||||
|
throw new AccessDeniedException("Insufficient permissions for user '"+
|
||||||
|
(user != null ? user.getShortName() : "null")+"' "+
|
||||||
|
"for scanner open on table " + Bytes.toString(getTableName(e)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// log success
|
||||||
|
logResult(authResult);
|
||||||
|
}
|
||||||
|
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 owner = scannerOwners.get(s);
|
||||||
|
if (owner != null && !owner.equals(RequestContext.getRequestUserName())) {
|
||||||
|
throw new AccessDeniedException("User '"+
|
||||||
|
RequestContext.getRequestUserName()+"' is not the scanner owner!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- AccessControllerProtocol implementation ---- */
|
||||||
|
/*
|
||||||
|
* These methods are only allowed to be called against the _acl_ region(s).
|
||||||
|
* This will be restricted by both client side and endpoint implementations.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void grant(byte[] user, TablePermission permission)
|
||||||
|
throws IOException {
|
||||||
|
// verify it's only running at .acl.
|
||||||
|
if (aclRegion) {
|
||||||
|
if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("Received request to grant access permission to '"
|
||||||
|
+ Bytes.toString(user) + "'. "
|
||||||
|
+ permission.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
requirePermission(Permission.Action.ADMIN);
|
||||||
|
|
||||||
|
AccessControlLists.addTablePermission(regionEnv.getConfiguration(),
|
||||||
|
permission.getTable(), Bytes.toString(user), permission);
|
||||||
|
if (AUDITLOG.isTraceEnabled()) {
|
||||||
|
// audit log should store permission changes in addition to auth results
|
||||||
|
AUDITLOG.trace("Granted user '" + Bytes.toString(user) + "' permission "
|
||||||
|
+ permission.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new CoprocessorException(AccessController.class, "This method " +
|
||||||
|
"can only execute at " +
|
||||||
|
Bytes.toString(AccessControlLists.ACL_TABLE_NAME) + " table.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void revoke(byte[] user, TablePermission permission)
|
||||||
|
throws IOException{
|
||||||
|
// only allowed to be called on _acl_ region
|
||||||
|
if (aclRegion) {
|
||||||
|
if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("Received request to revoke access permission for '"
|
||||||
|
+ Bytes.toString(user) + "'. "
|
||||||
|
+ permission.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
requirePermission(Permission.Action.ADMIN);
|
||||||
|
|
||||||
|
AccessControlLists.removeTablePermission(regionEnv.getConfiguration(),
|
||||||
|
permission.getTable(), Bytes.toString(user), permission);
|
||||||
|
if (AUDITLOG.isTraceEnabled()) {
|
||||||
|
// audit log should record all permission changes
|
||||||
|
AUDITLOG.trace("Revoked user '" + Bytes.toString(user) + "' permission "
|
||||||
|
+ permission.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new CoprocessorException(AccessController.class, "This method " +
|
||||||
|
"can only execute at " +
|
||||||
|
Bytes.toString(AccessControlLists.ACL_TABLE_NAME) + " table.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<UserPermission> getUserPermissions(final byte[] tableName)
|
||||||
|
throws IOException {
|
||||||
|
// only allowed to be called on _acl_ region
|
||||||
|
if (aclRegion) {
|
||||||
|
requirePermission(Permission.Action.ADMIN);
|
||||||
|
|
||||||
|
List<UserPermission> perms = AccessControlLists.getUserPermissions
|
||||||
|
(regionEnv.getConfiguration(), tableName);
|
||||||
|
return perms;
|
||||||
|
} else {
|
||||||
|
throw new CoprocessorException(AccessController.class, "This method " +
|
||||||
|
"can only execute at " +
|
||||||
|
Bytes.toString(AccessControlLists.ACL_TABLE_NAME) + " table.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getProtocolVersion(String protocol, long clientVersion) throws IOException {
|
||||||
|
return PROTOCOL_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProtocolSignature getProtocolSignature(String protocol,
|
||||||
|
long clientVersion, int clientMethodsHash) throws IOException {
|
||||||
|
if (AccessControllerProtocol.class.getName().equals(protocol)) {
|
||||||
|
return new ProtocolSignature(PROTOCOL_VERSION, null);
|
||||||
|
}
|
||||||
|
throw new HBaseRPC.UnknownProtocolException(
|
||||||
|
"Unexpected protocol requested: "+protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getTableName(RegionCoprocessorEnvironment e) {
|
||||||
|
HRegion region = e.getRegion();
|
||||||
|
byte[] tableName = null;
|
||||||
|
|
||||||
|
if (region != null) {
|
||||||
|
HRegionInfo regionInfo = region.getRegionInfo();
|
||||||
|
if (regionInfo != null) {
|
||||||
|
tableName = regionInfo.getTableName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tableName;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.apache.hadoop.hbase.ipc.CoprocessorProtocol;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom protocol defined for maintaining and querying access control lists.
|
||||||
|
*/
|
||||||
|
public interface AccessControllerProtocol extends CoprocessorProtocol {
|
||||||
|
/**
|
||||||
|
* Grants the given user or group the privilege to perform the given actions
|
||||||
|
* over the specified scope contained in {@link TablePermission}
|
||||||
|
* @param user the user name, or, if prefixed with "@", group name receiving
|
||||||
|
* the grant
|
||||||
|
* @param permission the details of the provided permissions
|
||||||
|
* @throws IOException if the grant could not be applied
|
||||||
|
*/
|
||||||
|
public void grant(byte[] user, TablePermission permission)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revokes a previously granted privilege from a user or group.
|
||||||
|
* Note that the provided {@link TablePermission} details must exactly match
|
||||||
|
* a stored grant. For example, if user "bob" has been granted "READ" access
|
||||||
|
* to table "data", over column family and qualifer "info:colA", then the
|
||||||
|
* table, column family and column qualifier must all be specified.
|
||||||
|
* Attempting to revoke permissions over just the "data" table will have
|
||||||
|
* no effect.
|
||||||
|
* @param user the user name, or, if prefixed with "@", group name whose
|
||||||
|
* privileges are being revoked
|
||||||
|
* @param permission the details of the previously granted permission to revoke
|
||||||
|
* @throws IOException if the revocation could not be performed
|
||||||
|
*/
|
||||||
|
public void revoke(byte[] user, TablePermission permission)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the permissions currently stored for the given table, returning
|
||||||
|
* a list of currently granted permissions, along with the user or group
|
||||||
|
* each is associated with.
|
||||||
|
* @param tableName the table of the permission grants to return
|
||||||
|
* @return a list of the currently granted permissions, with associated user
|
||||||
|
* or group names
|
||||||
|
* @throws IOException if there is an error querying the permissions
|
||||||
|
*/
|
||||||
|
public List<UserPermission> getUserPermissions(byte[] tableName)
|
||||||
|
throws IOException;
|
||||||
|
}
|
@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.common.collect.Maps;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
import org.apache.hadoop.io.VersionedWritable;
|
||||||
|
|
||||||
|
import java.io.DataInput;
|
||||||
|
import java.io.DataOutput;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base permissions instance representing the ability to perform a given set
|
||||||
|
* of actions.
|
||||||
|
*
|
||||||
|
* @see TablePermission
|
||||||
|
*/
|
||||||
|
public class Permission extends VersionedWritable {
|
||||||
|
protected static final byte VERSION = 0;
|
||||||
|
public enum Action {
|
||||||
|
READ('R'), WRITE('W'), EXEC('X'), CREATE('C'), ADMIN('A');
|
||||||
|
|
||||||
|
private byte code;
|
||||||
|
Action(char code) {
|
||||||
|
this.code = (byte)code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte code() { return code; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Log LOG = LogFactory.getLog(Permission.class);
|
||||||
|
protected static Map<Byte,Action> ACTION_BY_CODE = Maps.newHashMap();
|
||||||
|
|
||||||
|
protected Action[] actions;
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (Action a : Action.values()) {
|
||||||
|
ACTION_BY_CODE.put(a.code(), a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Empty constructor for Writable implementation. <b>Do not use.</b> */
|
||||||
|
public Permission() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Permission(Action... assigned) {
|
||||||
|
if (assigned != null && assigned.length > 0) {
|
||||||
|
actions = Arrays.copyOf(assigned, assigned.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Permission(byte[] actionCodes) {
|
||||||
|
if (actionCodes != null) {
|
||||||
|
Action acts[] = new Action[actionCodes.length];
|
||||||
|
int j = 0;
|
||||||
|
for (int i=0; i<actionCodes.length; i++) {
|
||||||
|
byte b = actionCodes[i];
|
||||||
|
Action a = ACTION_BY_CODE.get(b);
|
||||||
|
if (a == null) {
|
||||||
|
LOG.error("Ignoring unknown action code '"+
|
||||||
|
Bytes.toStringBinary(new byte[]{b})+"'");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
acts[j++] = a;
|
||||||
|
}
|
||||||
|
this.actions = Arrays.copyOf(acts, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action[] getActions() {
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean implies(Action action) {
|
||||||
|
if (this.actions != null) {
|
||||||
|
for (Action a : this.actions) {
|
||||||
|
if (a == action) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof Permission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Permission other = (Permission)obj;
|
||||||
|
// check actions
|
||||||
|
if (actions == null && other.getActions() == null) {
|
||||||
|
return true;
|
||||||
|
} else if (actions != null && other.getActions() != null) {
|
||||||
|
Action[] otherActions = other.getActions();
|
||||||
|
if (actions.length != otherActions.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for (Action a : actions) {
|
||||||
|
for (Action oa : otherActions) {
|
||||||
|
if (a == oa) continue outer;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 37;
|
||||||
|
int result = 23;
|
||||||
|
for (Action a : actions) {
|
||||||
|
result = prime * result + a.code();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder str = new StringBuilder("[Permission: ")
|
||||||
|
.append("actions=");
|
||||||
|
if (actions != null) {
|
||||||
|
for (int i=0; i<actions.length; i++) {
|
||||||
|
if (i > 0)
|
||||||
|
str.append(",");
|
||||||
|
if (actions[i] != null)
|
||||||
|
str.append(actions[i].toString());
|
||||||
|
else
|
||||||
|
str.append("NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str.append("]");
|
||||||
|
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return the object version number */
|
||||||
|
public byte getVersion() {
|
||||||
|
return VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFields(DataInput in) throws IOException {
|
||||||
|
super.readFields(in);
|
||||||
|
int length = (int)in.readByte();
|
||||||
|
if (length > 0) {
|
||||||
|
actions = new Action[length];
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
byte b = in.readByte();
|
||||||
|
Action a = ACTION_BY_CODE.get(b);
|
||||||
|
if (a == null) {
|
||||||
|
throw new IOException("Unknown action code '"+
|
||||||
|
Bytes.toStringBinary(new byte[]{b})+"' in input");
|
||||||
|
}
|
||||||
|
this.actions[i] = a;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actions = new Action[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(DataOutput out) throws IOException {
|
||||||
|
super.write(out);
|
||||||
|
out.writeByte(actions != null ? actions.length : 0);
|
||||||
|
if (actions != null) {
|
||||||
|
for (Action a: actions) {
|
||||||
|
out.writeByte(a.code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,482 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.common.collect.ArrayListMultimap;
|
||||||
|
import com.google.common.collect.ListMultimap;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.hbase.KeyValue;
|
||||||
|
import org.apache.hadoop.hbase.security.User;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
|
||||||
|
import org.apache.zookeeper.KeeperException;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentSkipListMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs authorization checks for a given user's assigned permissions
|
||||||
|
*/
|
||||||
|
public class TableAuthManager {
|
||||||
|
/** Key for the user and group cache maps for globally assigned permissions */
|
||||||
|
private static final String GLOBAL_CACHE_KEY = ".access.";
|
||||||
|
private static Log LOG = LogFactory.getLog(TableAuthManager.class);
|
||||||
|
|
||||||
|
private static TableAuthManager instance;
|
||||||
|
|
||||||
|
/** Cache of global user permissions */
|
||||||
|
private ListMultimap<String,Permission> USER_CACHE = ArrayListMultimap.create();
|
||||||
|
/** Cache of global group permissions */
|
||||||
|
private ListMultimap<String,Permission> GROUP_CACHE = ArrayListMultimap.create();
|
||||||
|
|
||||||
|
private ConcurrentSkipListMap<byte[], ListMultimap<String,TablePermission>> TABLE_USER_CACHE =
|
||||||
|
new ConcurrentSkipListMap<byte[], ListMultimap<String,TablePermission>>(Bytes.BYTES_COMPARATOR);
|
||||||
|
|
||||||
|
private ConcurrentSkipListMap<byte[], ListMultimap<String,TablePermission>> TABLE_GROUP_CACHE =
|
||||||
|
new ConcurrentSkipListMap<byte[], ListMultimap<String,TablePermission>>(Bytes.BYTES_COMPARATOR);
|
||||||
|
|
||||||
|
private Configuration conf;
|
||||||
|
private ZKPermissionWatcher zkperms;
|
||||||
|
|
||||||
|
private TableAuthManager(ZooKeeperWatcher watcher, Configuration conf)
|
||||||
|
throws IOException {
|
||||||
|
this.conf = conf;
|
||||||
|
this.zkperms = new ZKPermissionWatcher(watcher, this, conf);
|
||||||
|
try {
|
||||||
|
this.zkperms.start();
|
||||||
|
} catch (KeeperException ke) {
|
||||||
|
LOG.error("ZooKeeper initialization failed", ke);
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize global permissions based on configuration
|
||||||
|
initGlobal(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initGlobal(Configuration conf) throws IOException {
|
||||||
|
User user = User.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();
|
||||||
|
|
||||||
|
// the system user is always included
|
||||||
|
List<String> superusers = Lists.asList(currentUser, conf.getStrings(
|
||||||
|
AccessControlLists.SUPERUSER_CONF_KEY, new String[0]));
|
||||||
|
if (superusers != null) {
|
||||||
|
for (String name : superusers) {
|
||||||
|
if (AccessControlLists.isGroupPrincipal(name)) {
|
||||||
|
GROUP_CACHE.put(AccessControlLists.getGroupName(name),
|
||||||
|
new Permission(Permission.Action.values()));
|
||||||
|
} else {
|
||||||
|
USER_CACHE.put(name, new Permission(Permission.Action.values()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZKPermissionWatcher getZKPermissionWatcher() {
|
||||||
|
return this.zkperms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshCacheFromWritable(byte[] table, byte[] data) throws IOException {
|
||||||
|
if (data != null && data.length > 0) {
|
||||||
|
DataInput in = new DataInputStream( new ByteArrayInputStream(data) );
|
||||||
|
ListMultimap<String,TablePermission> perms = AccessControlLists.readPermissions(in, conf);
|
||||||
|
cache(table, perms);
|
||||||
|
} else {
|
||||||
|
LOG.debug("Skipping permission cache refresh because writable data is empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the internal permissions cache for a single table, splitting
|
||||||
|
* the permissions listed into separate caches for users and groups to optimize
|
||||||
|
* group lookups.
|
||||||
|
*
|
||||||
|
* @param table
|
||||||
|
* @param tablePerms
|
||||||
|
*/
|
||||||
|
private void cache(byte[] table,
|
||||||
|
ListMultimap<String,TablePermission> tablePerms) {
|
||||||
|
// split user from group assignments so we don't have to prepend the group
|
||||||
|
// prefix every time we query for groups
|
||||||
|
ListMultimap<String,TablePermission> userPerms = ArrayListMultimap.create();
|
||||||
|
ListMultimap<String,TablePermission> groupPerms = ArrayListMultimap.create();
|
||||||
|
|
||||||
|
if (tablePerms != null) {
|
||||||
|
for (Map.Entry<String,TablePermission> entry : tablePerms.entries()) {
|
||||||
|
if (AccessControlLists.isGroupPrincipal(entry.getKey())) {
|
||||||
|
groupPerms.put(
|
||||||
|
entry.getKey().substring(AccessControlLists.GROUP_PREFIX.length()),
|
||||||
|
entry.getValue());
|
||||||
|
} else {
|
||||||
|
userPerms.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TABLE_GROUP_CACHE.put(table, groupPerms);
|
||||||
|
TABLE_USER_CACHE.put(table, userPerms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TablePermission> getUserPermissions(String username, byte[] table) {
|
||||||
|
ListMultimap<String, TablePermission> tablePerms = TABLE_USER_CACHE.get(table);
|
||||||
|
if (tablePerms != null) {
|
||||||
|
return tablePerms.get(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TablePermission> getGroupPermissions(String groupName, byte[] table) {
|
||||||
|
ListMultimap<String, TablePermission> tablePerms = TABLE_GROUP_CACHE.get(table);
|
||||||
|
if (tablePerms != null) {
|
||||||
|
return tablePerms.get(groupName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorizes a global permission
|
||||||
|
* @param perms
|
||||||
|
* @param action
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean authorize(List<Permission> perms, Permission.Action action) {
|
||||||
|
if (perms != null) {
|
||||||
|
for (Permission p : perms) {
|
||||||
|
if (p.implies(action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("No permissions found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize a global permission based on ACLs for the given user and the
|
||||||
|
* user's groups.
|
||||||
|
* @param user
|
||||||
|
* @param action
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean authorize(User user, Permission.Action action) {
|
||||||
|
if (user == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorize(USER_CACHE.get(user.getShortName()), action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] groups = user.getGroupNames();
|
||||||
|
if (groups != null) {
|
||||||
|
for (String group : groups) {
|
||||||
|
if (authorize(GROUP_CACHE.get(group), action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean authorize(List<TablePermission> perms, byte[] table, byte[] family,
|
||||||
|
Permission.Action action) {
|
||||||
|
return authorize(perms, table, family, null, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean authorize(List<TablePermission> perms, byte[] table, byte[] family,
|
||||||
|
byte[] qualifier, Permission.Action action) {
|
||||||
|
if (perms != null) {
|
||||||
|
for (TablePermission p : perms) {
|
||||||
|
if (p.implies(table, family, qualifier, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("No permissions found for table="+Bytes.toStringBinary(table));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authorize(User user, byte[] table, KeyValue kv,
|
||||||
|
TablePermission.Action action) {
|
||||||
|
List<TablePermission> userPerms = getUserPermissions(
|
||||||
|
user.getShortName(), table);
|
||||||
|
if (authorize(userPerms, table, kv, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] groupNames = user.getGroupNames();
|
||||||
|
if (groupNames != null) {
|
||||||
|
for (String group : groupNames) {
|
||||||
|
List<TablePermission> groupPerms = getGroupPermissions(group, table);
|
||||||
|
if (authorize(groupPerms, table, kv, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean authorize(List<TablePermission> perms, byte[] table, KeyValue kv,
|
||||||
|
TablePermission.Action action) {
|
||||||
|
if (perms != null) {
|
||||||
|
for (TablePermission p : perms) {
|
||||||
|
if (p.implies(table, kv, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("No permissions for authorize() check, table=" +
|
||||||
|
Bytes.toStringBinary(table));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks global authorization for a specific action for a user, based on the
|
||||||
|
* stored user permissions.
|
||||||
|
*/
|
||||||
|
public boolean authorizeUser(String username, Permission.Action action) {
|
||||||
|
return authorize(USER_CACHE.get(username), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks authorization to a given table and column family for a user, based on the
|
||||||
|
* stored user permissions.
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param table
|
||||||
|
* @param family
|
||||||
|
* @param action
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean authorizeUser(String username, byte[] table, byte[] family,
|
||||||
|
Permission.Action action) {
|
||||||
|
return authorizeUser(username, table, family, null, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authorizeUser(String username, byte[] table, byte[] family,
|
||||||
|
byte[] qualifier, Permission.Action action) {
|
||||||
|
// global authorization supercedes table level
|
||||||
|
if (authorizeUser(username, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return authorize(getUserPermissions(username, table), table, family,
|
||||||
|
qualifier, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks authorization for a given action for a group, based on the stored
|
||||||
|
* permissions.
|
||||||
|
*/
|
||||||
|
public boolean authorizeGroup(String groupName, Permission.Action action) {
|
||||||
|
return authorize(GROUP_CACHE.get(groupName), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks authorization to a given table and column family for a group, based
|
||||||
|
* on the stored permissions.
|
||||||
|
* @param groupName
|
||||||
|
* @param table
|
||||||
|
* @param family
|
||||||
|
* @param action
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean authorizeGroup(String groupName, byte[] table, byte[] family,
|
||||||
|
Permission.Action action) {
|
||||||
|
// global authorization supercedes table level
|
||||||
|
if (authorizeGroup(groupName, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return authorize(getGroupPermissions(groupName, table), table, family, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authorize(User user, byte[] table, byte[] family,
|
||||||
|
byte[] qualifier, Permission.Action action) {
|
||||||
|
if (authorizeUser(user.getShortName(), table, family, qualifier, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] groups = user.getGroupNames();
|
||||||
|
if (groups != null) {
|
||||||
|
for (String group : groups) {
|
||||||
|
if (authorizeGroup(group, table, family, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authorize(User user, byte[] table, byte[] family,
|
||||||
|
Permission.Action action) {
|
||||||
|
return authorize(user, table, family, null, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given user has a {@link TablePermission} matching up
|
||||||
|
* to the column family portion of a permission. Note that this permission
|
||||||
|
* may be scoped to a given column qualifier and does not guarantee that
|
||||||
|
* authorize() on the same column family would return true.
|
||||||
|
*/
|
||||||
|
public boolean matchPermission(User user,
|
||||||
|
byte[] table, byte[] family, TablePermission.Action action) {
|
||||||
|
List<TablePermission> userPerms = getUserPermissions(
|
||||||
|
user.getShortName(), table);
|
||||||
|
if (userPerms != null) {
|
||||||
|
for (TablePermission p : userPerms) {
|
||||||
|
if (p.matchesFamily(table, family, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] groups = user.getGroupNames();
|
||||||
|
if (groups != null) {
|
||||||
|
for (String group : groups) {
|
||||||
|
List<TablePermission> groupPerms = getGroupPermissions(group, table);
|
||||||
|
if (groupPerms != null) {
|
||||||
|
for (TablePermission p : groupPerms) {
|
||||||
|
if (p.matchesFamily(table, family, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matchPermission(User user,
|
||||||
|
byte[] table, byte[] family, byte[] qualifier,
|
||||||
|
TablePermission.Action action) {
|
||||||
|
List<TablePermission> userPerms = getUserPermissions(
|
||||||
|
user.getShortName(), table);
|
||||||
|
if (userPerms != null) {
|
||||||
|
for (TablePermission p : userPerms) {
|
||||||
|
if (p.matchesFamilyQualifier(table, family, qualifier, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] groups = user.getGroupNames();
|
||||||
|
if (groups != null) {
|
||||||
|
for (String group : groups) {
|
||||||
|
List<TablePermission> groupPerms = getGroupPermissions(group, table);
|
||||||
|
if (groupPerms != null) {
|
||||||
|
for (TablePermission p : groupPerms) {
|
||||||
|
if (p.matchesFamilyQualifier(table, family, qualifier, action)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(byte[] table) {
|
||||||
|
TABLE_USER_CACHE.remove(table);
|
||||||
|
TABLE_GROUP_CACHE.remove(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites the existing permission set for a given user for a table, and
|
||||||
|
* triggers an update for zookeeper synchronization.
|
||||||
|
* @param username
|
||||||
|
* @param table
|
||||||
|
* @param perms
|
||||||
|
*/
|
||||||
|
public void setUserPermissions(String username, byte[] table,
|
||||||
|
List<TablePermission> perms) {
|
||||||
|
ListMultimap<String,TablePermission> tablePerms = TABLE_USER_CACHE.get(table);
|
||||||
|
if (tablePerms == null) {
|
||||||
|
tablePerms = ArrayListMultimap.create();
|
||||||
|
TABLE_USER_CACHE.put(table, tablePerms);
|
||||||
|
}
|
||||||
|
tablePerms.replaceValues(username, perms);
|
||||||
|
writeToZooKeeper(table, tablePerms, TABLE_GROUP_CACHE.get(table));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites the existing permission set for a group and triggers an update
|
||||||
|
* for zookeeper synchronization.
|
||||||
|
* @param group
|
||||||
|
* @param table
|
||||||
|
* @param perms
|
||||||
|
*/
|
||||||
|
public void setGroupPermissions(String group, byte[] table,
|
||||||
|
List<TablePermission> perms) {
|
||||||
|
ListMultimap<String,TablePermission> tablePerms = TABLE_GROUP_CACHE.get(table);
|
||||||
|
if (tablePerms == null) {
|
||||||
|
tablePerms = ArrayListMultimap.create();
|
||||||
|
TABLE_GROUP_CACHE.put(table, tablePerms);
|
||||||
|
}
|
||||||
|
tablePerms.replaceValues(group, perms);
|
||||||
|
writeToZooKeeper(table, TABLE_USER_CACHE.get(table), tablePerms);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeToZooKeeper(byte[] table,
|
||||||
|
ListMultimap<String,TablePermission> userPerms,
|
||||||
|
ListMultimap<String,TablePermission> groupPerms) {
|
||||||
|
ListMultimap<String,TablePermission> tmp = ArrayListMultimap.create();
|
||||||
|
if (userPerms != null) {
|
||||||
|
tmp.putAll(userPerms);
|
||||||
|
}
|
||||||
|
if (groupPerms != null) {
|
||||||
|
for (String group : groupPerms.keySet()) {
|
||||||
|
tmp.putAll(AccessControlLists.GROUP_PREFIX + group,
|
||||||
|
groupPerms.get(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] serialized = AccessControlLists.writePermissionsAsBytes(tmp, conf);
|
||||||
|
zkperms.writeToZookeeper(Bytes.toString(table), serialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<ZooKeeperWatcher,TableAuthManager> managerMap =
|
||||||
|
new HashMap<ZooKeeperWatcher,TableAuthManager>();
|
||||||
|
|
||||||
|
public synchronized static TableAuthManager get(
|
||||||
|
ZooKeeperWatcher watcher, Configuration conf) throws IOException {
|
||||||
|
instance = managerMap.get(watcher);
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new TableAuthManager(watcher, conf);
|
||||||
|
managerMap.put(watcher, instance);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,295 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.hbase.KeyValue;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
|
||||||
|
import java.io.DataInput;
|
||||||
|
import java.io.DataOutput;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an authorization for access for the given actions, optionally
|
||||||
|
* restricted to the given column family or column qualifier, over the
|
||||||
|
* given table. If the family property is <code>null</code>, it implies
|
||||||
|
* full table access.
|
||||||
|
*/
|
||||||
|
public class TablePermission extends Permission {
|
||||||
|
private static Log LOG = LogFactory.getLog(TablePermission.class);
|
||||||
|
|
||||||
|
private byte[] table;
|
||||||
|
private byte[] family;
|
||||||
|
private byte[] qualifier;
|
||||||
|
|
||||||
|
/** Nullary constructor for Writable, do not use */
|
||||||
|
public TablePermission() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new permission for the given table and (optionally) column family,
|
||||||
|
* allowing the given actions.
|
||||||
|
* @param table the table
|
||||||
|
* @param family the family, can be null if a global permission on the table
|
||||||
|
* @param assigned the list of allowed actions
|
||||||
|
*/
|
||||||
|
public TablePermission(byte[] table, byte[] family, Action... assigned) {
|
||||||
|
this(table, family, null, assigned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new permission for the given table, restricted to the given
|
||||||
|
* column family and qualifer, allowing the assigned actions to be performed.
|
||||||
|
* @param table the table
|
||||||
|
* @param family the family, can be null if a global permission on the table
|
||||||
|
* @param assigned the list of allowed actions
|
||||||
|
*/
|
||||||
|
public TablePermission(byte[] table, byte[] family, byte[] qualifier,
|
||||||
|
Action... assigned) {
|
||||||
|
super(assigned);
|
||||||
|
this.table = table;
|
||||||
|
this.family = family;
|
||||||
|
this.qualifier = qualifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new permission for the given table, family and column qualifier,
|
||||||
|
* allowing the actions matching the provided byte codes to be performed.
|
||||||
|
* @param table the table
|
||||||
|
* @param family the family, can be null if a global permission on the table
|
||||||
|
* @param actionCodes the list of allowed action codes
|
||||||
|
*/
|
||||||
|
public TablePermission(byte[] table, byte[] family, byte[] qualifier,
|
||||||
|
byte[] actionCodes) {
|
||||||
|
super(actionCodes);
|
||||||
|
this.table = table;
|
||||||
|
this.family = family;
|
||||||
|
this.qualifier = qualifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getTable() {
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getFamily() {
|
||||||
|
return family;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getQualifier() {
|
||||||
|
return qualifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that a given table operation is authorized by this permission
|
||||||
|
* instance.
|
||||||
|
*
|
||||||
|
* @param table the table where the operation is being performed
|
||||||
|
* @param family the column family to which the operation is restricted,
|
||||||
|
* if <code>null</code> implies "all"
|
||||||
|
* @param qualifier the column qualifier to which the action is restricted,
|
||||||
|
* if <code>null</code> implies "all"
|
||||||
|
* @param action the action being requested
|
||||||
|
* @return <code>true</code> if the action within the given scope is allowed
|
||||||
|
* by this permission, <code>false</code>
|
||||||
|
*/
|
||||||
|
public boolean implies(byte[] table, byte[] family, byte[] qualifier,
|
||||||
|
Action action) {
|
||||||
|
if (!Bytes.equals(this.table, table)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.family != null &&
|
||||||
|
(family == null ||
|
||||||
|
!Bytes.equals(this.family, family))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.qualifier != null &&
|
||||||
|
(qualifier == null ||
|
||||||
|
!Bytes.equals(this.qualifier, qualifier))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check actions
|
||||||
|
return super.implies(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this permission grants access to perform the given action on
|
||||||
|
* the given table and key value.
|
||||||
|
* @param table the table on which the operation is being performed
|
||||||
|
* @param kv the KeyValue on which the operation is being requested
|
||||||
|
* @param action the action requested
|
||||||
|
* @return <code>true</code> if the action is allowed over the given scope
|
||||||
|
* by this permission, otherwise <code>false</code>
|
||||||
|
*/
|
||||||
|
public boolean implies(byte[] table, KeyValue kv, Action action) {
|
||||||
|
if (!Bytes.equals(this.table, table)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family != null &&
|
||||||
|
(Bytes.compareTo(family, 0, family.length,
|
||||||
|
kv.getBuffer(), kv.getFamilyOffset(), kv.getFamilyLength()) != 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qualifier != null &&
|
||||||
|
(Bytes.compareTo(qualifier, 0, qualifier.length,
|
||||||
|
kv.getBuffer(), kv.getQualifierOffset(), kv.getQualifierLength()) != 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check actions
|
||||||
|
return super.implies(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code> if this permission matches the given column
|
||||||
|
* family at least. This only indicates a partial match against the table
|
||||||
|
* and column family, however, and does not guarantee that implies() for the
|
||||||
|
* column same family would return <code>true</code>. In the case of a
|
||||||
|
* column-qualifier specific permission, for example, implies() would still
|
||||||
|
* return false.
|
||||||
|
*/
|
||||||
|
public boolean matchesFamily(byte[] table, byte[] family, Action action) {
|
||||||
|
if (!Bytes.equals(this.table, table)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.family != null &&
|
||||||
|
(family == null ||
|
||||||
|
!Bytes.equals(this.family, family))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore qualifier
|
||||||
|
// check actions
|
||||||
|
return super.implies(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if the given permission matches the given qualifier.
|
||||||
|
* @param table the table name to match
|
||||||
|
* @param family the column family to match
|
||||||
|
* @param qualifier the qualifier name to match
|
||||||
|
* @param action the action requested
|
||||||
|
* @return <code>true</code> if the table, family and qualifier match,
|
||||||
|
* otherwise <code>false</code>
|
||||||
|
*/
|
||||||
|
public boolean matchesFamilyQualifier(byte[] table, byte[] family, byte[] qualifier,
|
||||||
|
Action action) {
|
||||||
|
if (!matchesFamily(table, family, action)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (this.qualifier != null &&
|
||||||
|
(qualifier == null ||
|
||||||
|
!Bytes.equals(this.qualifier, qualifier))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.implies(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof TablePermission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TablePermission other = (TablePermission)obj;
|
||||||
|
|
||||||
|
if (!(Bytes.equals(table, other.getTable()) &&
|
||||||
|
((family == null && other.getFamily() == null) ||
|
||||||
|
Bytes.equals(family, other.getFamily())) &&
|
||||||
|
((qualifier == null && other.getQualifier() == null) ||
|
||||||
|
Bytes.equals(qualifier, other.getQualifier()))
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check actions
|
||||||
|
return super.equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 37;
|
||||||
|
int result = super.hashCode();
|
||||||
|
if (table != null) {
|
||||||
|
result = prime * result + Bytes.hashCode(table);
|
||||||
|
}
|
||||||
|
if (family != null) {
|
||||||
|
result = prime * result + Bytes.hashCode(family);
|
||||||
|
}
|
||||||
|
if (qualifier != null) {
|
||||||
|
result = prime * result + Bytes.hashCode(qualifier);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder str = new StringBuilder("[TablePermission: ")
|
||||||
|
.append("table=").append(Bytes.toString(table))
|
||||||
|
.append(", family=").append(Bytes.toString(family))
|
||||||
|
.append(", qualifier=").append(Bytes.toString(qualifier))
|
||||||
|
.append(", actions=");
|
||||||
|
if (actions != null) {
|
||||||
|
for (int i=0; i<actions.length; i++) {
|
||||||
|
if (i > 0)
|
||||||
|
str.append(",");
|
||||||
|
if (actions[i] != null)
|
||||||
|
str.append(actions[i].toString());
|
||||||
|
else
|
||||||
|
str.append("NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str.append("]");
|
||||||
|
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFields(DataInput in) throws IOException {
|
||||||
|
super.readFields(in);
|
||||||
|
table = Bytes.readByteArray(in);
|
||||||
|
if (in.readBoolean()) {
|
||||||
|
family = Bytes.readByteArray(in);
|
||||||
|
}
|
||||||
|
if (in.readBoolean()) {
|
||||||
|
qualifier = Bytes.readByteArray(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(DataOutput out) throws IOException {
|
||||||
|
super.write(out);
|
||||||
|
Bytes.writeByteArray(out, table);
|
||||||
|
out.writeBoolean(family != null);
|
||||||
|
if (family != null) {
|
||||||
|
Bytes.writeByteArray(out, family);
|
||||||
|
}
|
||||||
|
out.writeBoolean(qualifier != null);
|
||||||
|
if (qualifier != null) {
|
||||||
|
Bytes.writeByteArray(out, qualifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
|
||||||
|
import java.io.DataInput;
|
||||||
|
import java.io.DataOutput;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an authorization for access over the given table, column family
|
||||||
|
* plus qualifier, for the given user.
|
||||||
|
*/
|
||||||
|
public class UserPermission extends TablePermission {
|
||||||
|
private static Log LOG = LogFactory.getLog(UserPermission.class);
|
||||||
|
|
||||||
|
private byte[] user;
|
||||||
|
|
||||||
|
/** Nullary constructor for Writable, do not use */
|
||||||
|
public UserPermission() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance for the given user, table and column family.
|
||||||
|
* @param user the user
|
||||||
|
* @param table the table
|
||||||
|
* @param family the family, can be null if action is allowed over the entire
|
||||||
|
* table
|
||||||
|
* @param assigned the list of allowed actions
|
||||||
|
*/
|
||||||
|
public UserPermission(byte[] user, byte[] table, byte[] family,
|
||||||
|
Action... assigned) {
|
||||||
|
super(table, family, assigned);
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new permission for the given user, table, column family and
|
||||||
|
* column qualifier.
|
||||||
|
* @param user the user
|
||||||
|
* @param table the table
|
||||||
|
* @param family the family, can be null if action is allowed over the entire
|
||||||
|
* table
|
||||||
|
* @param qualifier the column qualifier, can be null if action is allowed
|
||||||
|
* over the entire column family
|
||||||
|
* @param assigned the list of allowed actions
|
||||||
|
*/
|
||||||
|
public UserPermission(byte[] user, byte[] table, byte[] family,
|
||||||
|
byte[] qualifier, Action... assigned) {
|
||||||
|
super(table, family, qualifier, assigned);
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance for the given user, table, column family and
|
||||||
|
* qualifier, matching the actions with the given codes.
|
||||||
|
* @param user the user
|
||||||
|
* @param table the table
|
||||||
|
* @param family the family, can be null if action is allowed over the entire
|
||||||
|
* table
|
||||||
|
* @param qualifier the column qualifier, can be null if action is allowed
|
||||||
|
* over the entire column family
|
||||||
|
* @param actionCodes the list of allowed action codes
|
||||||
|
*/
|
||||||
|
public UserPermission(byte[] user, byte[] table, byte[] family,
|
||||||
|
byte[] qualifier, byte[] actionCodes) {
|
||||||
|
super(table, family, qualifier, actionCodes);
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof UserPermission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UserPermission other = (UserPermission)obj;
|
||||||
|
|
||||||
|
if ((Bytes.equals(user, other.getUser()) &&
|
||||||
|
super.equals(obj))) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 37;
|
||||||
|
int result = super.hashCode();
|
||||||
|
if (user != null) {
|
||||||
|
result = prime * result + Bytes.hashCode(user);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder str = new StringBuilder("UserPermission: ")
|
||||||
|
.append("user=").append(Bytes.toString(user))
|
||||||
|
.append(", ").append(super.toString());
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFields(DataInput in) throws IOException {
|
||||||
|
super.readFields(in);
|
||||||
|
user = Bytes.readByteArray(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(DataOutput out) throws IOException {
|
||||||
|
super.write(out);
|
||||||
|
Bytes.writeByteArray(out, user);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
import org.apache.hadoop.hbase.zookeeper.ZKUtil;
|
||||||
|
import org.apache.hadoop.hbase.zookeeper.ZooKeeperListener;
|
||||||
|
import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
|
||||||
|
import org.apache.zookeeper.KeeperException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles synchronization of access control list entries and updates
|
||||||
|
* throughout all nodes in the cluster. The {@link AccessController} instance
|
||||||
|
* on the {@code _acl_} table regions, creates a znode for each table as
|
||||||
|
* {@code /hbase/acl/tablename}, with the znode data containing a serialized
|
||||||
|
* list of the permissions granted for the table. The {@code AccessController}
|
||||||
|
* instances on all other cluster hosts watch the znodes for updates, which
|
||||||
|
* trigger updates in the {@link TableAuthManager} permission cache.
|
||||||
|
*/
|
||||||
|
public class ZKPermissionWatcher extends ZooKeeperListener {
|
||||||
|
private static Log LOG = LogFactory.getLog(ZKPermissionWatcher.class);
|
||||||
|
// parent node for permissions lists
|
||||||
|
static final String ACL_NODE = "acl";
|
||||||
|
TableAuthManager authManager;
|
||||||
|
String aclZNode;
|
||||||
|
|
||||||
|
public ZKPermissionWatcher(ZooKeeperWatcher watcher,
|
||||||
|
TableAuthManager authManager, Configuration conf) {
|
||||||
|
super(watcher);
|
||||||
|
this.authManager = authManager;
|
||||||
|
String aclZnodeParent = conf.get("zookeeper.znode.acl.parent", ACL_NODE);
|
||||||
|
this.aclZNode = ZKUtil.joinZNode(watcher.baseZNode, aclZnodeParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() throws KeeperException {
|
||||||
|
watcher.registerListener(this);
|
||||||
|
if (ZKUtil.watchAndCheckExists(watcher, aclZNode)) {
|
||||||
|
List<ZKUtil.NodeAndData> existing =
|
||||||
|
ZKUtil.getChildDataAndWatchForNewChildren(watcher, aclZNode);
|
||||||
|
if (existing != null) {
|
||||||
|
refreshNodes(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void nodeCreated(String path) {
|
||||||
|
if (path.equals(aclZNode)) {
|
||||||
|
try {
|
||||||
|
List<ZKUtil.NodeAndData> nodes =
|
||||||
|
ZKUtil.getChildDataAndWatchForNewChildren(watcher, aclZNode);
|
||||||
|
refreshNodes(nodes);
|
||||||
|
} catch (KeeperException ke) {
|
||||||
|
LOG.error("Error reading data from zookeeper", ke);
|
||||||
|
// only option is to abort
|
||||||
|
watcher.abort("Zookeeper error obtaining acl node children", ke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void nodeDeleted(String path) {
|
||||||
|
if (aclZNode.equals(ZKUtil.getParent(path))) {
|
||||||
|
String table = ZKUtil.getNodeName(path);
|
||||||
|
authManager.remove(Bytes.toBytes(table));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void nodeDataChanged(String path) {
|
||||||
|
if (aclZNode.equals(ZKUtil.getParent(path))) {
|
||||||
|
// update cache on an existing table node
|
||||||
|
String table = ZKUtil.getNodeName(path);
|
||||||
|
try {
|
||||||
|
byte[] data = ZKUtil.getDataAndWatch(watcher, path);
|
||||||
|
authManager.refreshCacheFromWritable(Bytes.toBytes(table), data);
|
||||||
|
} catch (KeeperException ke) {
|
||||||
|
LOG.error("Error reading data from zookeeper for node "+table, ke);
|
||||||
|
// only option is to abort
|
||||||
|
watcher.abort("Zookeeper error getting data for node " + table, ke);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
LOG.error("Error reading permissions writables", ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void nodeChildrenChanged(String path) {
|
||||||
|
if (path.equals(aclZNode)) {
|
||||||
|
// table permissions changed
|
||||||
|
try {
|
||||||
|
List<ZKUtil.NodeAndData> nodes =
|
||||||
|
ZKUtil.getChildDataAndWatchForNewChildren(watcher, aclZNode);
|
||||||
|
refreshNodes(nodes);
|
||||||
|
} catch (KeeperException ke) {
|
||||||
|
LOG.error("Error reading data from zookeeper for path "+path, ke);
|
||||||
|
watcher.abort("Zookeeper error get node children for path "+path, ke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshNodes(List<ZKUtil.NodeAndData> nodes) {
|
||||||
|
for (ZKUtil.NodeAndData n : nodes) {
|
||||||
|
if (n.isEmpty()) continue;
|
||||||
|
String path = n.getNode();
|
||||||
|
String table = ZKUtil.getNodeName(path);
|
||||||
|
try {
|
||||||
|
byte[] nodeData = n.getData();
|
||||||
|
if (LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("Updating permissions cache from node "+table+" with data: "+
|
||||||
|
Bytes.toStringBinary(nodeData));
|
||||||
|
}
|
||||||
|
authManager.refreshCacheFromWritable(Bytes.toBytes(table),
|
||||||
|
nodeData);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
LOG.error("Failed parsing permissions for table '" + table +
|
||||||
|
"' from zk", ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Write a table's access controls to the permissions mirror in zookeeper
|
||||||
|
* @param tableName
|
||||||
|
* @param permsData
|
||||||
|
*/
|
||||||
|
public void writeToZookeeper(String tableName,
|
||||||
|
byte[] permsData) {
|
||||||
|
String zkNode =
|
||||||
|
ZKUtil.joinZNode(ZKUtil.joinZNode(watcher.baseZNode, ACL_NODE),
|
||||||
|
tableName);
|
||||||
|
try {
|
||||||
|
ZKUtil.createWithParents(watcher, zkNode);
|
||||||
|
ZKUtil.updateExistingNodeData(watcher, zkNode,
|
||||||
|
permsData, -1);
|
||||||
|
} catch (KeeperException e) {
|
||||||
|
LOG.error("Failed updating permissions for table '" + tableName +
|
||||||
|
"'", e);
|
||||||
|
watcher.abort("Failed writing node "+zkNode+" to zookeeper", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.hbase.ipc.SecureRpcEngine;
|
||||||
|
import org.apache.hadoop.hbase.security.User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for testing security
|
||||||
|
*/
|
||||||
|
public class SecureTestUtil {
|
||||||
|
public static void enableSecurity(Configuration conf) throws IOException {
|
||||||
|
conf.set("hadoop.security.authorization", "false");
|
||||||
|
conf.set("hadoop.security.authentication", "simple");
|
||||||
|
conf.set("hbase.rpc.engine", SecureRpcEngine.class.getName());
|
||||||
|
conf.set("hbase.coprocessor.master.classes", AccessController.class.getName());
|
||||||
|
conf.set("hbase.coprocessor.region.classes", AccessController.class.getName());
|
||||||
|
// add the process running user to superusers
|
||||||
|
String currentUser = User.getCurrent().getName();
|
||||||
|
conf.set("hbase.superuser", "admin,"+currentUser);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* 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.security.PrivilegedExceptionAction;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
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.HBaseTestingUtility;
|
||||||
|
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.ResultScanner;
|
||||||
|
import org.apache.hadoop.hbase.client.Scan;
|
||||||
|
import org.apache.hadoop.hbase.security.AccessDeniedException;
|
||||||
|
import org.apache.hadoop.hbase.security.User;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class TestAccessControlFilter {
|
||||||
|
private static Log LOG = LogFactory.getLog(TestAccessControlFilter.class);
|
||||||
|
private static HBaseTestingUtility TEST_UTIL;
|
||||||
|
|
||||||
|
private static User ADMIN;
|
||||||
|
private static User READER;
|
||||||
|
private static User LIMITED;
|
||||||
|
private static User DENIED;
|
||||||
|
|
||||||
|
private static byte[] TABLE = Bytes.toBytes("testtable");
|
||||||
|
private static byte[] FAMILY = Bytes.toBytes("f1");
|
||||||
|
private static byte[] PRIVATE_COL = Bytes.toBytes("private");
|
||||||
|
private static byte[] PUBLIC_COL = Bytes.toBytes("public");
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setupBeforeClass() throws Exception {
|
||||||
|
TEST_UTIL = new HBaseTestingUtility();
|
||||||
|
Configuration conf = TEST_UTIL.getConfiguration();
|
||||||
|
SecureTestUtil.enableSecurity(conf);
|
||||||
|
String baseuser = User.getCurrent().getShortName();
|
||||||
|
conf.set("hbase.superuser", conf.get("hbase.superuser", "") +
|
||||||
|
String.format(",%s.hfs.0,%s.hfs.1,%s.hfs.2", baseuser, baseuser, baseuser));
|
||||||
|
TEST_UTIL.startMiniCluster();
|
||||||
|
TEST_UTIL.waitTableAvailable(AccessControlLists.ACL_TABLE_NAME, 5000);
|
||||||
|
|
||||||
|
ADMIN = User.createUserForTesting(conf, "admin", new String[]{"supergroup"});
|
||||||
|
READER = User.createUserForTesting(conf, "reader", new String[0]);
|
||||||
|
LIMITED = User.createUserForTesting(conf, "limited", new String[0]);
|
||||||
|
DENIED = User.createUserForTesting(conf, "denied", new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void tearDownAfterClass() throws Exception {
|
||||||
|
TEST_UTIL.shutdownMiniCluster();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQualifierAccess() throws Exception {
|
||||||
|
final HTable table = TEST_UTIL.createTable(TABLE, FAMILY);
|
||||||
|
|
||||||
|
// set permissions
|
||||||
|
ADMIN.runAs(new PrivilegedExceptionAction<Object>() {
|
||||||
|
@Override
|
||||||
|
public Object run() throws Exception {
|
||||||
|
HTable aclmeta = new HTable(TEST_UTIL.getConfiguration(),
|
||||||
|
AccessControlLists.ACL_TABLE_NAME);
|
||||||
|
AccessControllerProtocol acls = aclmeta.coprocessorProxy(
|
||||||
|
AccessControllerProtocol.class, Bytes.toBytes("testtable"));
|
||||||
|
TablePermission perm = new TablePermission(TABLE, null, Permission.Action.READ);
|
||||||
|
acls.grant(Bytes.toBytes(READER.getShortName()), perm);
|
||||||
|
perm = new TablePermission(TABLE, FAMILY, PUBLIC_COL, Permission.Action.READ);
|
||||||
|
acls.grant(Bytes.toBytes(LIMITED.getShortName()), perm);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// put some test data
|
||||||
|
List<Put> puts = new ArrayList<Put>(100);
|
||||||
|
for (int i=0; i<100; i++) {
|
||||||
|
Put p = new Put(Bytes.toBytes(i));
|
||||||
|
p.add(FAMILY, PRIVATE_COL, Bytes.toBytes("secret "+i));
|
||||||
|
p.add(FAMILY, PUBLIC_COL, Bytes.toBytes("info "+i));
|
||||||
|
puts.add(p);
|
||||||
|
}
|
||||||
|
table.put(puts);
|
||||||
|
|
||||||
|
// test read
|
||||||
|
READER.runAs(new PrivilegedExceptionAction<Object>() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Configuration conf = new Configuration(TEST_UTIL.getConfiguration());
|
||||||
|
// force a new RS connection
|
||||||
|
conf.set("testkey", UUID.randomUUID().toString());
|
||||||
|
HTable t = new HTable(conf, TABLE);
|
||||||
|
ResultScanner rs = t.getScanner(new Scan());
|
||||||
|
int rowcnt = 0;
|
||||||
|
for (Result r : rs) {
|
||||||
|
rowcnt++;
|
||||||
|
int rownum = Bytes.toInt(r.getRow());
|
||||||
|
assertTrue(r.containsColumn(FAMILY, PRIVATE_COL));
|
||||||
|
assertEquals("secret "+rownum, Bytes.toString(r.getValue(FAMILY, PRIVATE_COL)));
|
||||||
|
assertTrue(r.containsColumn(FAMILY, PUBLIC_COL));
|
||||||
|
assertEquals("info "+rownum, Bytes.toString(r.getValue(FAMILY, PUBLIC_COL)));
|
||||||
|
}
|
||||||
|
assertEquals("Expected 100 rows returned", 100, rowcnt);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// test read with qualifier filter
|
||||||
|
LIMITED.runAs(new PrivilegedExceptionAction<Object>() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Configuration conf = new Configuration(TEST_UTIL.getConfiguration());
|
||||||
|
// force a new RS connection
|
||||||
|
conf.set("testkey", UUID.randomUUID().toString());
|
||||||
|
HTable t = new HTable(conf, TABLE);
|
||||||
|
ResultScanner rs = t.getScanner(new Scan());
|
||||||
|
int rowcnt = 0;
|
||||||
|
for (Result r : rs) {
|
||||||
|
rowcnt++;
|
||||||
|
int rownum = Bytes.toInt(r.getRow());
|
||||||
|
assertFalse(r.containsColumn(FAMILY, PRIVATE_COL));
|
||||||
|
assertTrue(r.containsColumn(FAMILY, PUBLIC_COL));
|
||||||
|
assertEquals("info " + rownum, Bytes.toString(r.getValue(FAMILY, PUBLIC_COL)));
|
||||||
|
}
|
||||||
|
assertEquals("Expected 100 rows returned", 100, rowcnt);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// test as user with no permission
|
||||||
|
DENIED.runAs(new PrivilegedExceptionAction(){
|
||||||
|
public Object run() throws Exception {
|
||||||
|
try {
|
||||||
|
Configuration conf = new Configuration(TEST_UTIL.getConfiguration());
|
||||||
|
// force a new RS connection
|
||||||
|
conf.set("testkey", UUID.randomUUID().toString());
|
||||||
|
HTable t = new HTable(conf, TABLE);
|
||||||
|
ResultScanner rs = t.getScanner(new Scan());
|
||||||
|
fail("Attempt to open scanner should have been denied");
|
||||||
|
} catch (AccessDeniedException ade) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,980 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.io.IOException;
|
||||||
|
import java.security.PrivilegedExceptionAction;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
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.HRegionInfo;
|
||||||
|
import org.apache.hadoop.hbase.HServerAddress;
|
||||||
|
import org.apache.hadoop.hbase.HTableDescriptor;
|
||||||
|
import org.apache.hadoop.hbase.ServerName;
|
||||||
|
import org.apache.hadoop.hbase.client.*;
|
||||||
|
import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment;
|
||||||
|
import org.apache.hadoop.hbase.coprocessor.ObserverContext;
|
||||||
|
import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
|
||||||
|
import org.apache.hadoop.hbase.security.AccessDeniedException;
|
||||||
|
import org.apache.hadoop.hbase.security.User;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs authorization checks for common operations, according to different
|
||||||
|
* levels of authorized users.
|
||||||
|
*/
|
||||||
|
public class TestAccessController {
|
||||||
|
private static Log LOG = LogFactory.getLog(TestAccessController.class);
|
||||||
|
private static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
|
||||||
|
private static Configuration conf;
|
||||||
|
|
||||||
|
// user with all permissions
|
||||||
|
private static User SUPERUSER;
|
||||||
|
// table owner user
|
||||||
|
private static User USER_OWNER;
|
||||||
|
// user with rw permissions
|
||||||
|
private static User USER_RW;
|
||||||
|
// user with read-only permissions
|
||||||
|
private static User USER_RO;
|
||||||
|
// user with no permissions
|
||||||
|
private static User USER_NONE;
|
||||||
|
|
||||||
|
private static byte[] TEST_TABLE = Bytes.toBytes("testtable");
|
||||||
|
private static byte[] TEST_FAMILY = Bytes.toBytes("f1");
|
||||||
|
|
||||||
|
private static MasterCoprocessorEnvironment CP_ENV;
|
||||||
|
private static AccessController ACCESS_CONTROLLER;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setupBeforeClass() throws Exception {
|
||||||
|
// setup configuration
|
||||||
|
conf = TEST_UTIL.getConfiguration();
|
||||||
|
SecureTestUtil.enableSecurity(conf);
|
||||||
|
|
||||||
|
TEST_UTIL.startMiniCluster();
|
||||||
|
MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster()
|
||||||
|
.getMaster().getCoprocessorHost();
|
||||||
|
cpHost.load(AccessController.class, Coprocessor.PRIORITY_HIGHEST, conf);
|
||||||
|
ACCESS_CONTROLLER = (AccessController)cpHost.findCoprocessor(
|
||||||
|
AccessController.class.getName());
|
||||||
|
CP_ENV = cpHost.createEnvironment(AccessController.class, ACCESS_CONTROLLER,
|
||||||
|
Coprocessor.PRIORITY_HIGHEST, 1, conf);
|
||||||
|
|
||||||
|
// create a set of test users
|
||||||
|
SUPERUSER = User.createUserForTesting(conf, "admin", new String[]{"supergroup"});
|
||||||
|
USER_OWNER = User.createUserForTesting(conf, "owner", new String[0]);
|
||||||
|
USER_RW = User.createUserForTesting(conf, "rwuser", new String[0]);
|
||||||
|
USER_RO = User.createUserForTesting(conf, "rouser", new String[0]);
|
||||||
|
USER_NONE = User.createUserForTesting(conf, "nouser", new String[0]);
|
||||||
|
|
||||||
|
HBaseAdmin admin = TEST_UTIL.getHBaseAdmin();
|
||||||
|
HTableDescriptor htd = new HTableDescriptor(TEST_TABLE);
|
||||||
|
htd.addFamily(new HColumnDescriptor(TEST_FAMILY));
|
||||||
|
htd.setOwnerString(USER_OWNER.getShortName());
|
||||||
|
admin.createTable(htd);
|
||||||
|
|
||||||
|
// initilize access control
|
||||||
|
HTable meta = new HTable(conf, AccessControlLists.ACL_TABLE_NAME);
|
||||||
|
AccessControllerProtocol protocol =
|
||||||
|
meta.coprocessorProxy(AccessControllerProtocol.class, TEST_TABLE);
|
||||||
|
protocol.grant(Bytes.toBytes(USER_RW.getShortName()),
|
||||||
|
new TablePermission(TEST_TABLE, TEST_FAMILY, Permission.Action.READ,
|
||||||
|
Permission.Action.WRITE));
|
||||||
|
|
||||||
|
protocol.grant(Bytes.toBytes(USER_RO.getShortName()),
|
||||||
|
new TablePermission(TEST_TABLE, TEST_FAMILY, Permission.Action.READ));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void tearDownAfterClass() throws Exception {
|
||||||
|
TEST_UTIL.shutdownMiniCluster();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyAllowed(User user, PrivilegedExceptionAction action)
|
||||||
|
throws Exception {
|
||||||
|
try {
|
||||||
|
user.runAs(action);
|
||||||
|
} catch (AccessDeniedException ade) {
|
||||||
|
fail("Expected action to pass for user '" + user.getShortName() +
|
||||||
|
"' but was denied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyDenied(User user, PrivilegedExceptionAction action)
|
||||||
|
throws Exception {
|
||||||
|
try {
|
||||||
|
user.runAs(action);
|
||||||
|
fail("Expected AccessDeniedException for user '" + user.getShortName() + "'");
|
||||||
|
} catch (RetriesExhaustedWithDetailsException e) {
|
||||||
|
// in case of batch operations, and put, the client assembles a
|
||||||
|
// RetriesExhaustedWithDetailsException instead of throwing an
|
||||||
|
// AccessDeniedException
|
||||||
|
boolean isAccessDeniedException = false;
|
||||||
|
for ( Throwable ex : e.getCauses()) {
|
||||||
|
if (ex instanceof AccessDeniedException) {
|
||||||
|
isAccessDeniedException = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isAccessDeniedException ) {
|
||||||
|
fail("Not receiving AccessDeniedException for user '" +
|
||||||
|
user.getShortName() + "'");
|
||||||
|
}
|
||||||
|
} catch (AccessDeniedException ade) {
|
||||||
|
// expected result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTableCreate() throws Exception {
|
||||||
|
PrivilegedExceptionAction createTable = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
HTableDescriptor htd = new HTableDescriptor("testnewtable");
|
||||||
|
htd.addFamily(new HColumnDescriptor(TEST_FAMILY));
|
||||||
|
ACCESS_CONTROLLER.preCreateTable(
|
||||||
|
ObserverContext.createAndPrepare(CP_ENV, null), htd, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, createTable);
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, createTable);
|
||||||
|
verifyDenied(USER_RW, createTable);
|
||||||
|
verifyDenied(USER_RO, createTable);
|
||||||
|
verifyDenied(USER_NONE, createTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTableModify() throws Exception {
|
||||||
|
PrivilegedExceptionAction disableTable = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
HTableDescriptor htd = new HTableDescriptor(TEST_TABLE);
|
||||||
|
htd.addFamily(new HColumnDescriptor(TEST_FAMILY));
|
||||||
|
htd.addFamily(new HColumnDescriptor("fam_"+User.getCurrent().getShortName()));
|
||||||
|
ACCESS_CONTROLLER.preModifyTable(ObserverContext.createAndPrepare(CP_ENV, null), TEST_TABLE, htd);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, disableTable);
|
||||||
|
verifyDenied(USER_RW, disableTable);
|
||||||
|
verifyDenied(USER_RO, disableTable);
|
||||||
|
verifyDenied(USER_NONE, disableTable);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, disableTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTableDelete() throws Exception {
|
||||||
|
PrivilegedExceptionAction disableTable = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preDeleteTable(ObserverContext.createAndPrepare(CP_ENV, null), TEST_TABLE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, disableTable);
|
||||||
|
verifyDenied(USER_RW, disableTable);
|
||||||
|
verifyDenied(USER_RO, disableTable);
|
||||||
|
verifyDenied(USER_NONE, disableTable);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, disableTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddColumn() throws Exception {
|
||||||
|
final HColumnDescriptor hcd = new HColumnDescriptor("fam_new");
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preAddColumn(ObserverContext.createAndPrepare(CP_ENV, null), TEST_TABLE, hcd);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testModifyColumn() throws Exception {
|
||||||
|
final HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY);
|
||||||
|
hcd.setMaxVersions(10);
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preModifyColumn(ObserverContext.createAndPrepare(CP_ENV, null), TEST_TABLE, hcd);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeleteColumn() throws Exception {
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preDeleteColumn(ObserverContext.createAndPrepare(CP_ENV, null), TEST_TABLE, TEST_FAMILY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTableDisable() throws Exception {
|
||||||
|
PrivilegedExceptionAction disableTable = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preDisableTable(ObserverContext.createAndPrepare(CP_ENV, null), TEST_TABLE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, disableTable);
|
||||||
|
verifyDenied(USER_RW, disableTable);
|
||||||
|
verifyDenied(USER_RO, disableTable);
|
||||||
|
verifyDenied(USER_NONE, disableTable);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, disableTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTableEnable() throws Exception {
|
||||||
|
PrivilegedExceptionAction enableTable = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preEnableTable(ObserverContext.createAndPrepare(CP_ENV, null), TEST_TABLE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, enableTable);
|
||||||
|
verifyDenied(USER_RW, enableTable);
|
||||||
|
verifyDenied(USER_RO, enableTable);
|
||||||
|
verifyDenied(USER_NONE, enableTable);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, enableTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMove() throws Exception {
|
||||||
|
HTable table = new HTable(TEST_UTIL.getConfiguration(), TEST_TABLE);
|
||||||
|
Map<HRegionInfo,HServerAddress> regions = table.getRegionsInfo();
|
||||||
|
final Map.Entry<HRegionInfo,HServerAddress> firstRegion =
|
||||||
|
regions.entrySet().iterator().next();
|
||||||
|
final ServerName server = TEST_UTIL.getHBaseCluster().getRegionServer(0).getServerName();
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preMove(ObserverContext.createAndPrepare(CP_ENV, null),
|
||||||
|
firstRegion.getKey(), server, server);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAssign() throws Exception {
|
||||||
|
HTable table = new HTable(TEST_UTIL.getConfiguration(), TEST_TABLE);
|
||||||
|
Map<HRegionInfo,HServerAddress> regions = table.getRegionsInfo();
|
||||||
|
final Map.Entry<HRegionInfo,HServerAddress> firstRegion =
|
||||||
|
regions.entrySet().iterator().next();
|
||||||
|
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preAssign(ObserverContext.createAndPrepare(CP_ENV, null),
|
||||||
|
firstRegion.getKey());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUnassign() throws Exception {
|
||||||
|
HTable table = new HTable(TEST_UTIL.getConfiguration(), TEST_TABLE);
|
||||||
|
Map<HRegionInfo,HServerAddress> regions = table.getRegionsInfo();
|
||||||
|
final Map.Entry<HRegionInfo,HServerAddress> firstRegion =
|
||||||
|
regions.entrySet().iterator().next();
|
||||||
|
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preUnassign(ObserverContext.createAndPrepare(CP_ENV, null),
|
||||||
|
firstRegion.getKey(), false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBalance() throws Exception {
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preBalance(ObserverContext.createAndPrepare(CP_ENV, null));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBalanceSwitch() throws Exception {
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preBalanceSwitch(ObserverContext.createAndPrepare(CP_ENV, null), true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShutdown() throws Exception {
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preShutdown(ObserverContext.createAndPrepare(CP_ENV, null));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStopMaster() throws Exception {
|
||||||
|
PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
ACCESS_CONTROLLER.preStopMaster(ObserverContext.createAndPrepare(CP_ENV, null));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// all others should be denied
|
||||||
|
verifyDenied(USER_OWNER, action);
|
||||||
|
verifyDenied(USER_RW, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// verify that superuser can create tables
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyWrite(PrivilegedExceptionAction action) throws Exception {
|
||||||
|
// should be denied
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
verifyDenied(USER_RO, action);
|
||||||
|
|
||||||
|
// should be allowed
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
verifyAllowed(USER_OWNER, action);
|
||||||
|
verifyAllowed(USER_RW, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyRead(PrivilegedExceptionAction action) throws Exception {
|
||||||
|
// should be denied
|
||||||
|
verifyDenied(USER_NONE, action);
|
||||||
|
|
||||||
|
// should be allowed
|
||||||
|
verifyAllowed(SUPERUSER, action);
|
||||||
|
verifyAllowed(USER_OWNER, action);
|
||||||
|
verifyAllowed(USER_RW, action);
|
||||||
|
verifyAllowed(USER_RO, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRead() throws Exception {
|
||||||
|
// get action
|
||||||
|
PrivilegedExceptionAction getAction = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Get g = new Get(Bytes.toBytes("random_row"));
|
||||||
|
g.addFamily(TEST_FAMILY);
|
||||||
|
HTable t = new HTable(conf, TEST_TABLE);
|
||||||
|
t.get(g);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
verifyRead(getAction);
|
||||||
|
|
||||||
|
// action for scanning
|
||||||
|
PrivilegedExceptionAction scanAction = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Scan s = new Scan();
|
||||||
|
s.addFamily(TEST_FAMILY);
|
||||||
|
|
||||||
|
HTable table = new HTable(conf, TEST_TABLE);
|
||||||
|
ResultScanner scanner = table.getScanner(s);
|
||||||
|
try {
|
||||||
|
for (Result r = scanner.next(); r != null; r = scanner.next()) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
} finally {
|
||||||
|
scanner.close();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
verifyRead(scanAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// test put, delete, increment
|
||||||
|
public void testWrite() throws Exception {
|
||||||
|
// put action
|
||||||
|
PrivilegedExceptionAction putAction = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Put p = new Put(Bytes.toBytes("random_row"));
|
||||||
|
p.add(TEST_FAMILY, Bytes.toBytes("Qualifier"), Bytes.toBytes(1));
|
||||||
|
HTable t = new HTable(conf, TEST_TABLE);
|
||||||
|
t.put(p);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
verifyWrite(putAction);
|
||||||
|
|
||||||
|
// delete action
|
||||||
|
PrivilegedExceptionAction deleteAction = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Delete d = new Delete(Bytes.toBytes("random_row"));
|
||||||
|
d.deleteFamily(TEST_FAMILY);
|
||||||
|
HTable t = new HTable(conf, TEST_TABLE);
|
||||||
|
t.delete(d);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
verifyWrite(deleteAction);
|
||||||
|
|
||||||
|
// increment action
|
||||||
|
PrivilegedExceptionAction incrementAction = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Increment inc = new Increment(Bytes.toBytes("random_row"));
|
||||||
|
inc.addColumn(TEST_FAMILY, Bytes.toBytes("Qualifier"), 1);
|
||||||
|
HTable t = new HTable(conf, TEST_TABLE);
|
||||||
|
t.increment(inc);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
verifyWrite(incrementAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGrantRevoke() throws Exception {
|
||||||
|
final byte[] tableName = Bytes.toBytes("TempTable");
|
||||||
|
final byte[] family1 = Bytes.toBytes("f1");
|
||||||
|
final byte[] family2 = Bytes.toBytes("f2");
|
||||||
|
final byte[] qualifier = Bytes.toBytes("q");
|
||||||
|
|
||||||
|
// create table
|
||||||
|
HBaseAdmin admin = TEST_UTIL.getHBaseAdmin();
|
||||||
|
if (admin.tableExists(tableName)) {
|
||||||
|
admin.disableTable(tableName);
|
||||||
|
admin.deleteTable(tableName);
|
||||||
|
}
|
||||||
|
HTableDescriptor htd = new HTableDescriptor(tableName);
|
||||||
|
htd.addFamily(new HColumnDescriptor(family1));
|
||||||
|
htd.addFamily(new HColumnDescriptor(family2));
|
||||||
|
htd.setOwnerString(USER_OWNER.getShortName());
|
||||||
|
admin.createTable(htd);
|
||||||
|
|
||||||
|
// create temp users
|
||||||
|
User user = User.createUserForTesting(TEST_UTIL.getConfiguration(),
|
||||||
|
"user", new String[0]);
|
||||||
|
|
||||||
|
// perms only stored against the first region
|
||||||
|
HTable acl = new HTable(conf, AccessControlLists.ACL_TABLE_NAME);
|
||||||
|
AccessControllerProtocol protocol =
|
||||||
|
acl.coprocessorProxy(AccessControllerProtocol.class,
|
||||||
|
tableName);
|
||||||
|
|
||||||
|
// prepare actions:
|
||||||
|
PrivilegedExceptionAction putActionAll = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Put p = new Put(Bytes.toBytes("a"));
|
||||||
|
p.add(family1, qualifier, Bytes.toBytes("v1"));
|
||||||
|
p.add(family2, qualifier, Bytes.toBytes("v2"));
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.put(p);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction putAction1 = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Put p = new Put(Bytes.toBytes("a"));
|
||||||
|
p.add(family1, qualifier, Bytes.toBytes("v1"));
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.put(p);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction putAction2 = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Put p = new Put(Bytes.toBytes("a"));
|
||||||
|
p.add(family2, qualifier, Bytes.toBytes("v2"));
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.put(p);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction getActionAll = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Get g = new Get(Bytes.toBytes("random_row"));
|
||||||
|
g.addFamily(family1);
|
||||||
|
g.addFamily(family2);
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.get(g);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction getAction1 = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Get g = new Get(Bytes.toBytes("random_row"));
|
||||||
|
g.addFamily(family1);
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.get(g);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction getAction2 = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Get g = new Get(Bytes.toBytes("random_row"));
|
||||||
|
g.addFamily(family2);
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.get(g);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction deleteActionAll = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Delete d = new Delete(Bytes.toBytes("random_row"));
|
||||||
|
d.deleteFamily(family1);
|
||||||
|
d.deleteFamily(family2);
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.delete(d);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction deleteAction1 = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Delete d = new Delete(Bytes.toBytes("random_row"));
|
||||||
|
d.deleteFamily(family1);
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.delete(d);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction deleteAction2 = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Delete d = new Delete(Bytes.toBytes("random_row"));
|
||||||
|
d.deleteFamily(family2);
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.delete(d);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// initial check:
|
||||||
|
verifyDenied(user, getActionAll);
|
||||||
|
verifyDenied(user, getAction1);
|
||||||
|
verifyDenied(user, getAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, putActionAll);
|
||||||
|
verifyDenied(user, putAction1);
|
||||||
|
verifyDenied(user, putAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, deleteActionAll);
|
||||||
|
verifyDenied(user, deleteAction1);
|
||||||
|
verifyDenied(user, deleteAction2);
|
||||||
|
|
||||||
|
// grant table read permission
|
||||||
|
protocol.grant(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, null, Permission.Action.READ));
|
||||||
|
Thread.sleep(100);
|
||||||
|
// check
|
||||||
|
verifyAllowed(user, getActionAll);
|
||||||
|
verifyAllowed(user, getAction1);
|
||||||
|
verifyAllowed(user, getAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, putActionAll);
|
||||||
|
verifyDenied(user, putAction1);
|
||||||
|
verifyDenied(user, putAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, deleteActionAll);
|
||||||
|
verifyDenied(user, deleteAction1);
|
||||||
|
verifyDenied(user, deleteAction2);
|
||||||
|
|
||||||
|
// grant table write permission
|
||||||
|
protocol.grant(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, null, Permission.Action.WRITE));
|
||||||
|
Thread.sleep(100);
|
||||||
|
verifyDenied(user, getActionAll);
|
||||||
|
verifyDenied(user, getAction1);
|
||||||
|
verifyDenied(user, getAction2);
|
||||||
|
|
||||||
|
verifyAllowed(user, putActionAll);
|
||||||
|
verifyAllowed(user, putAction1);
|
||||||
|
verifyAllowed(user, putAction2);
|
||||||
|
|
||||||
|
verifyAllowed(user, deleteActionAll);
|
||||||
|
verifyAllowed(user, deleteAction1);
|
||||||
|
verifyAllowed(user, deleteAction2);
|
||||||
|
|
||||||
|
// revoke table permission
|
||||||
|
protocol.grant(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, null, Permission.Action.READ,
|
||||||
|
Permission.Action.WRITE));
|
||||||
|
|
||||||
|
protocol.revoke(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, null));
|
||||||
|
Thread.sleep(100);
|
||||||
|
verifyDenied(user, getActionAll);
|
||||||
|
verifyDenied(user, getAction1);
|
||||||
|
verifyDenied(user, getAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, putActionAll);
|
||||||
|
verifyDenied(user, putAction1);
|
||||||
|
verifyDenied(user, putAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, deleteActionAll);
|
||||||
|
verifyDenied(user, deleteAction1);
|
||||||
|
verifyDenied(user, deleteAction2);
|
||||||
|
|
||||||
|
// grant column family read permission
|
||||||
|
protocol.grant(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, family1, Permission.Action.READ));
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
verifyAllowed(user, getActionAll);
|
||||||
|
verifyAllowed(user, getAction1);
|
||||||
|
verifyDenied(user, getAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, putActionAll);
|
||||||
|
verifyDenied(user, putAction1);
|
||||||
|
verifyDenied(user, putAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, deleteActionAll);
|
||||||
|
verifyDenied(user, deleteAction1);
|
||||||
|
verifyDenied(user, deleteAction2);
|
||||||
|
|
||||||
|
// grant column family write permission
|
||||||
|
protocol.grant(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, family2, Permission.Action.WRITE));
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
verifyAllowed(user, getActionAll);
|
||||||
|
verifyAllowed(user, getAction1);
|
||||||
|
verifyDenied(user, getAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, putActionAll);
|
||||||
|
verifyDenied(user, putAction1);
|
||||||
|
verifyAllowed(user, putAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, deleteActionAll);
|
||||||
|
verifyDenied(user, deleteAction1);
|
||||||
|
verifyAllowed(user, deleteAction2);
|
||||||
|
|
||||||
|
// revoke column family permission
|
||||||
|
protocol.revoke(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, family2));
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
verifyAllowed(user, getActionAll);
|
||||||
|
verifyAllowed(user, getAction1);
|
||||||
|
verifyDenied(user, getAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, putActionAll);
|
||||||
|
verifyDenied(user, putAction1);
|
||||||
|
verifyDenied(user, putAction2);
|
||||||
|
|
||||||
|
verifyDenied(user, deleteActionAll);
|
||||||
|
verifyDenied(user, deleteAction1);
|
||||||
|
verifyDenied(user, deleteAction2);
|
||||||
|
|
||||||
|
// delete table
|
||||||
|
admin.disableTable(tableName);
|
||||||
|
admin.deleteTable(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasFoundUserPermission(UserPermission userPermission,
|
||||||
|
List<UserPermission> perms) {
|
||||||
|
return perms.contains(userPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGrantRevokeAtQualifierLevel() throws Exception {
|
||||||
|
final byte[] tableName = Bytes.toBytes("testGrantRevokeAtQualifierLevel");
|
||||||
|
final byte[] family1 = Bytes.toBytes("f1");
|
||||||
|
final byte[] family2 = Bytes.toBytes("f2");
|
||||||
|
final byte[] qualifier = Bytes.toBytes("q");
|
||||||
|
|
||||||
|
// create table
|
||||||
|
HBaseAdmin admin = TEST_UTIL.getHBaseAdmin();
|
||||||
|
|
||||||
|
if (admin.tableExists(tableName)) {
|
||||||
|
admin.disableTable(tableName);
|
||||||
|
admin.deleteTable(tableName);
|
||||||
|
}
|
||||||
|
HTableDescriptor htd = new HTableDescriptor(tableName);
|
||||||
|
htd.addFamily(new HColumnDescriptor(family1));
|
||||||
|
htd.addFamily(new HColumnDescriptor(family2));
|
||||||
|
htd.setOwnerString(USER_OWNER.getShortName());
|
||||||
|
admin.createTable(htd);
|
||||||
|
|
||||||
|
// create temp users
|
||||||
|
User user = User.createUserForTesting(TEST_UTIL.getConfiguration(),
|
||||||
|
"user", new String[0]);
|
||||||
|
|
||||||
|
HTable acl = new HTable(conf, AccessControlLists.ACL_TABLE_NAME);
|
||||||
|
AccessControllerProtocol protocol =
|
||||||
|
acl.coprocessorProxy(AccessControllerProtocol.class, tableName);
|
||||||
|
|
||||||
|
PrivilegedExceptionAction getQualifierAction = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Get g = new Get(Bytes.toBytes("random_row"));
|
||||||
|
g.addColumn(family1, qualifier);
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.get(g);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction putQualifierAction = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Put p = new Put(Bytes.toBytes("random_row"));
|
||||||
|
p.add(family1, qualifier, Bytes.toBytes("v1"));
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.put(p);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PrivilegedExceptionAction deleteQualifierAction = new PrivilegedExceptionAction() {
|
||||||
|
public Object run() throws Exception {
|
||||||
|
Delete d = new Delete(Bytes.toBytes("random_row"));
|
||||||
|
d.deleteColumn(family1, qualifier);
|
||||||
|
//d.deleteFamily(family1);
|
||||||
|
HTable t = new HTable(conf, tableName);
|
||||||
|
t.delete(d);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protocol.revoke(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, family1));
|
||||||
|
verifyDenied(user, getQualifierAction);
|
||||||
|
verifyDenied(user, putQualifierAction);
|
||||||
|
verifyDenied(user, deleteQualifierAction);
|
||||||
|
|
||||||
|
protocol.grant(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, family1, qualifier,
|
||||||
|
Permission.Action.READ));
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
verifyAllowed(user, getQualifierAction);
|
||||||
|
verifyDenied(user, putQualifierAction);
|
||||||
|
verifyDenied(user, deleteQualifierAction);
|
||||||
|
|
||||||
|
// only grant write permission
|
||||||
|
// TODO: comment this portion after HBASE-3583
|
||||||
|
protocol.grant(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, family1, qualifier,
|
||||||
|
Permission.Action.WRITE));
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
verifyDenied(user, getQualifierAction);
|
||||||
|
verifyAllowed(user, putQualifierAction);
|
||||||
|
verifyAllowed(user, deleteQualifierAction);
|
||||||
|
|
||||||
|
// grant both read and write permission.
|
||||||
|
protocol.grant(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, family1, qualifier,
|
||||||
|
Permission.Action.READ, Permission.Action.WRITE));
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
verifyAllowed(user, getQualifierAction);
|
||||||
|
verifyAllowed(user, putQualifierAction);
|
||||||
|
verifyAllowed(user, deleteQualifierAction);
|
||||||
|
|
||||||
|
// revoke family level permission won't impact column level.
|
||||||
|
protocol.revoke(Bytes.toBytes(user.getShortName()),
|
||||||
|
new TablePermission(tableName, family1, qualifier));
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
verifyDenied(user, getQualifierAction);
|
||||||
|
verifyDenied(user, putQualifierAction);
|
||||||
|
verifyDenied(user, deleteQualifierAction);
|
||||||
|
|
||||||
|
// delete table
|
||||||
|
admin.disableTable(tableName);
|
||||||
|
admin.deleteTable(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPermissionList() throws Exception {
|
||||||
|
final byte[] tableName = Bytes.toBytes("testPermissionList");
|
||||||
|
final byte[] family1 = Bytes.toBytes("f1");
|
||||||
|
final byte[] family2 = Bytes.toBytes("f2");
|
||||||
|
final byte[] qualifier = Bytes.toBytes("q");
|
||||||
|
final byte[] user = Bytes.toBytes("user");
|
||||||
|
|
||||||
|
// create table
|
||||||
|
HBaseAdmin admin = TEST_UTIL.getHBaseAdmin();
|
||||||
|
if (admin.tableExists(tableName)) {
|
||||||
|
admin.disableTable(tableName);
|
||||||
|
admin.deleteTable(tableName);
|
||||||
|
}
|
||||||
|
HTableDescriptor htd = new HTableDescriptor(tableName);
|
||||||
|
htd.addFamily(new HColumnDescriptor(family1));
|
||||||
|
htd.addFamily(new HColumnDescriptor(family2));
|
||||||
|
htd.setOwnerString(USER_OWNER.getShortName());
|
||||||
|
admin.createTable(htd);
|
||||||
|
|
||||||
|
HTable acl = new HTable(conf, AccessControlLists.ACL_TABLE_NAME);
|
||||||
|
AccessControllerProtocol protocol =
|
||||||
|
acl.coprocessorProxy(AccessControllerProtocol.class, tableName);
|
||||||
|
|
||||||
|
List<UserPermission> perms = protocol.getUserPermissions(tableName);
|
||||||
|
|
||||||
|
UserPermission up = new UserPermission(user,
|
||||||
|
tableName, family1, qualifier, Permission.Action.READ);
|
||||||
|
assertFalse("User should not be granted permission: " + up.toString(),
|
||||||
|
hasFoundUserPermission(up, perms));
|
||||||
|
|
||||||
|
// grant read permission
|
||||||
|
UserPermission upToSet = new UserPermission(user,
|
||||||
|
tableName, family1, qualifier, Permission.Action.READ);
|
||||||
|
protocol.grant(user, upToSet);
|
||||||
|
perms = protocol.getUserPermissions(tableName);
|
||||||
|
|
||||||
|
UserPermission upToVerify = new UserPermission(user,
|
||||||
|
tableName, family1, qualifier, Permission.Action.READ);
|
||||||
|
assertTrue("User should be granted permission: " + upToVerify.toString(),
|
||||||
|
hasFoundUserPermission(upToVerify, perms));
|
||||||
|
|
||||||
|
upToVerify = new UserPermission(user, tableName, family1, qualifier,
|
||||||
|
Permission.Action.WRITE);
|
||||||
|
assertFalse("User should not be granted permission: " + upToVerify.toString(),
|
||||||
|
hasFoundUserPermission(upToVerify, perms));
|
||||||
|
|
||||||
|
// grant read+write
|
||||||
|
upToSet = new UserPermission(user, tableName, family1, qualifier,
|
||||||
|
Permission.Action.WRITE, Permission.Action.READ);
|
||||||
|
protocol.grant(user, upToSet);
|
||||||
|
perms = protocol.getUserPermissions(tableName);
|
||||||
|
|
||||||
|
upToVerify = new UserPermission(user, tableName, family1, qualifier,
|
||||||
|
Permission.Action.WRITE, Permission.Action.READ);
|
||||||
|
assertTrue("User should be granted permission: " + upToVerify.toString(),
|
||||||
|
hasFoundUserPermission(upToVerify, perms));
|
||||||
|
|
||||||
|
protocol.revoke(user, upToSet);
|
||||||
|
perms = protocol.getUserPermissions(tableName);
|
||||||
|
assertFalse("User should not be granted permission: " + upToVerify.toString(),
|
||||||
|
hasFoundUserPermission(upToVerify, perms));
|
||||||
|
|
||||||
|
// delete table
|
||||||
|
admin.disableTable(tableName);
|
||||||
|
admin.deleteTable(tableName);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,305 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.common.collect.ArrayListMultimap;
|
||||||
|
import com.google.common.collect.ListMultimap;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.hbase.Abortable;
|
||||||
|
import org.apache.hadoop.hbase.HBaseTestingUtility;
|
||||||
|
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.util.Bytes;
|
||||||
|
import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the reading and writing of access permissions on {@code _acl_} table.
|
||||||
|
*/
|
||||||
|
public class TestTablePermissions {
|
||||||
|
private static final Log LOG = LogFactory.getLog(TestTablePermissions.class);
|
||||||
|
private static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
|
||||||
|
private static ZooKeeperWatcher ZKW;
|
||||||
|
private final static Abortable ABORTABLE = new Abortable() {
|
||||||
|
private final AtomicBoolean abort = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abort(String why, Throwable e) {
|
||||||
|
LOG.info(why, e);
|
||||||
|
abort.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAborted() {
|
||||||
|
return abort.get();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static byte[] TEST_TABLE = Bytes.toBytes("perms_test");
|
||||||
|
private static byte[] TEST_TABLE2 = Bytes.toBytes("perms_test2");
|
||||||
|
private static byte[] TEST_FAMILY = Bytes.toBytes("f1");
|
||||||
|
private static byte[] TEST_QUALIFIER = Bytes.toBytes("col1");
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void beforeClass() throws Exception {
|
||||||
|
// setup configuration
|
||||||
|
Configuration conf = UTIL.getConfiguration();
|
||||||
|
SecureTestUtil.enableSecurity(conf);
|
||||||
|
|
||||||
|
UTIL.startMiniCluster();
|
||||||
|
ZKW = new ZooKeeperWatcher(UTIL.getConfiguration(),
|
||||||
|
"TestTablePermissions", ABORTABLE);
|
||||||
|
|
||||||
|
UTIL.createTable(TEST_TABLE, TEST_FAMILY);
|
||||||
|
UTIL.createTable(TEST_TABLE2, TEST_FAMILY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void afterClass() throws Exception {
|
||||||
|
UTIL.shutdownMiniCluster();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasicWrite() throws Exception {
|
||||||
|
Configuration conf = UTIL.getConfiguration();
|
||||||
|
// add some permissions
|
||||||
|
AccessControlLists.addTablePermission(conf, TEST_TABLE,
|
||||||
|
"george", new TablePermission(TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ, TablePermission.Action.WRITE));
|
||||||
|
AccessControlLists.addTablePermission(conf, TEST_TABLE,
|
||||||
|
"hubert", new TablePermission(TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
AccessControlLists.addTablePermission(conf, TEST_TABLE,
|
||||||
|
"humphrey", new TablePermission(TEST_TABLE, TEST_FAMILY, TEST_QUALIFIER,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
|
||||||
|
// retrieve the same
|
||||||
|
ListMultimap<String,TablePermission> perms =
|
||||||
|
AccessControlLists.getTablePermissions(conf, TEST_TABLE);
|
||||||
|
List<TablePermission> userPerms = perms.get("george");
|
||||||
|
assertNotNull("Should have permissions for george", userPerms);
|
||||||
|
assertEquals("Should have 1 permission for george", 1, userPerms.size());
|
||||||
|
TablePermission permission = userPerms.get(0);
|
||||||
|
assertTrue("Permission should be for " + TEST_TABLE,
|
||||||
|
Bytes.equals(TEST_TABLE, permission.getTable()));
|
||||||
|
assertNull("Column family should be empty", permission.getFamily());
|
||||||
|
|
||||||
|
// check actions
|
||||||
|
assertNotNull(permission.getActions());
|
||||||
|
assertEquals(2, permission.getActions().length);
|
||||||
|
List<TablePermission.Action> actions = Arrays.asList(permission.getActions());
|
||||||
|
assertTrue(actions.contains(TablePermission.Action.READ));
|
||||||
|
assertTrue(actions.contains(TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
userPerms = perms.get("hubert");
|
||||||
|
assertNotNull("Should have permissions for hubert", userPerms);
|
||||||
|
assertEquals("Should have 1 permission for hubert", 1, userPerms.size());
|
||||||
|
permission = userPerms.get(0);
|
||||||
|
assertTrue("Permission should be for " + TEST_TABLE,
|
||||||
|
Bytes.equals(TEST_TABLE, permission.getTable()));
|
||||||
|
assertNull("Column family should be empty", permission.getFamily());
|
||||||
|
|
||||||
|
// check actions
|
||||||
|
assertNotNull(permission.getActions());
|
||||||
|
assertEquals(1, permission.getActions().length);
|
||||||
|
actions = Arrays.asList(permission.getActions());
|
||||||
|
assertTrue(actions.contains(TablePermission.Action.READ));
|
||||||
|
assertFalse(actions.contains(TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
userPerms = perms.get("humphrey");
|
||||||
|
assertNotNull("Should have permissions for humphrey", userPerms);
|
||||||
|
assertEquals("Should have 1 permission for humphrey", 1, userPerms.size());
|
||||||
|
permission = userPerms.get(0);
|
||||||
|
assertTrue("Permission should be for " + TEST_TABLE,
|
||||||
|
Bytes.equals(TEST_TABLE, permission.getTable()));
|
||||||
|
assertTrue("Permission should be for family " + TEST_FAMILY,
|
||||||
|
Bytes.equals(TEST_FAMILY, permission.getFamily()));
|
||||||
|
assertTrue("Permission should be for qualifier " + TEST_QUALIFIER,
|
||||||
|
Bytes.equals(TEST_QUALIFIER, permission.getQualifier()));
|
||||||
|
|
||||||
|
// check actions
|
||||||
|
assertNotNull(permission.getActions());
|
||||||
|
assertEquals(1, permission.getActions().length);
|
||||||
|
actions = Arrays.asList(permission.getActions());
|
||||||
|
assertTrue(actions.contains(TablePermission.Action.READ));
|
||||||
|
assertFalse(actions.contains(TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
// table 2 permissions
|
||||||
|
AccessControlLists.addTablePermission(conf, TEST_TABLE2, "hubert",
|
||||||
|
new TablePermission(TEST_TABLE2, null,
|
||||||
|
TablePermission.Action.READ, TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
// check full load
|
||||||
|
Map<byte[],ListMultimap<String,TablePermission>> allPerms =
|
||||||
|
AccessControlLists.loadAll(conf);
|
||||||
|
assertEquals("Full permission map should have entries for both test tables",
|
||||||
|
2, allPerms.size());
|
||||||
|
|
||||||
|
userPerms = allPerms.get(TEST_TABLE).get("hubert");
|
||||||
|
assertNotNull(userPerms);
|
||||||
|
assertEquals(1, userPerms.size());
|
||||||
|
permission = userPerms.get(0);
|
||||||
|
assertTrue(Bytes.equals(TEST_TABLE, permission.getTable()));
|
||||||
|
assertEquals(1, permission.getActions().length);
|
||||||
|
assertEquals(TablePermission.Action.READ, permission.getActions()[0]);
|
||||||
|
|
||||||
|
userPerms = allPerms.get(TEST_TABLE2).get("hubert");
|
||||||
|
assertNotNull(userPerms);
|
||||||
|
assertEquals(1, userPerms.size());
|
||||||
|
permission = userPerms.get(0);
|
||||||
|
assertTrue(Bytes.equals(TEST_TABLE2, permission.getTable()));
|
||||||
|
assertEquals(2, permission.getActions().length);
|
||||||
|
actions = Arrays.asList(permission.getActions());
|
||||||
|
assertTrue(actions.contains(TablePermission.Action.READ));
|
||||||
|
assertTrue(actions.contains(TablePermission.Action.WRITE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPersistence() throws Exception {
|
||||||
|
Configuration conf = UTIL.getConfiguration();
|
||||||
|
AccessControlLists.addTablePermission(conf, TEST_TABLE, "albert",
|
||||||
|
new TablePermission(TEST_TABLE, null, TablePermission.Action.READ));
|
||||||
|
AccessControlLists.addTablePermission(conf, TEST_TABLE, "betty",
|
||||||
|
new TablePermission(TEST_TABLE, null, TablePermission.Action.READ,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
AccessControlLists.addTablePermission(conf, TEST_TABLE, "clark",
|
||||||
|
new TablePermission(TEST_TABLE, TEST_FAMILY, TablePermission.Action.READ));
|
||||||
|
AccessControlLists.addTablePermission(conf, TEST_TABLE, "dwight",
|
||||||
|
new TablePermission(TEST_TABLE, TEST_FAMILY, TEST_QUALIFIER,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
// verify permissions survive changes in table metadata
|
||||||
|
ListMultimap<String,TablePermission> preperms =
|
||||||
|
AccessControlLists.getTablePermissions(conf, TEST_TABLE);
|
||||||
|
|
||||||
|
HTable table = new HTable(conf, TEST_TABLE);
|
||||||
|
table.put(new Put(Bytes.toBytes("row1"))
|
||||||
|
.add(TEST_FAMILY, TEST_QUALIFIER, Bytes.toBytes("v1")));
|
||||||
|
table.put(new Put(Bytes.toBytes("row2"))
|
||||||
|
.add(TEST_FAMILY, TEST_QUALIFIER, Bytes.toBytes("v2")));
|
||||||
|
HBaseAdmin admin = UTIL.getHBaseAdmin();
|
||||||
|
admin.split(TEST_TABLE);
|
||||||
|
|
||||||
|
// wait for split
|
||||||
|
Thread.sleep(10000);
|
||||||
|
|
||||||
|
ListMultimap<String,TablePermission> postperms =
|
||||||
|
AccessControlLists.getTablePermissions(conf, TEST_TABLE);
|
||||||
|
|
||||||
|
checkMultimapEqual(preperms, postperms);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerialization() throws Exception {
|
||||||
|
Configuration conf = UTIL.getConfiguration();
|
||||||
|
ListMultimap<String,TablePermission> permissions = ArrayListMultimap.create();
|
||||||
|
permissions.put("george", new TablePermission(TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
permissions.put("george", new TablePermission(TEST_TABLE, TEST_FAMILY,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
permissions.put("george", new TablePermission(TEST_TABLE2, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
permissions.put("hubert", new TablePermission(TEST_TABLE2, null,
|
||||||
|
TablePermission.Action.READ, TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
AccessControlLists.writePermissions(new DataOutputStream(bos),
|
||||||
|
permissions, conf);
|
||||||
|
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
|
||||||
|
ListMultimap<String,TablePermission> copy =
|
||||||
|
AccessControlLists.readPermissions(new DataInputStream(bis), conf);
|
||||||
|
|
||||||
|
checkMultimapEqual(permissions, copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkMultimapEqual(ListMultimap<String,TablePermission> first,
|
||||||
|
ListMultimap<String,TablePermission> second) {
|
||||||
|
assertEquals(first.size(), second.size());
|
||||||
|
for (String key : first.keySet()) {
|
||||||
|
List<TablePermission> firstPerms = first.get(key);
|
||||||
|
List<TablePermission> secondPerms = second.get(key);
|
||||||
|
assertNotNull(secondPerms);
|
||||||
|
assertEquals(firstPerms.size(), secondPerms.size());
|
||||||
|
LOG.info("First permissions: "+firstPerms.toString());
|
||||||
|
LOG.info("Second permissions: "+secondPerms.toString());
|
||||||
|
for (TablePermission p : firstPerms) {
|
||||||
|
assertTrue("Permission "+p.toString()+" not found", secondPerms.contains(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEquals() throws Exception {
|
||||||
|
TablePermission p1 = new TablePermission(TEST_TABLE, null, TablePermission.Action.READ);
|
||||||
|
TablePermission p2 = new TablePermission(TEST_TABLE, null, TablePermission.Action.READ);
|
||||||
|
assertTrue(p1.equals(p2));
|
||||||
|
assertTrue(p2.equals(p1));
|
||||||
|
|
||||||
|
p1 = new TablePermission(TEST_TABLE, null, TablePermission.Action.READ, TablePermission.Action.WRITE);
|
||||||
|
p2 = new TablePermission(TEST_TABLE, null, TablePermission.Action.WRITE, TablePermission.Action.READ);
|
||||||
|
assertTrue(p1.equals(p2));
|
||||||
|
assertTrue(p2.equals(p1));
|
||||||
|
|
||||||
|
p1 = new TablePermission(TEST_TABLE, TEST_FAMILY, TablePermission.Action.READ, TablePermission.Action.WRITE);
|
||||||
|
p2 = new TablePermission(TEST_TABLE, TEST_FAMILY, TablePermission.Action.WRITE, TablePermission.Action.READ);
|
||||||
|
assertTrue(p1.equals(p2));
|
||||||
|
assertTrue(p2.equals(p1));
|
||||||
|
|
||||||
|
p1 = new TablePermission(TEST_TABLE, TEST_FAMILY, TEST_QUALIFIER, TablePermission.Action.READ, TablePermission.Action.WRITE);
|
||||||
|
p2 = new TablePermission(TEST_TABLE, TEST_FAMILY, TEST_QUALIFIER, TablePermission.Action.WRITE, TablePermission.Action.READ);
|
||||||
|
assertTrue(p1.equals(p2));
|
||||||
|
assertTrue(p2.equals(p1));
|
||||||
|
|
||||||
|
p1 = new TablePermission(TEST_TABLE, null, TablePermission.Action.READ);
|
||||||
|
p2 = new TablePermission(TEST_TABLE, TEST_FAMILY, TablePermission.Action.READ);
|
||||||
|
assertFalse(p1.equals(p2));
|
||||||
|
assertFalse(p2.equals(p1));
|
||||||
|
|
||||||
|
p1 = new TablePermission(TEST_TABLE, null, TablePermission.Action.READ);
|
||||||
|
p2 = new TablePermission(TEST_TABLE, null, TablePermission.Action.WRITE);
|
||||||
|
assertFalse(p1.equals(p2));
|
||||||
|
assertFalse(p2.equals(p1));
|
||||||
|
p2 = new TablePermission(TEST_TABLE, null, TablePermission.Action.READ, TablePermission.Action.WRITE);
|
||||||
|
assertFalse(p1.equals(p2));
|
||||||
|
assertFalse(p2.equals(p1));
|
||||||
|
|
||||||
|
p1 = new TablePermission(TEST_TABLE, null, TablePermission.Action.READ);
|
||||||
|
p2 = new TablePermission(TEST_TABLE2, null, TablePermission.Action.READ);
|
||||||
|
assertFalse(p1.equals(p2));
|
||||||
|
assertFalse(p2.equals(p1));
|
||||||
|
|
||||||
|
p2 = new TablePermission(TEST_TABLE, null);
|
||||||
|
assertFalse(p1.equals(p2));
|
||||||
|
assertFalse(p2.equals(p1));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* 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 org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.hbase.Abortable;
|
||||||
|
import org.apache.hadoop.hbase.HBaseTestingUtility;
|
||||||
|
import org.apache.hadoop.hbase.util.Bytes;
|
||||||
|
import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the reading and writing of access permissions to and from zookeeper.
|
||||||
|
*/
|
||||||
|
public class TestZKPermissionsWatcher {
|
||||||
|
private static final Log LOG = LogFactory.getLog(TestZKPermissionsWatcher.class);
|
||||||
|
private static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
|
||||||
|
private static TableAuthManager AUTH_A;
|
||||||
|
private static TableAuthManager AUTH_B;
|
||||||
|
private final static Abortable ABORTABLE = new Abortable() {
|
||||||
|
private final AtomicBoolean abort = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abort(String why, Throwable e) {
|
||||||
|
LOG.info(why, e);
|
||||||
|
abort.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAborted() {
|
||||||
|
return abort.get();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static byte[] TEST_TABLE = Bytes.toBytes("perms_test");
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void beforeClass() throws Exception {
|
||||||
|
// setup configuration
|
||||||
|
Configuration conf = UTIL.getConfiguration();
|
||||||
|
SecureTestUtil.enableSecurity(conf);
|
||||||
|
|
||||||
|
// start minicluster
|
||||||
|
UTIL.startMiniCluster();
|
||||||
|
AUTH_A = TableAuthManager.get(new ZooKeeperWatcher(conf,
|
||||||
|
"TestZKPermissionsWatcher_1", ABORTABLE), conf);
|
||||||
|
AUTH_B = TableAuthManager.get(new ZooKeeperWatcher(conf,
|
||||||
|
"TestZKPermissionsWatcher_2", ABORTABLE), conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void afterClass() throws Exception {
|
||||||
|
UTIL.shutdownMiniCluster();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPermissionsWatcher() throws Exception {
|
||||||
|
assertFalse(AUTH_A.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertFalse(AUTH_A.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
assertFalse(AUTH_A.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertFalse(AUTH_A.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
assertFalse(AUTH_B.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertFalse(AUTH_B.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
assertFalse(AUTH_B.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertFalse(AUTH_B.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
// update ACL: george RW
|
||||||
|
List<TablePermission> acl = new ArrayList<TablePermission>();
|
||||||
|
acl.add(new TablePermission(TEST_TABLE, null, TablePermission.Action.READ,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
AUTH_A.setUserPermissions("george", TEST_TABLE, acl);
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
// check it
|
||||||
|
assertTrue(AUTH_A.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertTrue(AUTH_A.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
assertTrue(AUTH_B.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertTrue(AUTH_B.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
assertFalse(AUTH_A.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertFalse(AUTH_A.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
assertFalse(AUTH_B.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertFalse(AUTH_B.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
|
||||||
|
// update ACL: hubert R
|
||||||
|
acl = new ArrayList<TablePermission>();
|
||||||
|
acl.add(new TablePermission(TEST_TABLE, null, TablePermission.Action.READ));
|
||||||
|
AUTH_B.setUserPermissions("hubert", TEST_TABLE, acl);
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
// check it
|
||||||
|
assertTrue(AUTH_A.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertTrue(AUTH_A.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
assertTrue(AUTH_B.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertTrue(AUTH_B.authorizeUser("george", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
assertTrue(AUTH_A.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertFalse(AUTH_A.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
assertTrue(AUTH_B.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.READ));
|
||||||
|
assertFalse(AUTH_B.authorizeUser("hubert", TEST_TABLE, null,
|
||||||
|
TablePermission.Action.WRITE));
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,7 @@ import org.apache.hadoop.fs.Path;
|
|||||||
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
|
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
|
||||||
import org.apache.hadoop.hbase.io.hfile.Compression;
|
import org.apache.hadoop.hbase.io.hfile.Compression;
|
||||||
import org.apache.hadoop.hbase.regionserver.StoreFile;
|
import org.apache.hadoop.hbase.regionserver.StoreFile;
|
||||||
|
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.io.WritableComparable;
|
import org.apache.hadoop.io.WritableComparable;
|
||||||
|
|
||||||
@ -81,6 +82,10 @@ public class HTableDescriptor implements WritableComparable<HTableDescriptor> {
|
|||||||
private static final ImmutableBytesWritable MAX_FILESIZE_KEY =
|
private static final ImmutableBytesWritable MAX_FILESIZE_KEY =
|
||||||
new ImmutableBytesWritable(Bytes.toBytes(MAX_FILESIZE));
|
new ImmutableBytesWritable(Bytes.toBytes(MAX_FILESIZE));
|
||||||
|
|
||||||
|
public static final String OWNER = "OWNER";
|
||||||
|
public static final ImmutableBytesWritable OWNER_KEY =
|
||||||
|
new ImmutableBytesWritable(Bytes.toBytes(OWNER));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <em>INTERNAL</em> Used by rest interface to access this metadata
|
* <em>INTERNAL</em> Used by rest interface to access this metadata
|
||||||
* attribute which denotes if the table is Read Only
|
* attribute which denotes if the table is Read Only
|
||||||
@ -1064,7 +1069,7 @@ public class HTableDescriptor implements WritableComparable<HTableDescriptor> {
|
|||||||
public static final HTableDescriptor ROOT_TABLEDESC = new HTableDescriptor(
|
public static final HTableDescriptor ROOT_TABLEDESC = new HTableDescriptor(
|
||||||
HConstants.ROOT_TABLE_NAME,
|
HConstants.ROOT_TABLE_NAME,
|
||||||
new HColumnDescriptor[] { new HColumnDescriptor(HConstants.CATALOG_FAMILY,
|
new HColumnDescriptor[] { new HColumnDescriptor(HConstants.CATALOG_FAMILY,
|
||||||
10, // Ten is arbitrary number. Keep versions to help debuggging.
|
10, // Ten is arbitrary number. Keep versions to help debugging.
|
||||||
Compression.Algorithm.NONE.getName(), true, true, 8 * 1024,
|
Compression.Algorithm.NONE.getName(), true, true, 8 * 1024,
|
||||||
HConstants.FOREVER, StoreFile.BloomType.NONE.toString(),
|
HConstants.FOREVER, StoreFile.BloomType.NONE.toString(),
|
||||||
HConstants.REPLICATION_SCOPE_LOCAL) });
|
HConstants.REPLICATION_SCOPE_LOCAL) });
|
||||||
@ -1073,8 +1078,31 @@ public class HTableDescriptor implements WritableComparable<HTableDescriptor> {
|
|||||||
public static final HTableDescriptor META_TABLEDESC = new HTableDescriptor(
|
public static final HTableDescriptor META_TABLEDESC = new HTableDescriptor(
|
||||||
HConstants.META_TABLE_NAME, new HColumnDescriptor[] {
|
HConstants.META_TABLE_NAME, new HColumnDescriptor[] {
|
||||||
new HColumnDescriptor(HConstants.CATALOG_FAMILY,
|
new HColumnDescriptor(HConstants.CATALOG_FAMILY,
|
||||||
10, // Ten is arbitrary number. Keep versions to help debuggging.
|
10, // Ten is arbitrary number. Keep versions to help debugging.
|
||||||
Compression.Algorithm.NONE.getName(), true, true, 8 * 1024,
|
Compression.Algorithm.NONE.getName(), true, true, 8 * 1024,
|
||||||
HConstants.FOREVER, StoreFile.BloomType.NONE.toString(),
|
HConstants.FOREVER, StoreFile.BloomType.NONE.toString(),
|
||||||
HConstants.REPLICATION_SCOPE_LOCAL)});
|
HConstants.REPLICATION_SCOPE_LOCAL)});
|
||||||
|
|
||||||
|
|
||||||
|
public void setOwner(User owner) {
|
||||||
|
setOwnerString(owner != null ? owner.getShortName() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// used by admin.rb:alter(table_name,*args) to update owner.
|
||||||
|
public void setOwnerString(String ownerString) {
|
||||||
|
if (ownerString != null) {
|
||||||
|
setValue(OWNER_KEY, Bytes.toBytes(ownerString));
|
||||||
|
} else {
|
||||||
|
values.remove(OWNER_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOwnerString() {
|
||||||
|
if (getValue(OWNER_KEY) != null) {
|
||||||
|
return Bytes.toString(getValue(OWNER_KEY));
|
||||||
|
}
|
||||||
|
// Note that every table should have an owner (i.e. should have OWNER_KEY set).
|
||||||
|
// .META. and -ROOT- should return system user as owner, not null (see MasterFileSystem.java:bootstrap()).
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,10 +49,10 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
public abstract class BaseRegionObserver implements RegionObserver {
|
public abstract class BaseRegionObserver implements RegionObserver {
|
||||||
@Override
|
@Override
|
||||||
public void start(CoprocessorEnvironment e) { }
|
public void start(CoprocessorEnvironment e) throws IOException { }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop(CoprocessorEnvironment e) { }
|
public void stop(CoprocessorEnvironment e) throws IOException { }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preOpen(ObserverContext<RegionCoprocessorEnvironment> e) { }
|
public void preOpen(ObserverContext<RegionCoprocessorEnvironment> e) { }
|
||||||
|
@ -381,6 +381,9 @@ public class ZKUtil {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return node + " (" + RegionTransitionData.fromBytes(data) + ")";
|
return node + " (" + RegionTransitionData.fromBytes(data) + ")";
|
||||||
}
|
}
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return (data.length == 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -621,6 +621,12 @@
|
|||||||
</description>
|
</description>
|
||||||
</property>
|
</property>
|
||||||
|
|
||||||
|
<property>
|
||||||
|
<name>zookeeper.znode.acl.parent</name>
|
||||||
|
<value>acl</value>
|
||||||
|
<description>Root ZNode for access control lists.</description>
|
||||||
|
</property>
|
||||||
|
|
||||||
<property>
|
<property>
|
||||||
<name>hbase.coprocessor.region.classes</name>
|
<name>hbase.coprocessor.region.classes</name>
|
||||||
<value></value>
|
<value></value>
|
||||||
|
@ -75,3 +75,4 @@ require 'hbase/hbase'
|
|||||||
require 'hbase/admin'
|
require 'hbase/admin'
|
||||||
require 'hbase/table'
|
require 'hbase/table'
|
||||||
require 'hbase/replication_admin'
|
require 'hbase/replication_admin'
|
||||||
|
require 'hbase/security'
|
||||||
|
@ -289,7 +289,6 @@ module Hbase
|
|||||||
@admin.createTable(table_description)
|
@admin.createTable(table_description)
|
||||||
end
|
end
|
||||||
|
|
||||||
#----------------------------------------------------------------------------------------------
|
|
||||||
# Check the status of alter command (number of regions reopened)
|
# Check the status of alter command (number of regions reopened)
|
||||||
def alter_status(table_name)
|
def alter_status(table_name)
|
||||||
# Table name should be a string
|
# Table name should be a string
|
||||||
@ -336,7 +335,17 @@ module Hbase
|
|||||||
|
|
||||||
# No method parameter, try to use the args as a column definition
|
# No method parameter, try to use the args as a column definition
|
||||||
unless method = arg.delete(METHOD)
|
unless method = arg.delete(METHOD)
|
||||||
|
# Note that we handle owner here, and also below (see (2)) as part of the "METHOD => 'table_att'" table attributes.
|
||||||
|
# In other words, if OWNER is specified, then METHOD is set to table_att.
|
||||||
|
# alter 'tablename', {OWNER => 'username'} (that is, METHOD => 'table_att' is not specified).
|
||||||
|
if arg[OWNER]
|
||||||
|
htd.setOwnerString(arg[OWNER])
|
||||||
|
@admin.modifyTable(table_name.to_java_bytes, htd)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
descriptor = hcd(arg, htd)
|
descriptor = hcd(arg, htd)
|
||||||
|
|
||||||
if arg[COMPRESSION_COMPACT]
|
if arg[COMPRESSION_COMPACT]
|
||||||
descriptor.setValue(COMPRESSION_COMPACT, arg[COMPRESSION_COMPACT])
|
descriptor.setValue(COMPRESSION_COMPACT, arg[COMPRESSION_COMPACT])
|
||||||
end
|
end
|
||||||
@ -376,6 +385,8 @@ module Hbase
|
|||||||
htd.setReadOnly(JBoolean.valueOf(arg[READONLY])) if arg[READONLY]
|
htd.setReadOnly(JBoolean.valueOf(arg[READONLY])) if arg[READONLY]
|
||||||
htd.setMemStoreFlushSize(JLong.valueOf(arg[MEMSTORE_FLUSHSIZE])) if arg[MEMSTORE_FLUSHSIZE]
|
htd.setMemStoreFlushSize(JLong.valueOf(arg[MEMSTORE_FLUSHSIZE])) if arg[MEMSTORE_FLUSHSIZE]
|
||||||
htd.setDeferredLogFlush(JBoolean.valueOf(arg[DEFERRED_LOG_FLUSH])) if arg[DEFERRED_LOG_FLUSH]
|
htd.setDeferredLogFlush(JBoolean.valueOf(arg[DEFERRED_LOG_FLUSH])) if arg[DEFERRED_LOG_FLUSH]
|
||||||
|
# (2) Here, we handle the alternate syntax of ownership setting, where method => 'table_att' is specified.
|
||||||
|
htd.setOwnerString(arg[OWNER]) if arg[OWNER]
|
||||||
|
|
||||||
# set a coprocessor attribute
|
# set a coprocessor attribute
|
||||||
if arg.kind_of?(Hash)
|
if arg.kind_of?(Hash)
|
||||||
|
@ -22,6 +22,7 @@ include Java
|
|||||||
|
|
||||||
require 'hbase/admin'
|
require 'hbase/admin'
|
||||||
require 'hbase/table'
|
require 'hbase/table'
|
||||||
|
require 'hbase/security'
|
||||||
|
|
||||||
module Hbase
|
module Hbase
|
||||||
class Hbase
|
class Hbase
|
||||||
@ -52,5 +53,8 @@ module Hbase
|
|||||||
::Hbase::RepAdmin.new(configuration, formatter)
|
::Hbase::RepAdmin.new(configuration, formatter)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def security_admin(formatter)
|
||||||
|
::Hbase::SecurityAdmin.new(configuration, formatter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
128
src/main/ruby/hbase/security.rb
Normal file
128
src/main/ruby/hbase/security.rb
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
include Java
|
||||||
|
|
||||||
|
# Wrapper for org.apache.hadoop.hbase.client.HBaseAdmin
|
||||||
|
|
||||||
|
module Hbase
|
||||||
|
class SecurityAdmin
|
||||||
|
include HBaseConstants
|
||||||
|
|
||||||
|
def initialize(configuration, formatter)
|
||||||
|
@config = configuration
|
||||||
|
@admin = org.apache.hadoop.hbase.client.HBaseAdmin.new(configuration)
|
||||||
|
@formatter = formatter
|
||||||
|
end
|
||||||
|
|
||||||
|
#----------------------------------------------------------------------------------------------
|
||||||
|
def grant(user, permissions, table_name, family=nil, qualifier=nil)
|
||||||
|
security_available?
|
||||||
|
|
||||||
|
# Table should exist
|
||||||
|
raise(ArgumentError, "Can't find a table: #{table_name}") unless exists?(table_name)
|
||||||
|
|
||||||
|
htd = @admin.getTableDescriptor(table_name.to_java_bytes)
|
||||||
|
|
||||||
|
if (family != nil)
|
||||||
|
raise(ArgumentError, "Can't find a family: #{family}") unless htd.hasFamily(family.to_java_bytes)
|
||||||
|
end
|
||||||
|
|
||||||
|
#TODO: need to validate user name
|
||||||
|
|
||||||
|
# invoke cp endpoint to perform access control
|
||||||
|
fambytes = family.to_java_bytes if (family != nil)
|
||||||
|
qualbytes = qualifier.to_java_bytes if (qualifier != nil)
|
||||||
|
tp = org.apache.hadoop.hbase.security.access.TablePermission.new(table_name.to_java_bytes, fambytes, qualbytes, permissions.to_java_bytes)
|
||||||
|
meta_table = org.apache.hadoop.hbase.client.HTable.new(@config, org.apache.hadoop.hbase.security.access.AccessControlLists::ACL_TABLE_NAME)
|
||||||
|
protocol = meta_table.coprocessorProxy(org.apache.hadoop.hbase.security.access.AccessControllerProtocol.java_class,
|
||||||
|
org.apache.hadoop.hbase.HConstants::EMPTY_START_ROW)
|
||||||
|
protocol.grant(user.to_java_bytes, tp)
|
||||||
|
end
|
||||||
|
|
||||||
|
#----------------------------------------------------------------------------------------------
|
||||||
|
def revoke(user, table_name, family=nil, qualifier=nil)
|
||||||
|
security_available?
|
||||||
|
|
||||||
|
# Table should exist
|
||||||
|
raise(ArgumentError, "Can't find table: #{table_name}") unless exists?(table_name)
|
||||||
|
|
||||||
|
htd = @admin.getTableDescriptor(table_name.to_java_bytes)
|
||||||
|
|
||||||
|
if (family != nil)
|
||||||
|
raise(ArgumentError, "Can't find a family: #{family}") unless htd.hasFamily(family.to_java_bytes)
|
||||||
|
end
|
||||||
|
|
||||||
|
fambytes = family.to_java_bytes if (family != nil)
|
||||||
|
qualbytes = qualifier.to_java_bytes if (qualifier != nil)
|
||||||
|
tp = org.apache.hadoop.hbase.security.access.TablePermission.new(table_name.to_java_bytes, fambytes, qualbytes, "".to_java_bytes)
|
||||||
|
meta_table = org.apache.hadoop.hbase.client.HTable.new(@config, org.apache.hadoop.hbase.security.access.AccessControlLists::ACL_TABLE_NAME)
|
||||||
|
protocol = meta_table.coprocessorProxy(org.apache.hadoop.hbase.security.access.AccessControllerProtocol.java_class,
|
||||||
|
org.apache.hadoop.hbase.HConstants::EMPTY_START_ROW)
|
||||||
|
protocol.revoke(user.to_java_bytes, tp)
|
||||||
|
end
|
||||||
|
|
||||||
|
#----------------------------------------------------------------------------------------------
|
||||||
|
def user_permission(table_name)
|
||||||
|
security_available?
|
||||||
|
|
||||||
|
raise(ArgumentError, "Can't find table: #{table_name}") unless exists?(table_name)
|
||||||
|
|
||||||
|
meta_table = org.apache.hadoop.hbase.client.HTable.new(@config, org.apache.hadoop.hbase.security.access.AccessControlLists::ACL_TABLE_NAME)
|
||||||
|
protocol = meta_table.coprocessorProxy(org.apache.hadoop.hbase.security.access.AccessControllerProtocol.java_class,
|
||||||
|
org.apache.hadoop.hbase.HConstants::EMPTY_START_ROW)
|
||||||
|
perms = protocol.getUserPermissions(table_name.to_java_bytes)
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
count = 0
|
||||||
|
perms.each do |value|
|
||||||
|
user_name = String.from_java_bytes(value.getUser)
|
||||||
|
table = (value.getTable != nil) ? org.apache.hadoop.hbase.util.Bytes::toStringBinary(value.getTable) : ''
|
||||||
|
family = (value.getFamily != nil) ? org.apache.hadoop.hbase.util.Bytes::toStringBinary(value.getFamily) : ''
|
||||||
|
qualifier = (value.getQualifier != nil) ? org.apache.hadoop.hbase.util.Bytes::toStringBinary(value.getQualifier) : ''
|
||||||
|
|
||||||
|
action = org.apache.hadoop.hbase.security.access.Permission.new value.getActions
|
||||||
|
|
||||||
|
if block_given?
|
||||||
|
yield(user_name, "#{table},#{family},#{qualifier}: #{action.to_s}")
|
||||||
|
else
|
||||||
|
res[user_name] ||= {}
|
||||||
|
res[user_name][family + ":" +qualifier] = action
|
||||||
|
end
|
||||||
|
count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return ((block_given?) ? count : res)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Does table exist?
|
||||||
|
def exists?(table_name)
|
||||||
|
@admin.tableExists(table_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Make sure that security classes are available
|
||||||
|
def security_available?()
|
||||||
|
begin
|
||||||
|
org.apache.hadoop.hbase.security.access.AccessControllerProtocol
|
||||||
|
rescue NameError
|
||||||
|
raise(ArgumentError, "DISABLED: Security features are not available in this build of HBase")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
@ -87,6 +87,10 @@ module Shell
|
|||||||
@hbase_replication_admin ||= hbase.replication_admin(formatter)
|
@hbase_replication_admin ||= hbase.replication_admin(formatter)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hbase_security_admin
|
||||||
|
@hbase_security_admin ||= hbase.security_admin(formatter)
|
||||||
|
end
|
||||||
|
|
||||||
def export_commands(where)
|
def export_commands(where)
|
||||||
::Shell.commands.keys.each do |cmd|
|
::Shell.commands.keys.each do |cmd|
|
||||||
where.send :instance_eval, <<-EOF
|
where.send :instance_eval, <<-EOF
|
||||||
@ -284,3 +288,14 @@ Shell.load_command_group(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Shell.load_command_group(
|
||||||
|
'security',
|
||||||
|
:full_name => 'SECURITY TOOLS',
|
||||||
|
:comment => "NOTE: Above commands are only applicable if running with the AccessController coprocessor",
|
||||||
|
:commands => %w[
|
||||||
|
grant
|
||||||
|
revoke
|
||||||
|
user_permission
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -53,6 +53,10 @@ module Shell
|
|||||||
shell.hbase_replication_admin
|
shell.hbase_replication_admin
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def security_admin
|
||||||
|
shell.hbase_security_admin
|
||||||
|
end
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
|
|
||||||
def formatter
|
def formatter
|
||||||
|
43
src/main/ruby/shell/commands/grant.rb
Normal file
43
src/main/ruby/shell/commands/grant.rb
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
module Shell
|
||||||
|
module Commands
|
||||||
|
class Grant < Command
|
||||||
|
def help
|
||||||
|
return <<-EOF
|
||||||
|
Grant users specific rights to tables.
|
||||||
|
Syntax : grant <user> <permissions> <table> <column family> <column qualifier>
|
||||||
|
|
||||||
|
permissions is either zero or more letters from the set "RWXCA".
|
||||||
|
READ('R'), WRITE('W'), EXEC('X'), CREATE('C'), ADMIN('A')
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
hbase> grant 'bobsmith', 'RW', 't1', 'f1', 'col1'
|
||||||
|
EOF
|
||||||
|
end
|
||||||
|
|
||||||
|
def command(user, rights, table_name, family=nil, qualifier=nil)
|
||||||
|
format_simple_command do
|
||||||
|
security_admin.grant(user, rights, table_name, family, qualifier)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
39
src/main/ruby/shell/commands/revoke.rb
Normal file
39
src/main/ruby/shell/commands/revoke.rb
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
module Shell
|
||||||
|
module Commands
|
||||||
|
class Revoke < Command
|
||||||
|
def help
|
||||||
|
return <<-EOF
|
||||||
|
Revoke a user's access rights to tables.
|
||||||
|
Syntax : revoke <user> <table> <column family>
|
||||||
|
For example:
|
||||||
|
|
||||||
|
hbase> revoke 'bobsmith', 't1', 'f1'
|
||||||
|
EOF
|
||||||
|
end
|
||||||
|
|
||||||
|
def command(user, table_name, family=nil, qualifier=nil)
|
||||||
|
format_simple_command do
|
||||||
|
security_admin.revoke(user, table_name, family, qualifier)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
46
src/main/ruby/shell/commands/user_permission.rb
Normal file
46
src/main/ruby/shell/commands/user_permission.rb
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
module Shell
|
||||||
|
module Commands
|
||||||
|
class UserPermission < Command
|
||||||
|
def help
|
||||||
|
return <<-EOF
|
||||||
|
Show all table access permissions for the particular user.
|
||||||
|
Syntax : user_permission <table>
|
||||||
|
For example:
|
||||||
|
|
||||||
|
hbase> user_permission 'table1'
|
||||||
|
EOF
|
||||||
|
end
|
||||||
|
|
||||||
|
def command(table)
|
||||||
|
#format_simple_command do
|
||||||
|
#admin.user_permission(table)
|
||||||
|
now = Time.now
|
||||||
|
formatter.header(["User", "Table,Family,Qualifier:Permission"])
|
||||||
|
|
||||||
|
count = security_admin.user_permission(table) do |user, permission|
|
||||||
|
formatter.row([ user, permission])
|
||||||
|
end
|
||||||
|
|
||||||
|
formatter.footer(now, count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user