View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.security.access;
19  
20  import static org.junit.Assert.*;
21  
22  import java.util.UUID;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.hadoop.conf.Configuration;
27  import org.apache.hadoop.hbase.Coprocessor;
28  import org.apache.hadoop.hbase.HBaseTestingUtility;
29  import org.apache.hadoop.hbase.HColumnDescriptor;
30  import org.apache.hadoop.hbase.HTableDescriptor;
31  import org.apache.hadoop.hbase.testclassification.MediumTests;
32  import org.apache.hadoop.hbase.TableNotFoundException;
33  import org.apache.hadoop.hbase.client.Admin;
34  import org.apache.hadoop.hbase.client.Connection;
35  import org.apache.hadoop.hbase.client.ConnectionFactory;
36  import org.apache.hadoop.hbase.client.Put;
37  import org.apache.hadoop.hbase.client.Result;
38  import org.apache.hadoop.hbase.client.Scan;
39  import org.apache.hadoop.hbase.client.Table;
40  import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
41  import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost;
42  import org.apache.hadoop.hbase.security.User;
43  import org.apache.hadoop.hbase.security.access.Permission.Action;
44  import org.apache.hadoop.hbase.util.Bytes;
45  import org.apache.hadoop.hbase.util.TestTableName;
46  import org.apache.log4j.Level;
47  import org.apache.log4j.Logger;
48  import org.junit.After;
49  import org.junit.AfterClass;
50  import org.junit.Before;
51  import org.junit.BeforeClass;
52  import org.junit.Rule;
53  import org.junit.Test;
54  import org.junit.experimental.categories.Category;
55  
56  @Category(MediumTests.class)
57  public class TestScanEarlyTermination extends SecureTestUtil {
58    private static final Log LOG = LogFactory.getLog(TestScanEarlyTermination.class);
59  
60    static {
61      Logger.getLogger(AccessController.class).setLevel(Level.TRACE);
62      Logger.getLogger(AccessControlFilter.class).setLevel(Level.TRACE);
63      Logger.getLogger(TableAuthManager.class).setLevel(Level.TRACE);
64    }
65  
66    @Rule
67    public TestTableName TEST_TABLE = new TestTableName();
68    private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
69    private static final byte[] TEST_FAMILY1 = Bytes.toBytes("f1");
70    private static final byte[] TEST_FAMILY2 = Bytes.toBytes("f2");
71    private static final byte[] TEST_ROW = Bytes.toBytes("testrow");
72    private static final byte[] TEST_Q1 = Bytes.toBytes("q1");
73    private static final byte[] TEST_Q2 = Bytes.toBytes("q2");
74    private static final byte[] ZERO = Bytes.toBytes(0L);
75  
76    private static Configuration conf;
77  
78    private static User USER_OWNER;
79    private static User USER_OTHER;
80  
81    @BeforeClass
82    public static void setupBeforeClass() throws Exception {
83      // setup configuration
84      conf = TEST_UTIL.getConfiguration();
85      // Enable security
86      enableSecurity(conf);
87      // Verify enableSecurity sets up what we require
88      verifyConfiguration(conf);
89  
90      TEST_UTIL.startMiniCluster();
91      MasterCoprocessorHost cpHost = TEST_UTIL.getMiniHBaseCluster().getMaster()
92          .getMasterCoprocessorHost();
93      cpHost.load(AccessController.class, Coprocessor.PRIORITY_HIGHEST, conf);
94      AccessController ac = (AccessController)
95        cpHost.findCoprocessor(AccessController.class.getName());
96      cpHost.createEnvironment(AccessController.class, ac, Coprocessor.PRIORITY_HIGHEST, 1, conf);
97      RegionServerCoprocessorHost rsHost = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0)
98          .getRegionServerCoprocessorHost();
99      rsHost.createEnvironment(AccessController.class, ac, Coprocessor.PRIORITY_HIGHEST, 1, conf);
100 
101     // Wait for the ACL table to become available
102     TEST_UTIL.waitTableEnabled(AccessControlLists.ACL_TABLE_NAME);
103 
104     // create a set of test users
105     USER_OWNER = User.createUserForTesting(conf, "owner", new String[0]);
106     USER_OTHER = User.createUserForTesting(conf, "other", new String[0]);
107   }
108 
109   @AfterClass
110   public static void tearDownAfterClass() throws Exception {
111     TEST_UTIL.shutdownMiniCluster();
112   }
113 
114   @Before
115   public void setUp() throws Exception {
116     Admin admin = TEST_UTIL.getHBaseAdmin();
117     HTableDescriptor htd = new HTableDescriptor(TEST_TABLE.getTableName());
118     htd.setOwner(USER_OWNER);
119     HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY1);
120     hcd.setMaxVersions(10);
121     htd.addFamily(hcd);
122     hcd = new HColumnDescriptor(TEST_FAMILY2);
123     hcd.setMaxVersions(10);
124     htd.addFamily(hcd);
125 
126     // Enable backwards compatible early termination behavior in the HTD. We
127     // want to confirm that the per-table configuration is properly picked up.
128     htd.setConfiguration(AccessControlConstants.CF_ATTRIBUTE_EARLY_OUT, "true");
129 
130     admin.createTable(htd);
131     TEST_UTIL.waitUntilAllRegionsAssigned(TEST_TABLE.getTableName());
132   }
133 
134   @After
135   public void tearDown() throws Exception {
136     // Clean the _acl_ table
137     try {
138       TEST_UTIL.deleteTable(TEST_TABLE.getTableName());
139     } catch (TableNotFoundException ex) {
140       // Test deleted the table, no problem
141       LOG.info("Test deleted table " + TEST_TABLE.getTableName());
142     }
143     assertEquals(0, AccessControlLists.getTablePermissions(conf, TEST_TABLE.getTableName()).size());
144   }
145 
146   @Test
147   public void testEarlyScanTermination() throws Exception {
148     // Grant USER_OTHER access to TEST_FAMILY1 only
149     grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(), TEST_FAMILY1,
150       null, Action.READ);
151 
152     // Set up test data
153     verifyAllowed(new AccessTestAction() {
154       @Override
155       public Object run() throws Exception {
156         // force a new RS connection
157         conf.set("testkey", UUID.randomUUID().toString());
158         try (Connection connection = ConnectionFactory.createConnection(conf);
159              Table t = connection.getTable(TEST_TABLE.getTableName())) {
160           Put put = new Put(TEST_ROW).add(TEST_FAMILY1, TEST_Q1, ZERO);
161           t.put(put);
162           // Set a READ cell ACL for USER_OTHER on this value in FAMILY2
163           put = new Put(TEST_ROW).add(TEST_FAMILY2, TEST_Q1, ZERO);
164           put.setACL(USER_OTHER.getShortName(), new Permission(Action.READ));
165           t.put(put);
166           // Set an empty cell ACL for USER_OTHER on this other value in FAMILY2
167           put = new Put(TEST_ROW).add(TEST_FAMILY2, TEST_Q2, ZERO);
168           put.setACL(USER_OTHER.getShortName(), new Permission());
169           t.put(put);
170         }
171         return null;
172       }
173     }, USER_OWNER);
174 
175     // A scan of FAMILY1 will be allowed
176     verifyAllowed(new AccessTestAction() {
177       @Override
178       public Object run() throws Exception {
179         // force a new RS connection
180         conf.set("testkey", UUID.randomUUID().toString());
181         try (Connection connection = ConnectionFactory.createConnection(conf);
182              Table t = connection.getTable(TEST_TABLE.getTableName())) {
183           Scan scan = new Scan().addFamily(TEST_FAMILY1);
184           Result result = t.getScanner(scan).next();
185           if (result != null) {
186             assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1));
187             assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1));
188             return result.listCells();
189           }
190           return null;
191         }
192       }
193     }, USER_OTHER);
194 
195     // A scan of FAMILY1 and FAMILY2 will produce results for FAMILY1 without
196     // throwing an exception, however no cells from FAMILY2 will be returned
197     // because we early out checks at the CF level.
198     verifyAllowed(new AccessTestAction() {
199       @Override
200       public Object run() throws Exception {
201         // force a new RS connection
202         conf.set("testkey", UUID.randomUUID().toString());
203         try (Connection connection = ConnectionFactory.createConnection(conf);
204              Table t = connection.getTable(TEST_TABLE.getTableName())) {
205           Scan scan = new Scan();
206           Result result = t.getScanner(scan).next();
207           if (result != null) {
208             assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1));
209             assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1));
210             return result.listCells();
211           }
212           return null;
213         }
214       }
215     }, USER_OTHER);
216 
217     // A scan of FAMILY2 will throw an AccessDeniedException
218     verifyDenied(new AccessTestAction() {
219       @Override
220       public Object run() throws Exception {
221         // force a new RS connection
222         conf.set("testkey", UUID.randomUUID().toString());
223         try (Connection connection = ConnectionFactory.createConnection(conf);
224              Table t = connection.getTable(TEST_TABLE.getTableName())) {
225           Scan scan = new Scan().addFamily(TEST_FAMILY2);
226           Result result = t.getScanner(scan).next();
227           if (result != null) {
228             return result.listCells();
229           }
230           return null;
231         }
232       }
233     }, USER_OTHER);
234 
235     // Now grant USER_OTHER access to TEST_FAMILY2:TEST_Q2
236     grantOnTable(TEST_UTIL, USER_OTHER.getShortName(), TEST_TABLE.getTableName(), TEST_FAMILY2,
237       TEST_Q2, Action.READ);
238 
239     // A scan of FAMILY1 and FAMILY2 will produce combined results. In FAMILY2
240     // we have access granted to Q2 at the CF level. Because we early out
241     // checks at the CF level the cell ACL on Q1 also granting access is ignored.
242     verifyAllowed(new AccessTestAction() {
243       @Override
244       public Object run() throws Exception {
245         // force a new RS connection
246         conf.set("testkey", UUID.randomUUID().toString());
247         try (Connection connection = ConnectionFactory.createConnection(conf);
248              Table t = connection.getTable(TEST_TABLE.getTableName())) {
249           Scan scan = new Scan();
250           Result result = t.getScanner(scan).next();
251           if (result != null) {
252             assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY1, TEST_Q1));
253             assertFalse("Improper inclusion", result.containsColumn(TEST_FAMILY2, TEST_Q1));
254             assertTrue("Improper exclusion", result.containsColumn(TEST_FAMILY2, TEST_Q2));
255             return result.listCells();
256           }
257           return null;
258         }
259       }
260     }, USER_OTHER);
261   }
262 }