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.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER; 021import static org.junit.jupiter.api.Assertions.assertThrows; 022import static org.junit.jupiter.api.Assertions.assertTrue; 023 024import java.io.IOException; 025import java.util.ArrayList; 026import java.util.List; 027import org.apache.hadoop.conf.Configuration; 028import org.apache.hadoop.hbase.HBaseTestingUtil; 029import org.apache.hadoop.hbase.HConstants; 030import org.apache.hadoop.hbase.SingleProcessHBaseCluster; 031import org.apache.hadoop.hbase.TableName; 032import org.apache.hadoop.hbase.client.Connection; 033import org.apache.hadoop.hbase.client.ConnectionFactory; 034import org.apache.hadoop.hbase.client.Delete; 035import org.apache.hadoop.hbase.client.Put; 036import org.apache.hadoop.hbase.client.Row; 037import org.apache.hadoop.hbase.client.Table; 038import org.apache.hadoop.hbase.master.HMaster; 039import org.apache.hadoop.hbase.regionserver.HRegionServer; 040import org.apache.hadoop.hbase.testclassification.LargeTests; 041import org.apache.hadoop.hbase.testclassification.SecurityTests; 042import org.apache.hadoop.hbase.util.Bytes; 043import org.apache.hadoop.hbase.util.ConfigurationUtil; 044import org.junit.jupiter.api.AfterEach; 045import org.junit.jupiter.api.BeforeEach; 046import org.junit.jupiter.api.Tag; 047import org.junit.jupiter.api.Test; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050 051@Tag(SecurityTests.TAG) 052@Tag(LargeTests.TAG) 053@SuppressWarnings("deprecation") 054public class TestReadOnlyController { 055 056 private static final Logger LOG = LoggerFactory.getLogger(TestReadOnlyController.class); 057 private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); 058 private static final TableName TEST_TABLE = TableName.valueOf("read_only_test_table"); 059 private static final byte[] TEST_FAMILY = Bytes.toBytes("read_only_table_col_fam"); 060 private static HRegionServer hRegionServer; 061 private static HMaster hMaster; 062 private static Configuration conf; 063 private static Connection connection; 064 private static SingleProcessHBaseCluster cluster; 065 066 private static Table testTable; 067 068 @BeforeEach 069 public void beforeClass() throws Exception { 070 conf = TEST_UTIL.getConfiguration(); 071 072 // Shorten the run time of failed unit tests by limiting retries and the session timeout 073 // threshold 074 conf.setInt(HBASE_CLIENT_RETRIES_NUMBER, 1); 075 conf.setInt(HConstants.ZK_SESSION_TIMEOUT, 1000); 076 077 // Set up test class with Read-Only mode disabled so a table can be created 078 conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 079 080 try { 081 // Start the test cluster 082 cluster = TEST_UTIL.startMiniCluster(1); 083 084 hMaster = cluster.getMaster(); 085 hRegionServer = cluster.getRegionServerThreads().get(0).getRegionServer(); 086 connection = ConnectionFactory.createConnection(conf); 087 088 // Create a test table 089 testTable = TEST_UTIL.createTable(TEST_TABLE, TEST_FAMILY); 090 } catch (Exception e) { 091 // Delete the created table, and clean up the connection and cluster before throwing an 092 // exception 093 disableReadOnlyMode(); 094 TEST_UTIL.deleteTable(TEST_TABLE); 095 connection.close(); 096 TEST_UTIL.shutdownMiniCluster(); 097 throw new RuntimeException(e); 098 } 099 } 100 101 @AfterEach 102 public void afterClass() throws Exception { 103 if (connection != null) { 104 connection.close(); 105 } 106 TEST_UTIL.shutdownMiniCluster(); 107 } 108 109 private static void enableReadOnlyMode() { 110 // Dynamically enable Read-Only mode if it is not active 111 if (!ConfigurationUtil.isReadOnlyModeEnabledInConf(conf)) { 112 LOG.info("Dynamically enabling Read-Only mode by setting {} to true", 113 HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT); 114 conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true); 115 notifyObservers(); 116 } 117 } 118 119 private static void disableReadOnlyMode() { 120 // Dynamically disable Read-Only mode if it is active 121 if (ConfigurationUtil.isReadOnlyModeEnabledInConf(conf)) { 122 LOG.info("Dynamically disabling Read-Only mode by setting {} to false", 123 HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT); 124 conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); 125 notifyObservers(); 126 } 127 } 128 129 private static void notifyObservers() { 130 LOG.info("Notifying observers about configuration changes"); 131 hMaster.getConfigurationManager().notifyAllObservers(conf); 132 hRegionServer.getConfigurationManager().notifyAllObservers(conf); 133 } 134 135 // The test case for successfully creating a table with Read-Only mode disabled happens when 136 // setting up the test class, so we only need a test function for a failed table creation. 137 @Test 138 public void testCannotCreateTableWithReadOnlyEnabled() throws IOException { 139 enableReadOnlyMode(); 140 TableName newTable = TableName.valueOf("bad_read_only_test_table"); 141 142 IOException exception = assertThrows(IOException.class, () -> { 143 TEST_UTIL.createTable(newTable, TEST_FAMILY); 144 }); 145 assertTrue(exception.getMessage().contains("Operation not allowed in Read-Only Mode")); 146 } 147 148 @Test 149 public void testPutWithReadOnlyDisabled() throws IOException { 150 // Successfully put a row in the table since Read-Only mode is disabled 151 disableReadOnlyMode(); 152 final byte[] row2 = Bytes.toBytes("row2"); 153 final byte[] value = Bytes.toBytes("efgh"); 154 Put put = new Put(row2); 155 put.addColumn(TEST_FAMILY, null, value); 156 testTable.put(put); 157 } 158 159 @Test 160 public void testCannotPutWithReadOnlyEnabled() throws IOException { 161 // Prepare a Put command with Read-Only mode enabled 162 enableReadOnlyMode(); 163 final byte[] row1 = Bytes.toBytes("row1"); 164 final byte[] value = Bytes.toBytes("abcd"); 165 Put put = new Put(row1); 166 put.addColumn(TEST_FAMILY, null, value); 167 168 IOException exception = assertThrows(IOException.class, () -> { 169 testTable.put(put); 170 }); 171 assertTrue(exception.getMessage().contains("Operation not allowed in Read-Only Mode")); 172 } 173 174 @Test 175 public void testBatchPutWithReadOnlyDisabled() throws IOException, InterruptedException { 176 // Successfully create and run a batch Put operation with Read-Only mode disabled 177 disableReadOnlyMode(); 178 List<Row> actions = new ArrayList<>(); 179 actions.add(new Put(Bytes.toBytes("row10")).addColumn(TEST_FAMILY, null, Bytes.toBytes("10"))); 180 actions.add(new Delete(Bytes.toBytes("row10"))); 181 testTable.batch(actions, null); 182 } 183 184 @Test 185 public void testCannotBatchPutWithReadOnlyEnabled() throws IOException, InterruptedException { 186 // Create a batch Put operation that is expected to fail with Read-Only mode enabled 187 enableReadOnlyMode(); 188 List<Row> actions = new ArrayList<>(); 189 actions.add(new Put(Bytes.toBytes("row11")).addColumn(TEST_FAMILY, null, Bytes.toBytes("11"))); 190 actions.add(new Delete(Bytes.toBytes("row11"))); 191 192 IOException exception = assertThrows(IOException.class, () -> { 193 testTable.batch(actions, null); 194 }); 195 assertTrue(exception.getMessage().contains("Operation not allowed in Read-Only Mode")); 196 } 197}