This is an automated email from the ASF dual-hosted git repository.

andor pushed a commit to branch HBASE-29081_rebased
in repository https://gitbox.apache.org/repos/asf/hbase.git

commit 6091cb2f4e43994568378d16d37b8efae6f83e2f
Author: Anuj Sharma <[email protected]>
AuthorDate: Mon Dec 8 20:59:29 2025 +0530

    HBASE-29693: Implement the missing observer functions in the read-only 
controller (#7464)
    
    * HBASE-29693: Implement the missing observer functions in the read-only 
controller
    
    * Remove setter method to set read-only configuration
---
 .../hbase/security/access/ReadOnlyController.java  | 465 +++++++++++++-
 .../TestReadOnlyControllerBulkLoadObserver.java    |  86 +++
 .../TestReadOnlyControllerEndpointObserver.java    |  86 +++
 .../TestReadOnlyControllerMasterObserver.java      | 691 +++++++++++++++++++++
 .../TestReadOnlyControllerRegionObserver.java      | 560 +++++++++++++++++
 ...TestReadOnlyControllerRegionServerObserver.java | 122 ++++
 6 files changed, 1988 insertions(+), 22 deletions(-)

diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ReadOnlyController.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ReadOnlyController.java
index 6d9efadef21..56de46b52d5 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ReadOnlyController.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ReadOnlyController.java
@@ -19,7 +19,9 @@ package org.apache.hadoop.hbase.security.access;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
@@ -30,7 +32,12 @@ import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.NamespaceDescriptor;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.client.Append;
+import org.apache.hadoop.hbase.client.BalanceRequest;
+import org.apache.hadoop.hbase.client.CheckAndMutate;
+import org.apache.hadoop.hbase.client.CheckAndMutateResult;
 import org.apache.hadoop.hbase.client.Delete;
+import org.apache.hadoop.hbase.client.Durability;
+import org.apache.hadoop.hbase.client.Increment;
 import org.apache.hadoop.hbase.client.Mutation;
 import org.apache.hadoop.hbase.client.Put;
 import org.apache.hadoop.hbase.client.RegionInfo;
@@ -55,14 +62,23 @@ import org.apache.hadoop.hbase.filter.ByteArrayComparable;
 import org.apache.hadoop.hbase.filter.Filter;
 import org.apache.hadoop.hbase.master.MasterFileSystem;
 import org.apache.hadoop.hbase.master.MasterServices;
+import org.apache.hadoop.hbase.net.Address;
+import org.apache.hadoop.hbase.quotas.GlobalQuotaSettings;
 import org.apache.hadoop.hbase.regionserver.FlushLifeCycleTracker;
+import org.apache.hadoop.hbase.regionserver.InternalScanner;
 import org.apache.hadoop.hbase.regionserver.MiniBatchOperationInProgress;
+import org.apache.hadoop.hbase.regionserver.ScanOptions;
+import org.apache.hadoop.hbase.regionserver.ScanType;
 import org.apache.hadoop.hbase.regionserver.Store;
 import org.apache.hadoop.hbase.regionserver.StoreFile;
 import 
org.apache.hadoop.hbase.regionserver.compactions.CompactionLifeCycleTracker;
+import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequest;
+import org.apache.hadoop.hbase.replication.ReplicationPeerConfig;
+import org.apache.hadoop.hbase.replication.SyncReplicationState;
 import org.apache.hadoop.hbase.util.FSUtils;
 import org.apache.hadoop.hbase.util.Pair;
 import org.apache.hadoop.hbase.wal.WALEdit;
+import org.apache.hadoop.hbase.wal.WALKey;
 import org.apache.yetus.audience.InterfaceAudience;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -113,46 +129,125 @@ public class ReadOnlyController implements 
MasterCoprocessor, RegionCoprocessor,
     return Optional.of(this);
   }
 
+  @Override
+  public void preFlush(final ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    FlushLifeCycleTracker tracker) throws IOException {
+    internalReadOnlyGuard();
+    RegionObserver.super.preFlush(c, tracker);
+  }
+
+  @Override
+  public void preFlushScannerOpen(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Store store, ScanOptions options, FlushLifeCycleTracker tracker) throws 
IOException {
+    internalReadOnlyGuard();
+    RegionObserver.super.preFlushScannerOpen(c, store, options, tracker);
+  }
+
+  @Override
+  public InternalScanner preFlush(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Store store, InternalScanner scanner, FlushLifeCycleTracker tracker) 
throws IOException {
+    internalReadOnlyGuard();
+    return RegionObserver.super.preFlush(c, store, scanner, tracker);
+  }
+
+  @Override
+  public void preMemStoreCompaction(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Store store) throws IOException {
+    internalReadOnlyGuard();
+    RegionObserver.super.preMemStoreCompaction(c, store);
+  }
+
+  @Override
+  public void preMemStoreCompactionCompactScannerOpen(
+    ObserverContext<? extends RegionCoprocessorEnvironment> c, Store store, 
ScanOptions options)
+    throws IOException {
+    internalReadOnlyGuard();
+    RegionObserver.super.preMemStoreCompactionCompactScannerOpen(c, store, 
options);
+  }
+
+  @Override
+  public InternalScanner preMemStoreCompactionCompact(
+    ObserverContext<? extends RegionCoprocessorEnvironment> c, Store store, 
InternalScanner scanner)
+    throws IOException {
+    internalReadOnlyGuard();
+    return RegionObserver.super.preMemStoreCompactionCompact(c, store, 
scanner);
+  }
+
+  @Override
+  public void preCompactSelection(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Store store, List<? extends StoreFile> candidates, 
CompactionLifeCycleTracker tracker)
+    throws IOException {
+    internalReadOnlyGuard();
+    RegionObserver.super.preCompactSelection(c, store, candidates, tracker);
+  }
+
+  @Override
+  public void preCompactScannerOpen(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Store store, ScanType scanType, ScanOptions options, 
CompactionLifeCycleTracker tracker,
+    CompactionRequest request) throws IOException {
+    internalReadOnlyGuard();
+    RegionObserver.super.preCompactScannerOpen(c, store, scanType, options, 
tracker, request);
+  }
+
+  @Override
+  public InternalScanner preCompact(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Store store, InternalScanner scanner, ScanType scanType, 
CompactionLifeCycleTracker tracker,
+    CompactionRequest request) throws IOException {
+    internalReadOnlyGuard();
+    return RegionObserver.super.preCompact(c, store, scanner, scanType, 
tracker, request);
+  }
+
   @Override
   public void prePut(ObserverContext<? extends RegionCoprocessorEnvironment> 
c, Put put,
-    WALEdit edit) throws IOException {
+    WALEdit edit, Durability durability) throws IOException {
     TableName tableName = c.getEnvironment().getRegionInfo().getTable();
     if (tableName.isSystemTable()) {
       return;
     }
     internalReadOnlyGuard();
+    RegionObserver.super.prePut(c, put, edit, durability);
   }
 
   @Override
-  public void preDelete(ObserverContext<? extends 
RegionCoprocessorEnvironment> c, Delete delete,
+  public void prePut(ObserverContext<? extends RegionCoprocessorEnvironment> 
c, Put put,
     WALEdit edit) throws IOException {
-    if (c.getEnvironment().getRegionInfo().getTable().isSystemTable()) {
+    TableName tableName = c.getEnvironment().getRegionInfo().getTable();
+    if (tableName.isSystemTable()) {
       return;
     }
     internalReadOnlyGuard();
+    RegionObserver.super.prePut(c, put, edit);
   }
 
   @Override
-  public void preBatchMutate(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
-    MiniBatchOperationInProgress<Mutation> miniBatchOp) throws IOException {
-    TableName tableName = c.getEnvironment().getRegionInfo().getTable();
-    if (tableName.isSystemTable()) {
+  public void preDelete(ObserverContext<? extends 
RegionCoprocessorEnvironment> c, Delete delete,
+    WALEdit edit, Durability durability) throws IOException {
+    if (c.getEnvironment().getRegionInfo().getTable().isSystemTable()) {
       return;
     }
     internalReadOnlyGuard();
+    RegionObserver.super.preDelete(c, delete, edit, durability);
   }
 
   @Override
-  public void preFlush(final ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
-    FlushLifeCycleTracker tracker) throws IOException {
+  public void preDelete(ObserverContext<? extends 
RegionCoprocessorEnvironment> c, Delete delete,
+    WALEdit edit) throws IOException {
+    if (c.getEnvironment().getRegionInfo().getTable().isSystemTable()) {
+      return;
+    }
     internalReadOnlyGuard();
+    RegionObserver.super.preDelete(c, delete, edit);
   }
 
   @Override
-  public void preCompactSelection(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
-    Store store, List<? extends StoreFile> candidates, 
CompactionLifeCycleTracker tracker)
-    throws IOException {
+  public void preBatchMutate(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    MiniBatchOperationInProgress<Mutation> miniBatchOp) throws IOException {
+    TableName tableName = c.getEnvironment().getRegionInfo().getTable();
+    if (tableName.isSystemTable()) {
+      return;
+    }
     internalReadOnlyGuard();
+    RegionObserver.super.preBatchMutate(c, miniBatchOp);
   }
 
   @Override
@@ -231,6 +326,22 @@ public class ReadOnlyController implements 
MasterCoprocessor, RegionCoprocessor,
     return RegionObserver.super.preCheckAndDeleteAfterRowLock(c, row, filter, 
delete, result);
   }
 
+  @Override
+  public CheckAndMutateResult preCheckAndMutate(
+    ObserverContext<? extends RegionCoprocessorEnvironment> c, CheckAndMutate 
checkAndMutate,
+    CheckAndMutateResult result) throws IOException {
+    internalReadOnlyGuard();
+    return RegionObserver.super.preCheckAndMutate(c, checkAndMutate, result);
+  }
+
+  @Override
+  public CheckAndMutateResult preCheckAndMutateAfterRowLock(
+    ObserverContext<? extends RegionCoprocessorEnvironment> c, CheckAndMutate 
checkAndMutate,
+    CheckAndMutateResult result) throws IOException {
+    internalReadOnlyGuard();
+    return RegionObserver.super.preCheckAndMutateAfterRowLock(c, 
checkAndMutate, result);
+  }
+
   @Override
   public Result preAppend(ObserverContext<? extends 
RegionCoprocessorEnvironment> c, Append append)
     throws IOException {
@@ -252,6 +363,34 @@ public class ReadOnlyController implements 
MasterCoprocessor, RegionCoprocessor,
     return RegionObserver.super.preAppendAfterRowLock(c, append);
   }
 
+  @Override
+  public Result preIncrement(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Increment increment) throws IOException {
+    internalReadOnlyGuard();
+    return RegionObserver.super.preIncrement(c, increment);
+  }
+
+  @Override
+  public Result preIncrement(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Increment increment, WALEdit edit) throws IOException {
+    internalReadOnlyGuard();
+    return RegionObserver.super.preIncrement(c, increment, edit);
+  }
+
+  @Override
+  public Result preIncrementAfterRowLock(ObserverContext<? extends 
RegionCoprocessorEnvironment> c,
+    Increment increment) throws IOException {
+    internalReadOnlyGuard();
+    return RegionObserver.super.preIncrementAfterRowLock(c, increment);
+  }
+
+  @Override
+  public void preReplayWALs(ObserverContext<? extends 
RegionCoprocessorEnvironment> ctx,
+    RegionInfo info, Path edits) throws IOException {
+    internalReadOnlyGuard();
+    RegionObserver.super.preReplayWALs(ctx, info, edits);
+  }
+
   @Override
   public void preBulkLoadHFile(ObserverContext<? extends 
RegionCoprocessorEnvironment> ctx,
     List<Pair<byte[], String>> familyPaths) throws IOException {
@@ -259,12 +398,35 @@ public class ReadOnlyController implements 
MasterCoprocessor, RegionCoprocessor,
     RegionObserver.super.preBulkLoadHFile(ctx, familyPaths);
   }
 
+  @Override
+  public void preCommitStoreFile(ObserverContext<? extends 
RegionCoprocessorEnvironment> ctx,
+    byte[] family, List<Pair<Path, Path>> pairs) throws IOException {
+    internalReadOnlyGuard();
+    RegionObserver.super.preCommitStoreFile(ctx, family, pairs);
+  }
+
+  @Override
+  public void preWALAppend(ObserverContext<? extends 
RegionCoprocessorEnvironment> ctx, WALKey key,
+    WALEdit edit) throws IOException {
+    if (!key.getTableName().isSystemTable()) {
+      internalReadOnlyGuard();
+    }
+    RegionObserver.super.preWALAppend(ctx, key, edit);
+  }
+
   /* ---- MasterObserver Overrides ---- */
   @Override
   public Optional<MasterObserver> getMasterObserver() {
     return Optional.of(this);
   }
 
+  @Override
+  public TableDescriptor preCreateTableRegionsInfos(
+    ObserverContext<MasterCoprocessorEnvironment> ctx, TableDescriptor desc) 
throws IOException {
+    internalReadOnlyGuard();
+    return MasterObserver.super.preCreateTableRegionsInfos(ctx, desc);
+  }
+
   @Override
   public void preCreateTable(ObserverContext<MasterCoprocessorEnvironment> ctx,
     TableDescriptor desc, RegionInfo[] regions) throws IOException {
@@ -272,6 +434,13 @@ public class ReadOnlyController implements 
MasterCoprocessor, RegionCoprocessor,
     MasterObserver.super.preCreateTable(ctx, desc, regions);
   }
 
+  @Override
+  public void 
preCreateTableAction(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    TableDescriptor desc, RegionInfo[] regions) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preCreateTableAction(ctx, desc, regions);
+  }
+
   @Override
   public void preDeleteTable(ObserverContext<MasterCoprocessorEnvironment> 
ctx, TableName tableName)
     throws IOException {
@@ -308,6 +477,96 @@ public class ReadOnlyController implements 
MasterCoprocessor, RegionCoprocessor,
     return MasterObserver.super.preModifyTable(ctx, tableName, 
currentDescriptor, newDescriptor);
   }
 
+  @Override
+  public String 
preModifyTableStoreFileTracker(ObserverContext<MasterCoprocessorEnvironment> 
ctx,
+    TableName tableName, String dstSFT) throws IOException {
+    internalReadOnlyGuard();
+    return MasterObserver.super.preModifyTableStoreFileTracker(ctx, tableName, 
dstSFT);
+  }
+
+  @Override
+  public String preModifyColumnFamilyStoreFileTracker(
+    ObserverContext<MasterCoprocessorEnvironment> ctx, TableName tableName, 
byte[] family,
+    String dstSFT) throws IOException {
+    internalReadOnlyGuard();
+    return MasterObserver.super.preModifyColumnFamilyStoreFileTracker(ctx, 
tableName, family,
+      dstSFT);
+  }
+
+  @Override
+  public void 
preModifyTableAction(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    TableName tableName, TableDescriptor currentDescriptor, TableDescriptor 
newDescriptor)
+    throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preModifyTableAction(ctx, tableName, 
currentDescriptor, newDescriptor);
+  }
+
+  @Override
+  public void preSplitRegion(ObserverContext<MasterCoprocessorEnvironment> c, 
TableName tableName,
+    byte[] splitRow) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSplitRegion(c, tableName, splitRow);
+  }
+
+  @Override
+  public void 
preSplitRegionAction(ObserverContext<MasterCoprocessorEnvironment> c,
+    TableName tableName, byte[] splitRow) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSplitRegionAction(c, tableName, splitRow);
+  }
+
+  @Override
+  public void 
preSplitRegionBeforeMETAAction(ObserverContext<MasterCoprocessorEnvironment> 
ctx,
+    byte[] splitKey, List<Mutation> metaEntries) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSplitRegionBeforeMETAAction(ctx, splitKey, 
metaEntries);
+  }
+
+  @Override
+  public void 
preSplitRegionAfterMETAAction(ObserverContext<MasterCoprocessorEnvironment> ctx)
+    throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSplitRegionAfterMETAAction(ctx);
+  }
+
+  @Override
+  public void preTruncateRegion(ObserverContext<MasterCoprocessorEnvironment> 
c,
+    RegionInfo regionInfo) {
+    try {
+      internalReadOnlyGuard();
+    } catch (IOException e) {
+      LOG.info("Region truncation of region {} not allowed in read-only mode",
+        regionInfo.getRegionNameAsString());
+    }
+    MasterObserver.super.preTruncateRegion(c, regionInfo);
+  }
+
+  @Override
+  public void 
preTruncateRegionAction(ObserverContext<MasterCoprocessorEnvironment> c,
+    RegionInfo regionInfo) {
+    try {
+      internalReadOnlyGuard();
+    } catch (IOException e) {
+      LOG.info("Region truncation of region {} not allowed in read-only mode",
+        regionInfo.getRegionNameAsString());
+    }
+    MasterObserver.super.preTruncateRegionAction(c, regionInfo);
+  }
+
+  @Override
+  public void preMergeRegionsAction(final 
ObserverContext<MasterCoprocessorEnvironment> ctx,
+    final RegionInfo[] regionsToMerge) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preMergeRegionsAction(ctx, regionsToMerge);
+  }
+
+  @Override
+  public void 
preMergeRegionsCommitAction(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    RegionInfo[] regionsToMerge, List<Mutation> metaEntries) throws 
IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preMergeRegionsCommitAction(ctx, regionsToMerge, 
metaEntries);
+  }
+
   @Override
   public void preSnapshot(ObserverContext<MasterCoprocessorEnvironment> ctx,
     SnapshotDescription snapshot, TableDescriptor tableDescriptor) throws 
IOException {
@@ -359,25 +618,187 @@ public class ReadOnlyController implements 
MasterCoprocessor, RegionCoprocessor,
   }
 
   @Override
-  public void 
preMergeRegionsAction(ObserverContext<MasterCoprocessorEnvironment> ctx,
-    RegionInfo[] regionsToMerge) throws IOException {
+  public void 
preMasterStoreFlush(ObserverContext<MasterCoprocessorEnvironment> ctx)
+    throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preMasterStoreFlush(ctx);
+  }
+
+  @Override
+  public void preSetUserQuota(ObserverContext<MasterCoprocessorEnvironment> 
ctx, String userName,
+    GlobalQuotaSettings quotas) throws IOException {
     internalReadOnlyGuard();
-    MasterObserver.super.preMergeRegionsAction(ctx, regionsToMerge);
+    MasterObserver.super.preSetUserQuota(ctx, userName, quotas);
   }
 
-  /* ---- RegionServerObserver Overrides ---- */
   @Override
-  public void 
preRollWALWriterRequest(ObserverContext<RegionServerCoprocessorEnvironment> ctx)
+  public void preSetUserQuota(ObserverContext<MasterCoprocessorEnvironment> 
ctx, String userName,
+    TableName tableName, GlobalQuotaSettings quotas) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSetUserQuota(ctx, userName, tableName, quotas);
+  }
+
+  @Override
+  public void preSetUserQuota(ObserverContext<MasterCoprocessorEnvironment> 
ctx, String userName,
+    String namespace, GlobalQuotaSettings quotas) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSetUserQuota(ctx, userName, namespace, quotas);
+  }
+
+  @Override
+  public void preSetTableQuota(ObserverContext<MasterCoprocessorEnvironment> 
ctx,
+    TableName tableName, GlobalQuotaSettings quotas) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSetTableQuota(ctx, tableName, quotas);
+  }
+
+  @Override
+  public void 
preSetNamespaceQuota(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    String namespace, GlobalQuotaSettings quotas) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSetNamespaceQuota(ctx, namespace, quotas);
+  }
+
+  @Override
+  public void 
preSetRegionServerQuota(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    String regionServer, GlobalQuotaSettings quotas) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preSetRegionServerQuota(ctx, regionServer, quotas);
+  }
+
+  @Override
+  public void preMergeRegions(final 
ObserverContext<MasterCoprocessorEnvironment> ctx,
+    final RegionInfo[] regionsToMerge) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preMergeRegions(ctx, regionsToMerge);
+  }
+
+  @Override
+  public void 
preMoveServersAndTables(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    Set<Address> servers, Set<TableName> tables, String targetGroup) throws 
IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preMoveServersAndTables(ctx, servers, tables, 
targetGroup);
+  }
+
+  @Override
+  public void preMoveServers(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    Set<Address> servers, String targetGroup) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preMoveServers(ctx, servers, targetGroup);
+  }
+
+  @Override
+  public void preMoveTables(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    Set<TableName> tables, String targetGroup) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preMoveTables(ctx, tables, targetGroup);
+  }
+
+  @Override
+  public void preAddRSGroup(ObserverContext<MasterCoprocessorEnvironment> ctx, 
String name)
     throws IOException {
     internalReadOnlyGuard();
-    RegionServerObserver.super.preRollWALWriterRequest(ctx);
+    MasterObserver.super.preAddRSGroup(ctx, name);
   }
 
   @Override
-  public void 
preClearCompactionQueues(ObserverContext<RegionServerCoprocessorEnvironment> 
ctx)
+  public void preRemoveRSGroup(ObserverContext<MasterCoprocessorEnvironment> 
ctx, String name)
     throws IOException {
     internalReadOnlyGuard();
-    RegionServerObserver.super.preClearCompactionQueues(ctx);
+    MasterObserver.super.preRemoveRSGroup(ctx, name);
+  }
+
+  @Override
+  public void preBalanceRSGroup(ObserverContext<MasterCoprocessorEnvironment> 
ctx, String groupName,
+    BalanceRequest request) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preBalanceRSGroup(ctx, groupName, request);
+  }
+
+  @Override
+  public void preRemoveServers(ObserverContext<MasterCoprocessorEnvironment> 
ctx,
+    Set<Address> servers) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preRemoveServers(ctx, servers);
+  }
+
+  @Override
+  public void preRenameRSGroup(ObserverContext<MasterCoprocessorEnvironment> 
ctx, String oldName,
+    String newName) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preRenameRSGroup(ctx, oldName, newName);
+  }
+
+  @Override
+  public void 
preUpdateRSGroupConfig(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    String groupName, Map<String, String> configuration) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preUpdateRSGroupConfig(ctx, groupName, configuration);
+  }
+
+  @Override
+  public void 
preAddReplicationPeer(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    String peerId, ReplicationPeerConfig peerConfig) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preAddReplicationPeer(ctx, peerId, peerConfig);
+  }
+
+  @Override
+  public void 
preRemoveReplicationPeer(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    String peerId) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preRemoveReplicationPeer(ctx, peerId);
+  }
+
+  @Override
+  public void 
preEnableReplicationPeer(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    String peerId) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preEnableReplicationPeer(ctx, peerId);
+  }
+
+  @Override
+  public void 
preDisableReplicationPeer(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    String peerId) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preDisableReplicationPeer(ctx, peerId);
+  }
+
+  @Override
+  public void 
preUpdateReplicationPeerConfig(ObserverContext<MasterCoprocessorEnvironment> 
ctx,
+    String peerId, ReplicationPeerConfig peerConfig) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preUpdateReplicationPeerConfig(ctx, peerId, 
peerConfig);
+  }
+
+  @Override
+  public void preTransitReplicationPeerSyncReplicationState(
+    ObserverContext<MasterCoprocessorEnvironment> ctx, String peerId, 
SyncReplicationState state)
+    throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preTransitReplicationPeerSyncReplicationState(ctx, 
peerId, state);
+  }
+
+  @Override
+  public void preGrant(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    UserPermission userPermission, boolean mergeExistingPermissions) throws 
IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preGrant(ctx, userPermission, 
mergeExistingPermissions);
+  }
+
+  @Override
+  public void preRevoke(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    UserPermission userPermission) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preRevoke(ctx, userPermission);
+  }
+
+  /* ---- RegionServerObserver Overrides ---- */
+  @Override
+  public void 
preRollWALWriterRequest(ObserverContext<RegionServerCoprocessorEnvironment> ctx)
+    throws IOException {
+    internalReadOnlyGuard();
+    RegionServerObserver.super.preRollWALWriterRequest(ctx);
   }
 
   @Override
@@ -395,10 +816,10 @@ public class ReadOnlyController implements 
MasterCoprocessor, RegionCoprocessor,
   }
 
   @Override
-  public void 
preClearRegionBlockCache(ObserverContext<RegionServerCoprocessorEnvironment> 
ctx)
+  public void 
preReplicateLogEntries(ObserverContext<RegionServerCoprocessorEnvironment> ctx)
     throws IOException {
     internalReadOnlyGuard();
-    RegionServerObserver.super.preClearRegionBlockCache(ctx);
+    RegionServerObserver.super.preReplicateLogEntries(ctx);
   }
 
   /* ---- EndpointObserver Overrides ---- */
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerBulkLoadObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerBulkLoadObserver.java
new file mode 100644
index 00000000000..c17b2134432
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerBulkLoadObserver.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+// Tests methods of BulkLoad Observer which are implemented in 
ReadOnlyController,
+// by mocking the coprocessor environment and dependencies
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestReadOnlyControllerBulkLoadObserver {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestReadOnlyControllerBulkLoadObserver.class);
+
+  ReadOnlyController readOnlyController;
+  HBaseConfiguration readOnlyConf;
+
+  // Region Server Coprocessor mocking variables
+  ObserverContext<RegionCoprocessorEnvironment> ctx;
+
+  @Before
+  public void setup() throws Exception {
+    readOnlyController = new ReadOnlyController();
+    readOnlyConf = new HBaseConfiguration();
+    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+
+    // mocking variables initialization
+    ctx = mock(ObserverContext.class);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+
+  }
+
+  @Test(expected = IOException.class)
+  public void testPrePrepareBulkLoadReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.prePrepareBulkLoad(ctx);
+  }
+
+  @Test
+  public void testPrePrepareBulkLoadNoException() throws IOException {
+    readOnlyController.prePrepareBulkLoad(ctx);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCleanupBulkLoadReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCleanupBulkLoad(ctx);
+  }
+
+  @Test
+  public void testPreCleanupBulkLoadNoException() throws IOException {
+    readOnlyController.preCleanupBulkLoad(ctx);
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerEndpointObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerEndpointObserver.java
new file mode 100644
index 00000000000..b3562d8d3aa
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerEndpointObserver.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.hbase.thirdparty.com.google.protobuf.Message;
+import org.apache.hbase.thirdparty.com.google.protobuf.Service;
+
+// Tests methods of Endpoint Observer which are implemented in 
ReadOnlyController,
+// by mocking the coprocessor environment and dependencies.
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestReadOnlyControllerEndpointObserver {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestReadOnlyControllerEndpointObserver.class);
+
+  ReadOnlyController readOnlyController;
+  HBaseConfiguration readOnlyConf;
+
+  // Region Server Coprocessor mocking variables
+  ObserverContext<? extends RegionCoprocessorEnvironment> ctx;
+  Service service;
+  String methodName;
+  Message request;
+
+  @Before
+  public void setup() throws Exception {
+    readOnlyController = new ReadOnlyController();
+    readOnlyConf = new HBaseConfiguration();
+    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+
+    // mocking variables initialization
+    ctx = mock(ObserverContext.class);
+    service = mock(Service.class);
+    methodName = "testMethod";
+    request = mock(Message.class);
+
+    // Linking the mocks
+  }
+
+  @After
+  public void tearDown() throws Exception {
+
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreEndpointInvocationReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preEndpointInvocation(ctx, service, methodName, 
request);
+  }
+
+  @Test
+  public void testPreEndpointInvocationNoException() throws IOException {
+    readOnlyController.preEndpointInvocation(ctx, service, methodName, 
request);
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerMasterObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerMasterObserver.java
new file mode 100644
index 00000000000..02fc9c77d5f
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerMasterObserver.java
@@ -0,0 +1,691 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.NamespaceDescriptor;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.BalanceRequest;
+import org.apache.hadoop.hbase.client.Mutation;
+import org.apache.hadoop.hbase.client.RegionInfo;
+import org.apache.hadoop.hbase.client.SnapshotDescription;
+import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.net.Address;
+import org.apache.hadoop.hbase.quotas.GlobalQuotaSettings;
+import org.apache.hadoop.hbase.replication.ReplicationPeerConfig;
+import org.apache.hadoop.hbase.replication.SyncReplicationState;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+// Tests methods of Master Observer which are implemented in 
ReadOnlyController,
+// by mocking the coprocessor environment and dependencies
+
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestReadOnlyControllerMasterObserver {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestReadOnlyControllerMasterObserver.class);
+
+  ReadOnlyController readOnlyController;
+  HBaseConfiguration readOnlyConf;
+
+  // Master Coprocessor mocking variables
+  ObserverContext<MasterCoprocessorEnvironment> c, ctx;
+  TableDescriptor desc;
+  RegionInfo[] regions;
+  TableName tableName;
+  TableDescriptor currentDescriptor, newDescriptor;
+  String dstSFT;
+  byte[] family;
+  byte[] splitRow;
+  byte[] splitKey;
+  List<Mutation> metaEntries;
+  RegionInfo[] regionsToMerge;
+  SnapshotDescription snapshot;
+  TableDescriptor tableDescriptor;
+  NamespaceDescriptor ns;
+  NamespaceDescriptor currentNsDescriptor, newNsDescriptor;
+  String namespace;
+  String userName;
+  GlobalQuotaSettings quotas;
+  String regionServer;
+  Set<Address> servers;
+  Set<TableName> tables;
+  String targetGroup;
+  String name;
+  String groupName;
+  BalanceRequest request;
+  String oldName, newName;
+  Map<String, String> configuration;
+  String peerId;
+  ReplicationPeerConfig peerConfig;
+  SyncReplicationState state;
+  UserPermission userPermission;
+  boolean mergeExistingPermissions;
+
+  @Before
+  public void setup() throws Exception {
+    readOnlyController = new ReadOnlyController();
+    readOnlyConf = new HBaseConfiguration();
+    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+
+    // mocking variables initialization
+    c = mock(ObserverContext.class);
+    // ctx is created to make naming variable in sync with the Observer 
interface
+    // methods where 'ctx' is used as the ObserverContext variable name 
instead of 'c'.
+    // otherwise both are one and the same
+    ctx = c;
+    desc = mock(TableDescriptor.class);
+    regions = new RegionInfo[] {};
+    tableName = TableName.valueOf("testTable");
+    currentDescriptor = mock(TableDescriptor.class);
+    newDescriptor = mock(TableDescriptor.class);
+    dstSFT = "dstSFT";
+    family = Bytes.toBytes("testFamily");
+    splitRow = Bytes.toBytes("splitRow");
+    splitKey = Bytes.toBytes("splitKey");
+    metaEntries = List.of();
+    regionsToMerge = new RegionInfo[] {};
+    snapshot = mock(SnapshotDescription.class);
+    tableDescriptor = mock(TableDescriptor.class);
+    ns = mock(NamespaceDescriptor.class);
+    currentNsDescriptor = mock(NamespaceDescriptor.class);
+    newNsDescriptor = mock(NamespaceDescriptor.class);
+    namespace = "testNamespace";
+    userName = "testUser";
+    quotas = mock(GlobalQuotaSettings.class);
+    regionServer = "testRegionServer";
+    servers = Set.of();
+    tables = Set.of();
+    targetGroup = "targetGroup";
+    name = "testRSGroup";
+    groupName = "testGroupName";
+    request = BalanceRequest.newBuilder().build();
+    oldName = "oldRSGroupName";
+    newName = "newRSGroupName";
+    configuration = Map.of();
+    peerId = "testPeerId";
+    peerConfig = mock(ReplicationPeerConfig.class);
+    state = SyncReplicationState.NONE;
+    userPermission = mock(UserPermission.class);
+    mergeExistingPermissions = false;
+
+    // Linking the mocks:
+  }
+
+  @After
+  public void tearDown() throws Exception {
+
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCreateTableRegionsInfosReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCreateTableRegionsInfos(ctx, desc);
+  }
+
+  @Test
+  public void testPreCreateTableRegionsInfosNoException() throws IOException {
+    readOnlyController.preCreateTableRegionsInfos(ctx, desc);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCreateTableReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCreateTable(ctx, desc, regions);
+  }
+
+  @Test
+  public void testPreCreateTableNoException() throws IOException {
+    readOnlyController.preCreateTable(ctx, desc, regions);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCreateTableActionReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCreateTableAction(ctx, desc, regions);
+  }
+
+  @Test
+  public void testPreCreateTableActionNoException() throws IOException {
+    readOnlyController.preCreateTableAction(ctx, desc, regions);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreDeleteTableReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preDeleteTable(ctx, tableName);
+  }
+
+  @Test
+  public void testPreDeleteTableNoException() throws IOException {
+    readOnlyController.preDeleteTable(ctx, tableName);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreDeleteTableActionReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preDeleteTableAction(ctx, tableName);
+  }
+
+  @Test
+  public void testPreDeleteTableActionNoException() throws IOException {
+    readOnlyController.preDeleteTableAction(ctx, tableName);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreTruncateTableReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preTruncateTable(ctx, tableName);
+  }
+
+  @Test
+  public void testPreTruncateTableNoException() throws IOException {
+    readOnlyController.preTruncateTable(ctx, tableName);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreTruncateTableActionReadOnlyException() throws IOException 
{
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preTruncateTableAction(ctx, tableName);
+  }
+
+  @Test
+  public void testPreTruncateTableActionNoException() throws IOException {
+    readOnlyController.preTruncateTableAction(ctx, tableName);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreModifyTableReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preModifyTable(ctx, tableName, currentDescriptor, 
newDescriptor);
+  }
+
+  @Test
+  public void testPreModifyTableNoException() throws IOException {
+    readOnlyController.preModifyTable(ctx, tableName, currentDescriptor, 
newDescriptor);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreModifyTableStoreFileTrackerReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preModifyTableStoreFileTracker(ctx, tableName, dstSFT);
+  }
+
+  @Test
+  public void testPreModifyTableStoreFileTrackerNoException() throws 
IOException {
+    readOnlyController.preModifyTableStoreFileTracker(ctx, tableName, dstSFT);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreModifyColumnFamilyStoreFileTrackerReadOnlyException() 
throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preModifyColumnFamilyStoreFileTracker(ctx, tableName, 
family, dstSFT);
+  }
+
+  @Test
+  public void testPreModifyColumnFamilyStoreFileTrackerNoException() throws 
IOException {
+    readOnlyController.preModifyColumnFamilyStoreFileTracker(ctx, tableName, 
family, dstSFT);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreModifyTableActionReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preModifyTableAction(ctx, tableName, currentDescriptor, 
newDescriptor);
+  }
+
+  @Test
+  public void testPreModifyTableActionNoException() throws IOException {
+    readOnlyController.preModifyTableAction(ctx, tableName, currentDescriptor, 
newDescriptor);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSplitRegionReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSplitRegion(c, tableName, splitRow);
+  }
+
+  @Test
+  public void testPreSplitRegionNoException() throws IOException {
+    readOnlyController.preSplitRegion(c, tableName, splitRow);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSplitRegionActionReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSplitRegionAction(c, tableName, splitRow);
+  }
+
+  @Test
+  public void testPreSplitRegionActionNoException() throws IOException {
+    readOnlyController.preSplitRegionAction(c, tableName, splitRow);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSplitRegionBeforeMETAActionReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSplitRegionBeforeMETAAction(ctx, splitKey, 
metaEntries);
+  }
+
+  @Test
+  public void testPreSplitRegionBeforeMETAActionNoException() throws 
IOException {
+    readOnlyController.preSplitRegionBeforeMETAAction(ctx, splitKey, 
metaEntries);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSplitRegionAfterMETAActionReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSplitRegionAfterMETAAction(ctx);
+  }
+
+  @Test
+  public void testPreSplitRegionAfterMETAActionNoException() throws 
IOException {
+    readOnlyController.preSplitRegionAfterMETAAction(ctx);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMergeRegionsActionReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMergeRegionsAction(ctx, regionsToMerge);
+  }
+
+  @Test
+  public void testPreMergeRegionsActionNoException() throws IOException {
+    readOnlyController.preMergeRegionsAction(ctx, regionsToMerge);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMergeRegionsCommitActionReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMergeRegionsCommitAction(ctx, regionsToMerge, 
metaEntries);
+  }
+
+  @Test
+  public void testPreMergeRegionsCommitActionNoException() throws IOException {
+    readOnlyController.preMergeRegionsCommitAction(ctx, regionsToMerge, 
metaEntries);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSnapshotReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSnapshot(ctx, snapshot, tableDescriptor);
+  }
+
+  @Test
+  public void testPreSnapshotNoException() throws IOException {
+    readOnlyController.preSnapshot(ctx, snapshot, tableDescriptor);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCloneSnapshotReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCloneSnapshot(ctx, snapshot, tableDescriptor);
+  }
+
+  @Test
+  public void testPreCloneSnapshotNoException() throws IOException {
+    readOnlyController.preCloneSnapshot(ctx, snapshot, tableDescriptor);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreRestoreSnapshotReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preRestoreSnapshot(ctx, snapshot, tableDescriptor);
+  }
+
+  @Test
+  public void testPreRestoreSnapshotNoException() throws IOException {
+    readOnlyController.preRestoreSnapshot(ctx, snapshot, tableDescriptor);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreDeleteSnapshotReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preDeleteSnapshot(ctx, snapshot);
+  }
+
+  @Test
+  public void testPreDeleteSnapshotNoException() throws IOException {
+    readOnlyController.preDeleteSnapshot(ctx, snapshot);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCreateNamespaceReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCreateNamespace(ctx, ns);
+  }
+
+  @Test
+  public void testPreCreateNamespaceNoException() throws IOException {
+    readOnlyController.preCreateNamespace(ctx, ns);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreModifyNamespaceReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preModifyNamespace(ctx, currentNsDescriptor, 
newNsDescriptor);
+  }
+
+  @Test
+  public void testPreModifyNamespaceNoException() throws IOException {
+    readOnlyController.preModifyNamespace(ctx, currentNsDescriptor, 
newNsDescriptor);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreDeleteNamespaceReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preDeleteNamespace(ctx, namespace);
+  }
+
+  @Test
+  public void testPreDeleteNamespaceNoException() throws IOException {
+    readOnlyController.preDeleteNamespace(ctx, namespace);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMasterStoreFlushReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMasterStoreFlush(ctx);
+  }
+
+  @Test
+  public void testPreMasterStoreFlushNoException() throws IOException {
+    readOnlyController.preMasterStoreFlush(ctx);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSetUserQuotaReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSetUserQuota(ctx, userName, quotas);
+  }
+
+  @Test
+  public void testPreSetUserQuotaNoException() throws IOException {
+    readOnlyController.preSetUserQuota(ctx, userName, quotas);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSetUserQuotaOnTableReadOnlyException() throws IOException 
{
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSetUserQuota(ctx, userName, tableName, quotas);
+  }
+
+  @Test
+  public void testPreSetUserQuotaOnTableNoException() throws IOException {
+    readOnlyController.preSetUserQuota(ctx, userName, tableName, quotas);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSetUserQuotaOnNamespaceReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSetUserQuota(ctx, userName, namespace, quotas);
+  }
+
+  @Test
+  public void testPreSetUserQuotaOnNamespaceNoException() throws IOException {
+    readOnlyController.preSetUserQuota(ctx, userName, namespace, quotas);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSetTableQuotaReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSetTableQuota(ctx, tableName, quotas);
+  }
+
+  @Test
+  public void testPreSetTableQuotaNoException() throws IOException {
+    readOnlyController.preSetTableQuota(ctx, tableName, quotas);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSetNamespaceQuotaReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSetNamespaceQuota(ctx, namespace, quotas);
+  }
+
+  @Test
+  public void testPreSetNamespaceQuotaNoException() throws IOException {
+    readOnlyController.preSetNamespaceQuota(ctx, namespace, quotas);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreSetRegionServerQuotaReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preSetRegionServerQuota(ctx, regionServer, quotas);
+  }
+
+  @Test
+  public void testPreSetRegionServerQuotaNoException() throws IOException {
+    readOnlyController.preSetRegionServerQuota(ctx, regionServer, quotas);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMergeRegionsReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMergeRegions(ctx, regionsToMerge);
+  }
+
+  @Test
+  public void testPreMergeRegionsNoException() throws IOException {
+    readOnlyController.preMergeRegions(ctx, regionsToMerge);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMoveServersAndTablesReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMoveServersAndTables(ctx, servers, tables, 
targetGroup);
+  }
+
+  @Test
+  public void testPreMoveServersAndTablesNoException() throws IOException {
+    readOnlyController.preMoveServersAndTables(ctx, servers, tables, 
targetGroup);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMoveServersReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMoveServers(ctx, servers, targetGroup);
+  }
+
+  @Test
+  public void testPreMoveServersNoException() throws IOException {
+    readOnlyController.preMoveServers(ctx, servers, targetGroup);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMoveTablesReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMoveTables(ctx, tables, targetGroup);
+  }
+
+  @Test
+  public void testPreMoveTablesNoException() throws IOException {
+    readOnlyController.preMoveTables(ctx, tables, targetGroup);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreAddRSGroupReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preAddRSGroup(ctx, name);
+  }
+
+  @Test
+  public void testPreAddRSGroupNoException() throws IOException {
+    readOnlyController.preAddRSGroup(ctx, name);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreRemoveRSGroupReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preRemoveRSGroup(ctx, name);
+  }
+
+  @Test
+  public void testPreRemoveRSGroupNoException() throws IOException {
+    readOnlyController.preRemoveRSGroup(ctx, name);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreBalanceRSGroupReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preBalanceRSGroup(ctx, groupName, request);
+  }
+
+  @Test
+  public void testPreBalanceRSGroupNoException() throws IOException {
+    readOnlyController.preBalanceRSGroup(ctx, groupName, request);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreRemoveServersReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preRemoveServers(ctx, servers);
+  }
+
+  @Test
+  public void testPreRemoveServersNoException() throws IOException {
+    readOnlyController.preRemoveServers(ctx, servers);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreRenameRSGroupReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preRenameRSGroup(ctx, oldName, newName);
+  }
+
+  @Test
+  public void testPreRenameRSGroupNoException() throws IOException {
+    readOnlyController.preRenameRSGroup(ctx, oldName, newName);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreUpdateRSGroupConfigReadOnlyException() throws IOException 
{
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preUpdateRSGroupConfig(ctx, groupName, configuration);
+  }
+
+  @Test
+  public void testPreUpdateRSGroupConfigNoException() throws IOException {
+    readOnlyController.preUpdateRSGroupConfig(ctx, groupName, configuration);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreAddReplicationPeerReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preAddReplicationPeer(ctx, peerId, peerConfig);
+  }
+
+  @Test
+  public void testPreAddReplicationPeerNoException() throws IOException {
+    readOnlyController.preAddReplicationPeer(ctx, peerId, peerConfig);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreRemoveReplicationPeerReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preRemoveReplicationPeer(ctx, peerId);
+  }
+
+  @Test
+  public void testPreRemoveReplicationPeerNoException() throws IOException {
+    readOnlyController.preRemoveReplicationPeer(ctx, peerId);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreEnableReplicationPeerReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preEnableReplicationPeer(ctx, peerId);
+  }
+
+  @Test
+  public void testPreEnableReplicationPeerNoException() throws IOException {
+    readOnlyController.preEnableReplicationPeer(ctx, peerId);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreDisableReplicationPeerReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preDisableReplicationPeer(ctx, peerId);
+  }
+
+  @Test
+  public void testPreDisableReplicationPeerNoException() throws IOException {
+    readOnlyController.preDisableReplicationPeer(ctx, peerId);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreUpdateReplicationPeerConfigReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preUpdateReplicationPeerConfig(ctx, peerId, peerConfig);
+  }
+
+  @Test
+  public void testPreUpdateReplicationPeerConfigNoException() throws 
IOException {
+    readOnlyController.preUpdateReplicationPeerConfig(ctx, peerId, peerConfig);
+  }
+
+  @Test(expected = IOException.class)
+  public void 
testPreTransitReplicationPeerSyncReplicationStateReadOnlyException()
+    throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preTransitReplicationPeerSyncReplicationState(ctx, 
peerId, state);
+  }
+
+  @Test
+  public void testPreTransitReplicationPeerSyncReplicationStateNoException() 
throws IOException {
+    readOnlyController.preTransitReplicationPeerSyncReplicationState(ctx, 
peerId, state);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreGrantReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preGrant(ctx, userPermission, mergeExistingPermissions);
+  }
+
+  @Test
+  public void testPreGrantNoException() throws IOException {
+    readOnlyController.preGrant(ctx, userPermission, mergeExistingPermissions);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreRevokeReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preRevoke(ctx, userPermission);
+  }
+
+  @Test
+  public void testPreRevokeNoException() throws IOException {
+    readOnlyController.preRevoke(ctx, userPermission);
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionObserver.java
new file mode 100644
index 00000000000..6bd02b62f58
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionObserver.java
@@ -0,0 +1,560 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.List;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.CompareOperator;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Append;
+import org.apache.hadoop.hbase.client.CheckAndMutate;
+import org.apache.hadoop.hbase.client.CheckAndMutateResult;
+import org.apache.hadoop.hbase.client.Delete;
+import org.apache.hadoop.hbase.client.Durability;
+import org.apache.hadoop.hbase.client.Increment;
+import org.apache.hadoop.hbase.client.Mutation;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.RegionInfo;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
+import org.apache.hadoop.hbase.filter.ByteArrayComparable;
+import org.apache.hadoop.hbase.filter.Filter;
+import org.apache.hadoop.hbase.regionserver.FlushLifeCycleTracker;
+import org.apache.hadoop.hbase.regionserver.InternalScanner;
+import org.apache.hadoop.hbase.regionserver.MiniBatchOperationInProgress;
+import org.apache.hadoop.hbase.regionserver.ScanOptions;
+import org.apache.hadoop.hbase.regionserver.ScanType;
+import org.apache.hadoop.hbase.regionserver.Store;
+import org.apache.hadoop.hbase.regionserver.StoreFile;
+import 
org.apache.hadoop.hbase.regionserver.compactions.CompactionLifeCycleTracker;
+import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequest;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.util.Pair;
+import org.apache.hadoop.hbase.wal.WALEdit;
+import org.apache.hadoop.hbase.wal.WALKey;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+// Tests methods of Region Observer interface which are implemented in 
ReadOnlyController,
+// by mocking the coprocessor environment and dependencies.
+// V1 and V2 means version 1 and version 2 of the coprocessor method signature.
+// For example, prePut has 2 versions:
+// V1: prePut(ObserverContext<RegionCoprocessorEnvironment> c, Put put, 
WALEdit edit)
+// V2: prePut(ObserverContext<RegionCoprocessorEnvironment> c, Put put, 
WALEdit edit, Durability durability)
+
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestReadOnlyControllerRegionObserver {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestReadOnlyControllerRegionObserver.class);
+
+  ReadOnlyController readOnlyController;
+  HBaseConfiguration readOnlyConf;
+
+  // Region Coprocessor mocking variables
+  ObserverContext<RegionCoprocessorEnvironment> c, ctx;
+  RegionCoprocessorEnvironment env;
+  RegionInfo regionInfo;
+  Store store;
+  InternalScanner scanner;
+  ScanOptions options;
+  FlushLifeCycleTracker flushLifeCycleTracker;
+  List<StoreFile> candidates;
+  CompactionLifeCycleTracker compactionLifeCycleTracker;
+  ScanType scanType;
+  CompactionRequest compactionRequest;
+  TableName tableName;
+  Put put;
+  WALEdit edit;
+  Durability durability;
+  Delete delete;
+  MiniBatchOperationInProgress<Mutation> miniBatchOp;
+  byte[] row;
+  byte[] family;
+  byte[] qualifier;
+  Filter filter;
+  CompareOperator op;
+  ByteArrayComparable comparator;
+  boolean result;
+  CheckAndMutate checkAndMutate;
+  CheckAndMutateResult checkAndMutateResult;
+  Append append;
+  Increment increment;
+  RegionInfo info;
+  Path edits;
+  List<Pair<byte[], String>> familyPaths;
+  List<Pair<Path, Path>> pairs;
+  WALKey key;
+
+  @Before
+  public void setup() throws Exception {
+    readOnlyController = new ReadOnlyController();
+    readOnlyConf = new HBaseConfiguration();
+    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+
+    // mocking variables initialization
+    c = mock(ObserverContext.class);
+    // ctx is created to make naming variable in sync with the Observer 
interface
+    // methods where 'ctx' is used as the ObserverContext variable name 
instead of 'c'.
+    // otherwise both are one and the same
+    ctx = c;
+    env = mock(RegionCoprocessorEnvironment.class);
+    regionInfo = mock(RegionInfo.class);
+    store = mock(Store.class);
+    scanner = mock(InternalScanner.class);
+    options = mock(ScanOptions.class);
+    flushLifeCycleTracker = mock(FlushLifeCycleTracker.class);
+    compactionLifeCycleTracker = mock(CompactionLifeCycleTracker.class);
+    StoreFile sf1 = mock(StoreFile.class);
+    StoreFile sf2 = mock(StoreFile.class);
+    candidates = List.of(sf1, sf2);
+    scanType = ScanType.COMPACT_DROP_DELETES;
+    compactionRequest = mock(CompactionRequest.class);
+    tableName = TableName.valueOf("testTable");
+    put = mock(Put.class);
+    edit = mock(WALEdit.class);
+    durability = Durability.USE_DEFAULT;
+    delete = mock(Delete.class);
+    miniBatchOp = mock(MiniBatchOperationInProgress.class);
+    row = Bytes.toBytes("test-row");
+    family = Bytes.toBytes("test-family");
+    qualifier = Bytes.toBytes("test-qualifier");
+    filter = mock(Filter.class);
+    op = CompareOperator.NO_OP;
+    comparator = mock(ByteArrayComparable.class);
+    result = false;
+    checkAndMutate = CheckAndMutate
+      
.newBuilder(Bytes.toBytes("test-row")).ifEquals(Bytes.toBytes("test-family"),
+        Bytes.toBytes("test-qualifier"), Bytes.toBytes("test-value"))
+      .build(new Put(Bytes.toBytes("test-row")));
+    checkAndMutateResult = mock(CheckAndMutateResult.class);
+    append = mock(Append.class);
+    increment = mock(Increment.class);
+    edits = mock(Path.class);
+    familyPaths = List.of(new Pair<>(Bytes.toBytes("test-family"), 
"/path/to/hfile1"),
+      new Pair<>(Bytes.toBytes("test-family"), "/path/to/hfile2"));
+    pairs = List.of(new Pair<>(mock(Path.class), mock(Path.class)),
+      new Pair<>(mock(Path.class), mock(Path.class)));
+    key = mock(WALKey.class);
+
+    // Linking the mocks:
+    when(c.getEnvironment()).thenReturn(env);
+    when(env.getRegionInfo()).thenReturn(regionInfo);
+    when(regionInfo.getTable()).thenReturn(tableName);
+    when(key.getTableName()).thenReturn(tableName);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreFlushV1ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preFlush(c, flushLifeCycleTracker);
+  }
+
+  @Test
+  public void testPreFlushV1NoException() throws IOException {
+    readOnlyController.preFlush(c, flushLifeCycleTracker);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreFlushV2ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preFlush(c, store, scanner, flushLifeCycleTracker);
+  }
+
+  @Test
+  public void testPreFlushV2NoException() throws IOException {
+    readOnlyController.preFlush(c, store, scanner, flushLifeCycleTracker);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreFlushScannerOpenReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preFlushScannerOpen(c, store, options, 
flushLifeCycleTracker);
+  }
+
+  @Test
+  public void testPreFlushScannerOpenNoException() throws IOException {
+    readOnlyController.preFlushScannerOpen(c, store, options, 
flushLifeCycleTracker);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMemStoreCompactionReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMemStoreCompaction(c, store);
+  }
+
+  @Test
+  public void testPreMemStoreCompactionNoException() throws IOException {
+    readOnlyController.preMemStoreCompaction(c, store);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMemStoreCompactionCompactScannerOpenReadOnlyException() 
throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMemStoreCompactionCompactScannerOpen(c, store, 
options);
+  }
+
+  @Test
+  public void testPreMemStoreCompactionCompactScannerOpenNoException() throws 
IOException {
+    readOnlyController.preMemStoreCompactionCompactScannerOpen(c, store, 
options);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreMemStoreCompactionCompactReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preMemStoreCompactionCompact(c, store, scanner);
+  }
+
+  @Test
+  public void testPreMemStoreCompactionCompactNoException() throws IOException 
{
+    readOnlyController.preMemStoreCompactionCompact(c, store, scanner);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCompactSelectionReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCompactSelection(c, store, candidates, 
compactionLifeCycleTracker);
+  }
+
+  @Test
+  public void testPreCompactSelectionNoException() throws IOException {
+    readOnlyController.preCompactSelection(c, store, candidates, 
compactionLifeCycleTracker);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCompactScannerOpenReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCompactScannerOpen(c, store, scanType, options,
+      compactionLifeCycleTracker, compactionRequest);
+  }
+
+  @Test
+  public void testPreCompactScannerOpenNoException() throws IOException {
+    readOnlyController.preCompactScannerOpen(c, store, scanType, options,
+      compactionLifeCycleTracker, compactionRequest);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCompactReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCompact(c, store, scanner, scanType, 
compactionLifeCycleTracker,
+      compactionRequest);
+  }
+
+  @Test
+  public void testPreCompactNoException() throws IOException {
+    readOnlyController.preCompact(c, store, scanner, scanType, 
compactionLifeCycleTracker,
+      compactionRequest);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPrePutV1ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.prePut(c, put, edit);
+  }
+
+  @Test
+  public void testPrePutV1NoException() throws IOException {
+    readOnlyController.prePut(c, put, edit);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPrePutV2ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.prePut(c, put, edit, durability);
+  }
+
+  @Test
+  public void testPrePutV2NoException() throws IOException {
+    readOnlyController.prePut(c, put, edit, durability);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreDeleteV1ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preDelete(c, delete, edit);
+  }
+
+  @Test
+  public void testPreDeleteV1NoException() throws IOException {
+    readOnlyController.preDelete(c, delete, edit);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreDeleteV2ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preDelete(c, delete, edit, durability);
+  }
+
+  @Test
+  public void testPreDeleteV2NoException() throws IOException {
+    readOnlyController.preDelete(c, delete, edit, durability);
+  }
+
+  @Test
+  public void testPreBatchMutateNoException() throws IOException {
+    readOnlyController.preBatchMutate(c, miniBatchOp);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndPutV1ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndPut(c, row, family, qualifier, op, 
comparator, put, result);
+  }
+
+  @Test
+  public void testPreCheckAndPutV1NoException() throws IOException {
+    readOnlyController.preCheckAndPut(c, row, family, qualifier, op, 
comparator, put, result);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndPutV2ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndPut(c, row, filter, put, result);
+  }
+
+  @Test
+  public void testPreCheckAndPutV2NoException() throws IOException {
+    readOnlyController.preCheckAndPut(c, row, filter, put, result);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndPutAfterRowLockV1ReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndPutAfterRowLock(c, row, family, qualifier, 
op, comparator, put,
+      result);
+  }
+
+  @Test
+  public void testPreCheckAndPutAfterRowLockV1NoException() throws IOException 
{
+    readOnlyController.preCheckAndPutAfterRowLock(c, row, family, qualifier, 
op, comparator, put,
+      result);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndPutAfterRowLockV2ReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndPutAfterRowLock(c, row, filter, put, result);
+  }
+
+  @Test
+  public void testPreCheckAndPutAfterRowLockV2NoException() throws IOException 
{
+    readOnlyController.preCheckAndPutAfterRowLock(c, row, filter, put, result);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndDeleteV1ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndDelete(c, row, family, qualifier, op, 
comparator, delete, result);
+  }
+
+  @Test
+  public void testPreCheckAndDeleteV1NoException() throws IOException {
+    readOnlyController.preCheckAndDelete(c, row, family, qualifier, op, 
comparator, delete, result);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndDeleteV2ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndDelete(c, row, filter, delete, result);
+  }
+
+  @Test
+  public void testPreCheckAndDeleteV2NoException() throws IOException {
+    readOnlyController.preCheckAndDelete(c, row, filter, delete, result);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndDeleteAfterRowLockV1ReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndDeleteAfterRowLock(c, row, family, 
qualifier, op, comparator,
+      delete, result);
+  }
+
+  @Test
+  public void testPreCheckAndDeleteAfterRowLockV1NoException() throws 
IOException {
+    readOnlyController.preCheckAndDeleteAfterRowLock(c, row, family, 
qualifier, op, comparator,
+      delete, result);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndDeleteAfterRowLockV2ReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndDeleteAfterRowLock(c, row, filter, delete, 
result);
+  }
+
+  @Test
+  public void testPreCheckAndDeleteAfterRowLockV2NoException() throws 
IOException {
+    readOnlyController.preCheckAndDeleteAfterRowLock(c, row, filter, delete, 
result);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreBatchMutateReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preBatchMutate(c, miniBatchOp);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndMutateReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndMutate(c, checkAndMutate, 
checkAndMutateResult);
+  }
+
+  @Test
+  public void testPreCheckAndMutateNoException() throws IOException {
+    readOnlyController.preCheckAndMutate(c, checkAndMutate, 
checkAndMutateResult);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCheckAndMutateAfterRowLockReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCheckAndMutateAfterRowLock(c, checkAndMutate, 
checkAndMutateResult);
+  }
+
+  @Test
+  public void testPreCheckAndMutateAfterRowLockNoException() throws 
IOException {
+    readOnlyController.preCheckAndMutateAfterRowLock(c, checkAndMutate, 
checkAndMutateResult);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreAppendV1ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preAppend(c, append);
+  }
+
+  @Test
+  public void testPreAppendV1NoException() throws IOException {
+    readOnlyController.preAppend(c, append);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreAppendV2ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preAppend(c, append, edit);
+  }
+
+  @Test
+  public void testPreAppendV2NoException() throws IOException {
+    readOnlyController.preAppend(c, append, edit);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreAppendAfterRowLockReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preAppendAfterRowLock(c, append);
+  }
+
+  @Test
+  public void testPreAppendAfterRowLockNoException() throws IOException {
+    readOnlyController.preAppendAfterRowLock(c, append);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreIncrementV1ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preIncrement(c, increment);
+  }
+
+  @Test
+  public void testPreIncrementV1NoException() throws IOException {
+    readOnlyController.preIncrement(c, increment);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreIncrementV2ReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preIncrement(c, increment, edit);
+  }
+
+  @Test
+  public void testPreIncrementV2NoException() throws IOException {
+    readOnlyController.preIncrement(c, increment, edit);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreIncrementAfterRowLockReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preIncrementAfterRowLock(c, increment);
+  }
+
+  @Test
+  public void testPreIncrementAfterRowLockNoException() throws IOException {
+    readOnlyController.preIncrementAfterRowLock(c, increment);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreReplayWALsReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preReplayWALs(ctx, info, edits);
+  }
+
+  @Test
+  public void testPreReplayWALsNoException() throws IOException {
+    readOnlyController.preReplayWALs(ctx, info, edits);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreBulkLoadHFileReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preBulkLoadHFile(ctx, familyPaths);
+  }
+
+  @Test
+  public void testPreBulkLoadHFileNoException() throws IOException {
+    readOnlyController.preBulkLoadHFile(ctx, familyPaths);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreCommitStoreFileReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preCommitStoreFile(ctx, family, pairs);
+  }
+
+  @Test
+  public void testPreCommitStoreFileNoException() throws IOException {
+    readOnlyController.preCommitStoreFile(ctx, family, pairs);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreWALAppendReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preWALAppend(ctx, key, edit);
+  }
+
+  @Test
+  public void testPreWALAppendNoException() throws IOException {
+    readOnlyController.preWALAppend(ctx, key, edit);
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionServerObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionServerObserver.java
new file mode 100644
index 00000000000..5d9d2561846
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionServerObserver.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.client.Mutation;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessorEnvironment;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.hbase.thirdparty.com.google.protobuf.ByteString;
+
+import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos;
+import org.apache.hadoop.hbase.shaded.protobuf.generated.WALProtos;
+
+// Tests methods of Region Server Observer which are implemented in 
ReadOnlyController,
+// by mocking the coprocessor environment and dependencies
+@Category({ SecurityTests.class, SmallTests.class })
+public class TestReadOnlyControllerRegionServerObserver {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    
HBaseClassTestRule.forClass(TestReadOnlyControllerRegionServerObserver.class);
+
+  ReadOnlyController readOnlyController;
+  HBaseConfiguration readOnlyConf;
+
+  // Region Server Coprocessor mocking variables
+  ObserverContext<RegionServerCoprocessorEnvironment> ctx;
+  AdminProtos.WALEntry walEntry;
+  Mutation mutation;
+
+  @Before
+  public void setup() throws Exception {
+    readOnlyController = new ReadOnlyController();
+    readOnlyConf = new HBaseConfiguration();
+    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+
+    // mocking variables initialization
+    ctx = mock(ObserverContext.class);
+    walEntry = AdminProtos.WALEntry.newBuilder()
+      
.setKey(WALProtos.WALKey.newBuilder().setTableName(ByteString.copyFromUtf8("test"))
+        
.setEncodedRegionName(ByteString.copyFromUtf8("regionA")).setLogSequenceNumber(100)
+        .setWriteTime(2).build())
+      .build();
+    mutation = mock(Mutation.class);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreRollWALWriterRequestReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preRollWALWriterRequest(ctx);
+  }
+
+  @Test
+  public void testPreRollWALWriterRequestNoException() throws IOException {
+    readOnlyController.preRollWALWriterRequest(ctx);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreExecuteProceduresReadOnlyException() throws IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preExecuteProcedures(ctx);
+  }
+
+  @Test
+  public void testPreExecuteProceduresNoException() throws IOException {
+    readOnlyController.preExecuteProcedures(ctx);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreReplicationSinkBatchMutateReadOnlyException() throws 
IOException {
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preReplicationSinkBatchMutate(ctx, walEntry, mutation);
+  }
+
+  @Test
+  public void testPreReplicationSinkBatchMutateNoException() throws 
IOException {
+    readOnlyController.preReplicationSinkBatchMutate(ctx, walEntry, mutation);
+  }
+
+  @Test(expected = IOException.class)
+  public void testPreReplicateLogEntriesReadOnlyException() throws IOException 
{
+    readOnlyController.onConfigurationChange(readOnlyConf);
+    readOnlyController.preReplicateLogEntries(ctx);
+  }
+
+  @Test
+  public void testPreReplicateLogEntriesNoException() throws IOException {
+    readOnlyController.preReplicateLogEntries(ctx);
+  }
+}

Reply via email to