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