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.jupiter.api.Assertions.assertEquals; 021import static org.junit.jupiter.api.Assertions.assertFalse; 022import static org.junit.jupiter.api.Assertions.assertTrue; 023 024import org.apache.hadoop.conf.Configuration; 025import org.apache.hadoop.hbase.Coprocessor; 026import org.apache.hadoop.hbase.HBaseTestingUtil; 027import org.apache.hadoop.hbase.HConstants; 028import org.apache.hadoop.hbase.TableName; 029import org.apache.hadoop.hbase.TableNotFoundException; 030import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 031import org.apache.hadoop.hbase.client.Connection; 032import org.apache.hadoop.hbase.client.ConnectionFactory; 033import org.apache.hadoop.hbase.client.Put; 034import org.apache.hadoop.hbase.client.Result; 035import org.apache.hadoop.hbase.client.Scan; 036import org.apache.hadoop.hbase.client.Table; 037import org.apache.hadoop.hbase.client.TableDescriptor; 038import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 039import org.apache.hadoop.hbase.master.MasterCoprocessorHost; 040import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost; 041import org.apache.hadoop.hbase.security.User; 042import org.apache.hadoop.hbase.security.access.Permission.Action; 043import org.apache.hadoop.hbase.testclassification.MediumTests; 044import org.apache.hadoop.hbase.testclassification.SecurityTests; 045import org.apache.hadoop.hbase.util.Bytes; 046import org.junit.jupiter.api.AfterAll; 047import org.junit.jupiter.api.AfterEach; 048import org.junit.jupiter.api.BeforeAll; 049import org.junit.jupiter.api.BeforeEach; 050import org.junit.jupiter.api.Tag; 051import org.junit.jupiter.api.Test; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054 055@Tag(SecurityTests.TAG) 056@Tag(MediumTests.TAG) 057public class TestScanEarlyTermination extends SecureTestUtil { 058 059 private static final Logger LOG = LoggerFactory.getLogger(TestScanEarlyTermination.class); 060 061 private static TableName testTable; 062 private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 063 private static final byte[] TEST_FAMILY1 = Bytes.toBytes("f1"); 064 private static final byte[] TEST_FAMILY2 = Bytes.toBytes("f2"); 065 private static final byte[] TEST_ROW = Bytes.toBytes("testrow"); 066 private static final byte[] TEST_Q1 = Bytes.toBytes("q1"); 067 private static final byte[] TEST_Q2 = Bytes.toBytes("q2"); 068 private static final byte[] ZERO = Bytes.toBytes(0L); 069 070 private static Configuration conf; 071 072 private static User USER_OWNER; 073 private static User USER_OTHER; 074 075 @BeforeAll 076 public static void setupBeforeClass() throws Exception { 077 testTable = TableName.valueOf("testTable"); 078 // setup configuration 079 conf = TEST_UTIL.getConfiguration(); 080 conf.setInt(HConstants.REGION_SERVER_HIGH_PRIORITY_HANDLER_COUNT, 10); 081 // Enable security 082 enableSecurity(conf); 083 // Verify enableSecurity sets up what we require 084 verifyConfiguration(conf); 085 086 TEST_UTIL.startMiniCluster(); 087 MasterCoprocessorHost cpHost = 088 TEST_UTIL.getMiniHBaseCluster().getMaster().getMasterCoprocessorHost(); 089 cpHost.load(AccessController.class, Coprocessor.PRIORITY_HIGHEST, conf); 090 AccessController ac = 091 (AccessController) cpHost.findCoprocessor(AccessController.class.getName()); 092 cpHost.createEnvironment(ac, Coprocessor.PRIORITY_HIGHEST, 1, conf); 093 RegionServerCoprocessorHost rsHost = 094 TEST_UTIL.getMiniHBaseCluster().getRegionServer(0).getRegionServerCoprocessorHost(); 095 rsHost.createEnvironment(ac, Coprocessor.PRIORITY_HIGHEST, 1, conf); 096 097 // Wait for the ACL table to become available 098 TEST_UTIL.waitTableEnabled(PermissionStorage.ACL_TABLE_NAME); 099 100 // create a set of test users 101 USER_OWNER = User.createUserForTesting(conf, "owner", new String[0]); 102 USER_OTHER = User.createUserForTesting(conf, "other", new String[0]); 103 104 // Grant table creation permission to USER_OWNER 105 grantGlobal(TEST_UTIL, USER_OWNER.getShortName(), Action.CREATE); 106 } 107 108 @AfterAll 109 public static void tearDownAfterClass() throws Exception { 110 TEST_UTIL.shutdownMiniCluster(); 111 } 112 113 @BeforeEach 114 public void setUp() throws Exception { 115 TableDescriptor tableDescriptor = TableDescriptorBuilder.newBuilder(testTable) 116 .setColumnFamily( 117 ColumnFamilyDescriptorBuilder.newBuilder(TEST_FAMILY1).setMaxVersions(10).build()) 118 .setColumnFamily( 119 ColumnFamilyDescriptorBuilder.newBuilder(TEST_FAMILY2).setMaxVersions(10).build()) 120 // Enable backwards compatible early termination behavior in the HTD. We 121 // want to confirm that the per-table configuration is properly picked up. 122 .setValue(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT, "true").build(); 123 124 createTable(TEST_UTIL, USER_OWNER, tableDescriptor); 125 TEST_UTIL.waitUntilAllRegionsAssigned(testTable); 126 } 127 128 @AfterEach 129 public void tearDown() throws Exception { 130 // Clean the _acl_ table 131 try { 132 TEST_UTIL.deleteTable(testTable); 133 } catch (TableNotFoundException ex) { 134 // Test deleted the table, no problem 135 LOG.info("Test deleted table " + testTable); 136 } 137 assertEquals(0, PermissionStorage.getTablePermissions(conf, testTable).size()); 138 } 139 140 @Test 141 public void testEarlyScanTermination() throws Exception { 142 // Grant USER_OTHER access to TEST_FAMILY1 only 143 grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), testTable, TEST_FAMILY1, null, Action.READ); 144 145 // Set up test data 146 verifyAllowed(new AccessTestAction() { 147 @Override 148 public Object run() throws Exception { 149 // force a new RS connection 150 conf.set("testkey", TEST_UTIL.getRandomUUID().toString()); 151 Connection connection = ConnectionFactory.createConnection(conf); 152 Table t = connection.getTable(testTable); 153 try { 154 Put put = new Put(TEST_ROW).addColumn(TEST_FAMILY1, TEST_Q1, ZERO); 155 t.put(put); 156 // Set a READ cell ACL for USER_OTHER on this value in FAMILY2 157 put = new Put(TEST_ROW).addColumn(TEST_FAMILY2, TEST_Q1, ZERO); 158 put.setACL(USER_OTHER.getShortName(), new Permission(Action.READ)); 159 t.put(put); 160 // Set an empty cell ACL for USER_OTHER on this other value in FAMILY2 161 put = new Put(TEST_ROW).addColumn(TEST_FAMILY2, TEST_Q2, ZERO); 162 put.setACL(USER_OTHER.getShortName(), new Permission()); 163 t.put(put); 164 } finally { 165 t.close(); 166 connection.close(); 167 } 168 return null; 169 } 170 }, USER_OWNER); 171 172 // A scan of FAMILY1 will be allowed 173 verifyAllowed(new AccessTestAction() { 174 @Override 175 public Object run() throws Exception { 176 // force a new RS connection 177 conf.set("testkey", TEST_UTIL.getRandomUUID().toString()); 178 Connection connection = ConnectionFactory.createConnection(conf); 179 Table t = connection.getTable(testTable); 180 try { 181 Scan scan = new Scan().addFamily(TEST_FAMILY1); 182 Result result = t.getScanner(scan).next(); 183 if (result != null) { 184 assertTrue(result.containsColumn(TEST_FAMILY1, TEST_Q1), "Improper exclusion"); 185 assertFalse(result.containsColumn(TEST_FAMILY2, TEST_Q1), "Improper inclusion"); 186 return result.listCells(); 187 } 188 return null; 189 } finally { 190 t.close(); 191 connection.close(); 192 } 193 } 194 }, USER_OTHER); 195 196 // A scan of FAMILY1 and FAMILY2 will produce results for FAMILY1 without 197 // throwing an exception, however no cells from FAMILY2 will be returned 198 // because we early out checks at the CF level. 199 verifyAllowed(new AccessTestAction() { 200 @Override 201 public Object run() throws Exception { 202 // force a new RS connection 203 conf.set("testkey", TEST_UTIL.getRandomUUID().toString()); 204 Connection connection = ConnectionFactory.createConnection(conf); 205 Table t = connection.getTable(testTable); 206 try { 207 Scan scan = new Scan(); 208 Result result = t.getScanner(scan).next(); 209 if (result != null) { 210 assertTrue(result.containsColumn(TEST_FAMILY1, TEST_Q1), "Improper exclusion"); 211 assertFalse(result.containsColumn(TEST_FAMILY2, TEST_Q1), "Improper inclusion"); 212 return result.listCells(); 213 } 214 return null; 215 } finally { 216 t.close(); 217 connection.close(); 218 } 219 } 220 }, USER_OTHER); 221 222 // A scan of FAMILY2 will throw an AccessDeniedException 223 verifyDenied(new AccessTestAction() { 224 @Override 225 public Object run() throws Exception { 226 // force a new RS connection 227 conf.set("testkey", TEST_UTIL.getRandomUUID().toString()); 228 Connection connection = ConnectionFactory.createConnection(conf); 229 Table t = connection.getTable(testTable); 230 try { 231 Scan scan = new Scan().addFamily(TEST_FAMILY2); 232 Result result = t.getScanner(scan).next(); 233 if (result != null) { 234 return result.listCells(); 235 } 236 return null; 237 } finally { 238 t.close(); 239 connection.close(); 240 } 241 } 242 }, USER_OTHER); 243 244 // Now grant USER_OTHER access to TEST_FAMILY2:TEST_Q2 245 grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), testTable, TEST_FAMILY2, TEST_Q2, 246 Action.READ); 247 248 // A scan of FAMILY1 and FAMILY2 will produce combined results. In FAMILY2 249 // we have access granted to Q2 at the CF level. Because we early out 250 // checks at the CF level the cell ACL on Q1 also granting access is ignored. 251 verifyAllowed(new AccessTestAction() { 252 @Override 253 public Object run() throws Exception { 254 // force a new RS connection 255 conf.set("testkey", TEST_UTIL.getRandomUUID().toString()); 256 Connection connection = ConnectionFactory.createConnection(conf); 257 Table t = connection.getTable(testTable); 258 try { 259 Scan scan = new Scan(); 260 Result result = t.getScanner(scan).next(); 261 if (result != null) { 262 assertTrue(result.containsColumn(TEST_FAMILY1, TEST_Q1), "Improper exclusion"); 263 assertFalse(result.containsColumn(TEST_FAMILY2, TEST_Q1), "Improper inclusion"); 264 assertTrue(result.containsColumn(TEST_FAMILY2, TEST_Q2), "Improper exclusion"); 265 return result.listCells(); 266 } 267 return null; 268 } finally { 269 t.close(); 270 connection.close(); 271 } 272 } 273 }, USER_OTHER); 274 } 275}