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.assertFalse;
021import static org.junit.jupiter.api.Assertions.assertNotNull;
022import static org.junit.jupiter.api.Assertions.assertNull;
023
024import java.util.Arrays;
025import java.util.List;
026import java.util.stream.Stream;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.hbase.HBaseTestingUtil;
029import org.apache.hadoop.hbase.HConstants;
030import org.apache.hadoop.hbase.TableName;
031import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
032import org.apache.hadoop.hbase.client.TableDescriptor;
033import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
034import org.apache.hadoop.hbase.master.HMaster;
035import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
036import org.apache.hadoop.hbase.regionserver.HRegion;
037import org.apache.hadoop.hbase.regionserver.HRegionServer;
038import org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost;
039import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost;
040import org.apache.hadoop.hbase.testclassification.MediumTests;
041import org.apache.hadoop.hbase.testclassification.SecurityTests;
042import org.junit.jupiter.api.AfterEach;
043import org.junit.jupiter.api.BeforeEach;
044import org.junit.jupiter.api.Tag;
045import org.junit.jupiter.params.ParameterizedTest;
046import org.junit.jupiter.params.provider.MethodSource;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050@Tag(SecurityTests.TAG)
051@Tag(MediumTests.TAG)
052public class TestReadOnlyControllerCoprocessorLoading {
053
054  private static final Logger LOG = LoggerFactory.getLogger(TestReadOnlyController.class);
055  private HBaseTestingUtil TEST_UTIL;
056
057  Configuration conf;
058  TableName tableName = TableName.valueOf("test_table");
059  HMaster master;
060  HRegionServer regionServer;
061  HRegion region;
062
063  private boolean initialReadOnlyMode;
064
065  public TestReadOnlyControllerCoprocessorLoading() {
066    this.initialReadOnlyMode = false;
067  }
068
069  @BeforeEach
070  public void setup() throws Exception {
071    TEST_UTIL = new HBaseTestingUtil();
072    if (TEST_UTIL.getMiniHBaseCluster() != null) {
073      TEST_UTIL.shutdownMiniCluster();
074    }
075  }
076
077  @AfterEach
078  public void tearDown() throws Exception {
079    TEST_UTIL.shutdownMiniCluster();
080  }
081
082  private void setupMiniCluster(boolean isReadOnlyEnabled) throws Exception {
083    conf = TEST_UTIL.getConfiguration();
084    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, isReadOnlyEnabled);
085    TEST_UTIL.startMiniCluster(1);
086
087    master = TEST_UTIL.getMiniHBaseCluster().getMaster();
088    regionServer = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
089  }
090
091  private void createTable() throws Exception {
092    // create a table to get at a region
093    TableDescriptor desc = TableDescriptorBuilder.newBuilder(tableName)
094      .setColumnFamily(ColumnFamilyDescriptorBuilder.of("cf")).build();
095    TEST_UTIL.getAdmin().createTable(desc);
096
097    List<HRegion> regions = regionServer.getRegions(tableName);
098    assertFalse(regions.isEmpty());
099    region = regions.get(0);
100  }
101
102  private Configuration setReadOnlyMode(boolean isReadOnlyEnabled) {
103    // Create a new configuration to mimic client server behavior
104    // otherwise the existing conf object is shared with the cluster
105    // and can cause side effects on other tests if not reset properly.
106    // This way we can ensure that only the coprocessor loading is tested
107    // without impacting other tests.
108    HBaseTestingUtil NEW_TEST_UTIL = new HBaseTestingUtil();
109    Configuration newConf = NEW_TEST_UTIL.getConfiguration();
110    // Set the read-only enabled config dynamically after cluster startup
111    newConf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, isReadOnlyEnabled);
112    master.getConfigurationManager().notifyAllObservers(newConf);
113    regionServer.getConfigurationManager().notifyAllObservers(newConf);
114    if (region != null) {
115      region.getConfigurationManager().notifyAllObservers(newConf);
116    }
117    return newConf;
118  }
119
120  private void verifyMasterReadOnlyControllerLoading(boolean isReadOnlyEnabled) {
121    MasterCoprocessorHost masterCPHost = master.getMasterCoprocessorHost();
122    if (isReadOnlyEnabled) {
123      assertNotNull(masterCPHost.findCoprocessor(MasterReadOnlyController.class.getName()),
124        MasterReadOnlyController.class.getName()
125          + " should be loaded at startup when readonly is true.");
126    } else {
127      assertNull(masterCPHost.findCoprocessor(MasterReadOnlyController.class.getName()),
128        MasterReadOnlyController.class.getName()
129          + " should not be loaded at startup when readonly support property is false.");
130    }
131  }
132
133  private void verifyRegionServerReadOnlyControllerLoading(boolean isReadOnlyEnabled) {
134    RegionServerCoprocessorHost rsCPHost = regionServer.getRegionServerCoprocessorHost();
135    if (isReadOnlyEnabled) {
136      assertNotNull(rsCPHost.findCoprocessor(RegionServerReadOnlyController.class.getName()),
137        RegionServerReadOnlyController.class.getName()
138          + " should be loaded at startup when readonly is true.");
139    } else {
140      assertNull(rsCPHost.findCoprocessor(RegionServerReadOnlyController.class.getName()),
141        RegionServerReadOnlyController.class.getName()
142          + " should not be loaded at startup when readonly support property is false.");
143    }
144  }
145
146  private void verifyRegionReadOnlyControllerLoading(boolean isReadOnlyEnabled) {
147    RegionCoprocessorHost regionCPHost = region.getCoprocessorHost();
148
149    if (isReadOnlyEnabled) {
150      assertNotNull(regionCPHost.findCoprocessor(RegionReadOnlyController.class.getName()),
151        RegionReadOnlyController.class.getName()
152          + " should be loaded at startup when readonly is true.");
153      assertNotNull(regionCPHost.findCoprocessor(EndpointReadOnlyController.class.getName()),
154        EndpointReadOnlyController.class.getName()
155          + " should be loaded at startup when readonly is true.");
156      assertNotNull(regionCPHost.findCoprocessor(BulkLoadReadOnlyController.class.getName()),
157        BulkLoadReadOnlyController.class.getName()
158          + " should be loaded at startup when readonly is true.");
159    } else {
160      assertNull(regionCPHost.findCoprocessor(RegionReadOnlyController.class.getName()),
161        RegionReadOnlyController.class.getName()
162          + " should not be loaded at startup when readonly support property is false");
163      assertNull(regionCPHost.findCoprocessor(EndpointReadOnlyController.class.getName()),
164        EndpointReadOnlyController.class.getName()
165          + " should not be loaded at startup when readonly support property is false");
166      assertNull(regionCPHost.findCoprocessor(BulkLoadReadOnlyController.class.getName()),
167        BulkLoadReadOnlyController.class.getName()
168          + " should not be loaded at startup when readonly support property is false");
169    }
170  }
171
172  private void verifyReadOnlyState(boolean isReadOnlyEnabled) throws Exception {
173    verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled);
174    verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled);
175    verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled);
176  }
177
178  @ParameterizedTest(name = "initialReadOnlyMode={0}")
179  @MethodSource("parameters")
180  public void testReadOnlyControllerStartupBehavior(boolean initialReadOnlyMode) throws Exception {
181    this.initialReadOnlyMode = initialReadOnlyMode;
182    setupMiniCluster(initialReadOnlyMode);
183    // Table creation is needed to get a region and verify region coprocessor loading hence we can't
184    // test region coprocessor loading at startup.
185    // This will get covered in the dynamic loading test where we will also verify that the
186    // coprocessors are loaded at after table creation dynamically.
187    verifyMasterReadOnlyControllerLoading(initialReadOnlyMode);
188    verifyRegionServerReadOnlyControllerLoading(initialReadOnlyMode);
189  }
190
191  @ParameterizedTest(name = "initialReadOnlyMode={0}")
192  @MethodSource("parameters")
193  public void testReadOnlyControllerLoadedWhenEnabledDynamically(boolean initialReadOnlyMode)
194    throws Exception {
195    this.initialReadOnlyMode = initialReadOnlyMode;
196    setupMiniCluster(initialReadOnlyMode);
197    if (!initialReadOnlyMode) {
198      createTable();
199    }
200    boolean isReadOnlyEnabled = true;
201    setReadOnlyMode(isReadOnlyEnabled);
202    verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled);
203    verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled);
204    if (!initialReadOnlyMode) {
205      verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled);
206    }
207  }
208
209  @ParameterizedTest(name = "initialReadOnlyMode={0}")
210  @MethodSource("parameters")
211  public void testReadOnlyControllerUnloadedWhenDisabledDynamically(boolean initialReadOnlyMode)
212    throws Exception {
213    this.initialReadOnlyMode = initialReadOnlyMode;
214    setupMiniCluster(initialReadOnlyMode);
215    boolean isReadOnlyEnabled = false;
216    Configuration newConf = setReadOnlyMode(isReadOnlyEnabled);
217    createTable();
218    // The newly created table's region has a stale conf that needs to be updated
219    region.onConfigurationChange(newConf);
220    verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled);
221    verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled);
222    verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled);
223  }
224
225  @ParameterizedTest(name = "initialReadOnlyMode={0}")
226  @MethodSource("parameters")
227  public void testReadOnlyControllerLoadUnloadedWhenMultipleReadOnlyToggle(
228    boolean initialReadOnlyMode) throws Exception {
229    this.initialReadOnlyMode = initialReadOnlyMode;
230    setupMiniCluster(initialReadOnlyMode);
231
232    // Ensure region exists before validation
233    Configuration newConf = setReadOnlyMode(false);
234    createTable();
235    // The newly created table's region has a stale conf that needs to be updated
236    region.onConfigurationChange(newConf);
237    verifyReadOnlyState(false);
238
239    // Define toggle sequence
240    boolean[] toggleSequence = new boolean[] { true, false, // basic toggle
241      true, true, // idempotent enable
242      false, false // idempotent disable
243    };
244
245    for (int i = 0; i < toggleSequence.length; i++) {
246      boolean state = toggleSequence[i];
247      LOG.info("Toggling read-only mode to {} (step {})", state, i);
248
249      setReadOnlyMode(state);
250      verifyReadOnlyState(state);
251    }
252  }
253
254  static Stream<Boolean> parameters() {
255    return Arrays.stream(new Boolean[] { Boolean.TRUE, Boolean.FALSE });
256  }
257}