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}