diff --git a/security/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java b/security/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java --- a/security/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java +++ b/security/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java @@ -14,17 +14,22 @@ 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 java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + 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.HRegionInfo; 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; @@ -52,8 +57,11 @@ 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.*; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.MapMaker; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; /** * Provides basic authorization checks for data access and administrative @@ -155,7 +163,7 @@ public class AccessController extends BaseRegionObserver /** * Version number for AccessControllerProtocol */ - private static final long PROTOCOL_VERSION = 1L; + private static final long PROTOCOL_VERSION = 2L; TableAuthManager authManager = null; @@ -959,6 +967,40 @@ public class AccessController extends BaseRegionObserver } @Override + public void checkPermissions(Permission[] permissions) throws IOException { + byte[] tableName = regionEnv.getRegion().getTableDesc().getName(); + for (Permission permission : permissions) { + if (permission instanceof TablePermission) { + TablePermission tperm = (TablePermission) permission; + for (Permission.Action action : permission.getActions()) { + if (!Arrays.equals(tperm.getTable(), tableName)) { + throw new CoprocessorException(AccessController.class, String.format("This method " + + "can only execute at the table specified in TablePermission. " + + "Table of the region:%s , requested table:%s", Bytes.toString(tableName), + Bytes.toString(tperm.getTable()))); + } + + HashMap> familyMap = Maps.newHashMapWithExpectedSize(1); + if (tperm.getFamily() != null) { + if (tperm.getQualifier() != null) { + familyMap.put(tperm.getFamily(), Sets.newHashSet(tperm.getQualifier())); + } else { + familyMap.put(tperm.getFamily(), null); + } + } + + requirePermission(action, regionEnv, familyMap); + } + + } else { + for (Permission.Action action : permission.getActions()) { + requirePermission(action); + } + } + } + } + + @Override public long getProtocolVersion(String protocol, long clientVersion) throws IOException { return PROTOCOL_VERSION; } diff --git a/security/src/main/java/org/apache/hadoop/hbase/security/access/AccessControllerProtocol.java b/security/src/main/java/org/apache/hadoop/hbase/security/access/AccessControllerProtocol.java --- a/security/src/main/java/org/apache/hadoop/hbase/security/access/AccessControllerProtocol.java +++ b/security/src/main/java/org/apache/hadoop/hbase/security/access/AccessControllerProtocol.java @@ -18,15 +18,19 @@ package org.apache.hadoop.hbase.security.access; -import org.apache.hadoop.hbase.ipc.CoprocessorProtocol; - import java.io.IOException; import java.util.List; +import org.apache.hadoop.hbase.ipc.CoprocessorProtocol; + /** * A custom protocol defined for maintaining and querying access control lists. */ public interface AccessControllerProtocol extends CoprocessorProtocol { + + /* V2: Added {@link #checkPermissions(Permission...)}) */ + public static final long VERSION = 2L; + /** * Grants the given user or group the privilege to perform the given actions * over the specified scope contained in {@link TablePermission} @@ -65,4 +69,18 @@ public interface AccessControllerProtocol extends CoprocessorProtocol { */ public List getUserPermissions(byte[] tableName) throws IOException; + + /** + * Checks whether the given Permissions will pass the access checks for the + * current user. Global permissions can be checked from the -acl- table + * or any other table, however TablePermissions can only be checked by + * the table's regions. If access control checks fail this method throws + * AccessDeniedException. + * @param permissions to check for. Permission subclasses can be used + * to do more specific checks at the table/family/column level. + * @throws IOException if there is an error checking the permissions + */ + public void checkPermissions(Permission[] permissions) + throws IOException; + } diff --git a/security/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java b/security/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java --- a/security/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java +++ b/security/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java @@ -18,9 +18,14 @@ package org.apache.hadoop.hbase.security.access; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.io.IOException; import java.security.PrivilegedExceptionAction; -import java.util.*; +import java.util.List; +import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,7 +37,17 @@ 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.client.Delete; +import org.apache.hadoop.hbase.client.Get; +import org.apache.hadoop.hbase.client.HBaseAdmin; +import org.apache.hadoop.hbase.client.HTable; +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.ResultScanner; +import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException; +import org.apache.hadoop.hbase.client.Scan; +import org.apache.hadoop.hbase.coprocessor.CoprocessorException; import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment; import org.apache.hadoop.hbase.coprocessor.ObserverContext; import org.apache.hadoop.hbase.master.MasterCoprocessorHost; @@ -43,8 +58,6 @@ 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. @@ -126,6 +139,13 @@ public class TestAccessController { } } + public void verifyAllowed(PrivilegedExceptionAction action, User... users) + throws Exception { + for (User user : users) { + verifyAllowed(user, action); + } + } + public void verifyDenied(User user, PrivilegedExceptionAction action) throws Exception { try { @@ -151,6 +171,13 @@ public class TestAccessController { } } + public void verifyDenied(PrivilegedExceptionAction action, User... users) + throws Exception { + for (User user : users) { + verifyDenied(user, action); + } + } + @Test public void testTableCreate() throws Exception { PrivilegedExceptionAction createTable = new PrivilegedExceptionAction() { @@ -977,4 +1004,193 @@ public class TestAccessController { admin.disableTable(tableName); admin.deleteTable(tableName); } + + /** global operations*/ + private void verifyGlobal(PrivilegedExceptionAction action) throws Exception { + // should be allowed + verifyAllowed(SUPERUSER, action); + + // should be denied + verifyDenied(USER_OWNER, action); + verifyDenied(USER_RW, action); + verifyDenied(USER_NONE, action); + verifyDenied(USER_RO, action); + } + + public void checkGlobalPerms(Permission.Action... actions) throws IOException { + HTable acl = new HTable(conf, AccessControlLists.ACL_TABLE_NAME); + AccessControllerProtocol protocol = + acl.coprocessorProxy(AccessControllerProtocol.class, new byte[0]); + + Permission[] perms = new Permission[actions.length]; + for (int i=0; i < actions.length; i++) { + perms[i] = new Permission(actions[i]); + } + + protocol.checkPermissions(perms); + } + + public void checkTablePerms(byte[] table, byte[] family, byte[] column, + Permission.Action... actions) throws IOException { + Permission[] perms = new Permission[actions.length]; + for (int i=0; i < actions.length; i++) { + perms[i] = new TablePermission(table, family, column, actions[i]); + } + + checkTablePerms(table, perms); + } + + public void checkTablePerms(byte[] table, Permission...perms) throws IOException { + HTable acl = new HTable(conf, table); + AccessControllerProtocol protocol = + acl.coprocessorProxy(AccessControllerProtocol.class, new byte[0]); + + protocol.checkPermissions(perms); + } + + public void grant(AccessControllerProtocol protocol, User user, byte[] t, byte[] f, + byte[] q, Permission.Action... actions) throws IOException { + protocol.grant(Bytes.toBytes(user.getShortName()), new TablePermission(t, f, q, actions)); + } + + @Test + public void testCheckPermissions() throws Exception { + final HTable acl = new HTable(conf, AccessControlLists.ACL_TABLE_NAME); + final AccessControllerProtocol protocol = + acl.coprocessorProxy(AccessControllerProtocol.class, TEST_TABLE); + + //-------------------------------------- + //test global permissions + PrivilegedExceptionAction globalAdmin = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkGlobalPerms(Permission.Action.ADMIN); + return null; + } + }; + //verify that only superuser can admin + verifyGlobal(globalAdmin); + + //-------------------------------------- + //test multiple permissions + PrivilegedExceptionAction globalReadWrite = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkGlobalPerms(Permission.Action.READ, Permission.Action.WRITE); + return null; + } + }; + + verifyGlobal(globalReadWrite); + + //-------------------------------------- + //table/column/qualifier level permissions + final byte[] TEST_Q1 = Bytes.toBytes("q1"); + final byte[] TEST_Q2 = Bytes.toBytes("q2"); + + User userTable = User.createUserForTesting(conf, "user_check_perms_table", new String[0]); + User userColumn = User.createUserForTesting(conf, "user_check_perms_family", new String[0]); + User userQualifier = User.createUserForTesting(conf, "user_check_perms_q", new String[0]); + + grant(protocol, userTable, TEST_TABLE, null, null, Permission.Action.READ); + grant(protocol, userColumn, TEST_TABLE, TEST_FAMILY, null, Permission.Action.READ); + grant(protocol, userQualifier, TEST_TABLE, TEST_FAMILY, TEST_Q1, Permission.Action.READ); + + PrivilegedExceptionAction tableRead = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkTablePerms(TEST_TABLE, null, null, Permission.Action.READ); + return null; + } + }; + + PrivilegedExceptionAction columnRead = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkTablePerms(TEST_TABLE, TEST_FAMILY, null, Permission.Action.READ); + return null; + } + }; + + PrivilegedExceptionAction qualifierRead = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkTablePerms(TEST_TABLE, TEST_FAMILY, TEST_Q1, Permission.Action.READ); + return null; + } + }; + + PrivilegedExceptionAction multiQualifierRead = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkTablePerms(TEST_TABLE, new Permission[] { + new TablePermission(TEST_TABLE, TEST_FAMILY, TEST_Q1, Permission.Action.READ), + new TablePermission(TEST_TABLE, TEST_FAMILY, TEST_Q2, Permission.Action.READ), + }); + return null; + } + }; + + PrivilegedExceptionAction globalAndTableRead = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkTablePerms(TEST_TABLE, new Permission[] { + new Permission(Permission.Action.READ), + new TablePermission(TEST_TABLE, null, (byte[])null, Permission.Action.READ), + }); + return null; + } + }; + + PrivilegedExceptionAction noCheck = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkTablePerms(TEST_TABLE, new Permission[0]); + return null; + } + }; + + verifyAllowed(tableRead, SUPERUSER, userTable); + verifyDenied(tableRead, userColumn, userQualifier); + + verifyAllowed(columnRead, SUPERUSER, userTable, userColumn); + verifyDenied(columnRead, userQualifier); + + verifyAllowed(qualifierRead, SUPERUSER, userTable, userColumn, userQualifier); + + verifyAllowed(multiQualifierRead, SUPERUSER, userTable, userColumn); + verifyDenied(multiQualifierRead, userQualifier); + + verifyAllowed(globalAndTableRead, SUPERUSER); + verifyDenied(globalAndTableRead, userTable, userColumn, userQualifier); + + verifyAllowed(noCheck, SUPERUSER, userTable, userColumn, userQualifier); + + //-------------------------------------- + //test family level multiple permissions + PrivilegedExceptionAction familyReadWrite = new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + checkTablePerms(TEST_TABLE, TEST_FAMILY, null, Permission.Action.READ, + Permission.Action.WRITE); + return null; + } + }; + // should be allowed + verifyAllowed(familyReadWrite, SUPERUSER, USER_OWNER, USER_RW); + // should be denied + verifyDenied(familyReadWrite, USER_NONE, USER_RO); + + //-------------------------------------- + //check for wrong table region + try { + //but ask for TablePermissions for TEST_TABLE + protocol.checkPermissions(new Permission[] {(Permission) new TablePermission( + TEST_TABLE, null, (byte[])null, Permission.Action.CREATE)}); + fail("this should have thrown CoprocessorException"); + } catch(CoprocessorException ex) { + //expected + } + + } }