001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.security.access; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertFalse; 022import static org.junit.Assert.assertTrue; 023 024import java.util.UUID; 025import org.apache.hadoop.conf.Configuration; 026import org.apache.hadoop.hbase.Coprocessor; 027import org.apache.hadoop.hbase.HBaseClassTestRule; 028import org.apache.hadoop.hbase.HBaseTestingUtility; 029import org.apache.hadoop.hbase.HColumnDescriptor; 030import org.apache.hadoop.hbase.HConstants; 031import org.apache.hadoop.hbase.HTableDescriptor; 032import org.apache.hadoop.hbase.TableNotFoundException; 033import org.apache.hadoop.hbase.TestTableName; 034import org.apache.hadoop.hbase.client.Admin; 035import org.apache.hadoop.hbase.client.Connection; 036import org.apache.hadoop.hbase.client.ConnectionFactory; 037import org.apache.hadoop.hbase.client.Put; 038import org.apache.hadoop.hbase.client.Result; 039import org.apache.hadoop.hbase.client.Scan; 040import org.apache.hadoop.hbase.client.Table; 041import org.apache.hadoop.hbase.master.MasterCoprocessorHost; 042import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost; 043import org.apache.hadoop.hbase.security.User; 044import org.apache.hadoop.hbase.security.access.Permission.Action; 045import org.apache.hadoop.hbase.testclassification.MediumTests; 046import org.apache.hadoop.hbase.testclassification.SecurityTests; 047import org.apache.hadoop.hbase.util.Bytes; 048import org.junit.After; 049import org.junit.AfterClass; 050import org.junit.Before; 051import org.junit.BeforeClass; 052import org.junit.ClassRule; 053import org.junit.Rule; 054import org.junit.Test; 055import org.junit.experimental.categories.Category; 056import org.slf4j.Logger; 057import org.slf4j.LoggerFactory; 058 059@Category({SecurityTests.class, MediumTests.class}) 060public class TestScanEarlyTermination extends SecureTestUtil { 061 062 @ClassRule 063 public static final HBaseClassTestRule CLASS_RULE = 064 HBaseClassTestRule.forClass(TestScanEarlyTermination.class); 065 066 private static final Logger LOG = LoggerFactory.getLogger(TestScanEarlyTermination.class); 067 068 @Rule 069 public TestTableName TEST_TABLE = new TestTableName(); 070 private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); 071 private static final byte[] TEST_FAMILY1 = Bytes.toBytes("f1"); 072 private static final byte[] TEST_FAMILY2 = Bytes.toBytes("f2"); 073 private static final byte[] TEST_ROW = Bytes.toBytes("testrow"); 074 private static final byte[] TEST_Q1 = Bytes.toBytes("q1"); 075 private static final byte[] TEST_Q2 = Bytes.toBytes("q2"); 076 private static final byte[] ZERO = Bytes.toBytes(0L); 077 078 private static Configuration conf; 079 080 private static User USER_OWNER; 081 private static User USER_OTHER; 082 083 @BeforeClass 084 public static void setupBeforeClass() throws Exception { 085 // setup configuration 086 conf = TEST_UTIL.getConfiguration(); 087 conf.setInt(HConstants.REGION_SERVER_HIGH_PRIORITY_HANDLER_COUNT, 10); 088 // Enable security 089 enableSecurity(conf); 090 // Verify enableSecurity sets up what we require 091 verifyConfiguration(conf); 092 093 TEST_UTIL.startMiniCluster(); 094 MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster().getMaster() 095 .getMasterCoprocessorHost(); 096 cpHost.load(AccessController.class, Coprocessor.PRIORITY_HIGHEST, conf); 097 AccessController ac = (AccessController) 098 cpHost.findCoprocessor(AccessController.class.getName()); 099 cpHost.createEnvironment(ac, Coprocessor.PRIORITY_HIGHEST, 1, conf); 100 RegionServerCoprocessorHost rsHost = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0) 101 .getRegionServerCoprocessorHost(); 102 rsHost.createEnvironment(ac, Coprocessor.PRIORITY_HIGHEST, 1, conf); 103 104 // Wait for the ACL table to become available 105 TEST_UTIL.waitTableEnabled(AccessControlLists.ACL_TABLE_NAME); 106 107 // create a set of test users 108 USER_OWNER = User.createUserForTesting(conf, "owner", new String[0]); 109 USER_OTHER = User.createUserForTesting(conf, "other", new String[0]); 110 } 111 112 @AfterClass 113 public static void tearDownAfterClass() throws Exception { 114 TEST_UTIL.shutdownMiniCluster(); 115 } 116 117 @Before 118 public void setUp() throws Exception { 119 Admin admin = TEST_UTIL.getAdmin(); 120 HTableDescriptor htd = new HTableDescriptor(TEST_TABLE.getTableName()); 121 htd.setOwner(USER_OWNER); 122 HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY1); 123 hcd.setMaxVersions(10); 124 htd.addFamily(hcd); 125 hcd = new HColumnDescriptor(TEST_FAMILY2); 126 hcd.setMaxVersions(10); 127 htd.addFamily(hcd); 128 129 // Enable backwards compatible early termination behavior in the HTD. We 130 // want to confirm that the per-table configuration is properly picked up. 131 htd.setConfiguration(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT, "true"); 132 133 admin.createTable(htd); 134 TEST_UTIL.waitUntilAllRegionsAssigned(TEST_TABLE.getTableName()); 135 } 136 137 @After 138 public void tearDown() throws Exception { 139 // Clean the _acl_ table 140 try { 141 TEST_UTIL.deleteTable(TEST_TABLE.getTableName()); 142 } catch (TableNotFoundException ex) { 143 // Test deleted the table, no problem 144 LOG.info("Test deleted table " + TEST_TABLE.getTableName()); 145 } 146 assertEquals(0, AccessControlLists.getTablePermissions(conf, TEST_TABLE.getTableName()).size()); 147 } 148 149 @Test 150 public void testEarlyScanTermination() throws Exception { 151 // Grant USER_OTHER access to TEST_FAMILY1 only 152 grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(), TEST_FAMILY1, 153 null, Action.READ); 154 155 // Set up test data 156 verifyAllowed(new AccessTestAction() { 157 @Override 158 public Object run() throws Exception { 159 // force a new RS connection 160 conf.set("testkey", UUID.randomUUID().toString()); 161 Connection connection = ConnectionFactory.createConnection(conf); 162 Table t = connection.getTable(TEST_TABLE.getTableName()); 163 try { 164 Put put = new Put(TEST_ROW).addColumn(TEST_FAMILY1, TEST_Q1, ZERO); 165 t.put(put); 166 // Set a READ cell ACL for USER_OTHER on this value in FAMILY2 167 put = new Put(TEST_ROW).addColumn(TEST_FAMILY2, TEST_Q1, ZERO); 168 put.setACL(USER_OTHER.getShortName(), new Permission(Action.READ)); 169 t.put(put); 170 // Set an empty cell ACL for USER_OTHER on this other value in FAMILY2 171 put = new Put(TEST_ROW).addColumn(TEST_FAMILY2, TEST_Q2, ZERO); 172 put.setACL(USER_OTHER.getShortName(), new Permission()); 173 t.put(put); 174 } finally { 175 t.close(); 176 connection.close(); 177 } 178 return null; 179 } 180 }, USER_OWNER); 181 182 // A scan of FAMILY1 will be allowed 183 verifyAllowed(new AccessTestAction() { 184 @Override 185 public Object run() throws Exception { 186 // force a new RS connection 187 conf.set("testkey", UUID.randomUUID().toString()); 188 Connection connection = ConnectionFactory.createConnection(conf); 189 Table t = connection.getTable(TEST_TABLE.getTableName()); 190 try { 191 Scan scan = new Scan().addFamily(TEST_FAMILY1); 192 Result result = t.getScanner(scan).next(); 193 if (result != null) { 194 assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1)); 195 assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1)); 196 return result.listCells(); 197 } 198 return null; 199 } finally { 200 t.close(); 201 connection.close(); 202 } 203 } 204 }, USER_OTHER); 205 206 // A scan of FAMILY1 and FAMILY2 will produce results for FAMILY1 without 207 // throwing an exception, however no cells from FAMILY2 will be returned 208 // because we early out checks at the CF level. 209 verifyAllowed(new AccessTestAction() { 210 @Override 211 public Object run() throws Exception { 212 // force a new RS connection 213 conf.set("testkey", UUID.randomUUID().toString()); 214 Connection connection = ConnectionFactory.createConnection(conf); 215 Table t = connection.getTable(TEST_TABLE.getTableName()); 216 try { 217 Scan scan = new Scan(); 218 Result result = t.getScanner(scan).next(); 219 if (result != null) { 220 assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1)); 221 assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1)); 222 return result.listCells(); 223 } 224 return null; 225 } finally { 226 t.close(); 227 connection.close(); 228 } 229 } 230 }, USER_OTHER); 231 232 // A scan of FAMILY2 will throw an AccessDeniedException 233 verifyDenied(new AccessTestAction() { 234 @Override 235 public Object run() throws Exception { 236 // force a new RS connection 237 conf.set("testkey", UUID.randomUUID().toString()); 238 Connection connection = ConnectionFactory.createConnection(conf); 239 Table t = connection.getTable(TEST_TABLE.getTableName()); 240 try { 241 Scan scan = new Scan().addFamily(TEST_FAMILY2); 242 Result result = t.getScanner(scan).next(); 243 if (result != null) { 244 return result.listCells(); 245 } 246 return null; 247 } finally { 248 t.close(); 249 connection.close(); 250 } 251 } 252 }, USER_OTHER); 253 254 // Now grant USER_OTHER access to TEST_FAMILY2:TEST_Q2 255 grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(), TEST_FAMILY2, 256 TEST_Q2, Action.READ); 257 258 // A scan of FAMILY1 and FAMILY2 will produce combined results. In FAMILY2 259 // we have access granted to Q2 at the CF level. Because we early out 260 // checks at the CF level the cell ACL on Q1 also granting access is ignored. 261 verifyAllowed(new AccessTestAction() { 262 @Override 263 public Object run() throws Exception { 264 // force a new RS connection 265 conf.set("testkey", UUID.randomUUID().toString()); 266 Connection connection = ConnectionFactory.createConnection(conf); 267 Table t = connection.getTable(TEST_TABLE.getTableName()); 268 try { 269 Scan scan = new Scan(); 270 Result result = t.getScanner(scan).next(); 271 if (result != null) { 272 assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1)); 273 assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1)); 274 assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY2, TEST_Q2)); 275 return result.listCells(); 276 } 277 return null; 278 } finally { 279 t.close(); 280 connection.close(); 281 } 282 } 283 }, USER_OTHER); 284 } 285}