This is an automated email from the ASF dual-hosted git repository.
Apache9 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/master by this push:
new fb4286d3046 HBASE-30337 Miscellaneous improvements on rpc checks
(#8560)
fb4286d3046 is described below
commit fb4286d3046ff525e8ffca8cb7fec1d2fb45b9f0
Author: Duo Zhang <[email protected]>
AuthorDate: Mon Aug 31 21:24:02 2026 +0800
HBASE-30337 Miscellaneous improvements on rpc checks (#8560)
Signed-off-by: Viraj Jasani <[email protected]>
Signed-off-by: Xiao Liu <[email protected]>
Reviewed-by: Aman Poonia <[email protected]>
---
.../hadoop/hbase/security/access/Permission.java | 5 +-
.../security/access/ShadedAccessControlUtil.java | 16 ++
.../apache/hadoop/hbase/HBaseRpcServicesBase.java | 55 -----
.../hadoop/hbase/coprocessor/MasterObserver.java | 52 ++++-
.../hadoop/hbase/master/MasterCoprocessorHost.java | 32 ++-
.../hadoop/hbase/master/MasterRpcServices.java | 17 +-
.../hadoop/hbase/regionserver/RSRpcServices.java | 56 ++++++
.../hbase/security/access/AccessController.java | 33 ++-
.../security/access/MasterReadOnlyController.java | 4 +-
.../security/access/TestAccessController.java | 62 ++++++
.../TestAccessControllerObserverCoverage.java | 222 +++++++++++++++++++++
11 files changed, 480 insertions(+), 74 deletions(-)
diff --git
a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/access/Permission.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/access/Permission.java
index ace6f302a19..78a53f725ec 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/access/Permission.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/access/Permission.java
@@ -26,6 +26,7 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import org.apache.hadoop.hbase.HBaseInterfaceAudience;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.VersionedWritable;
@@ -62,8 +63,8 @@ public class Permission extends VersionedWritable {
}
}
- @InterfaceAudience.Private
- protected enum Scope {
+ @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.COPROC)
+ public enum Scope {
GLOBAL('G'),
NAMESPACE('N'),
TABLE('T'),
diff --git
a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/access/ShadedAccessControlUtil.java
b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/access/ShadedAccessControlUtil.java
index cf7c797c98a..740c80dbceb 100644
---
a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/access/ShadedAccessControlUtil.java
+++
b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/access/ShadedAccessControlUtil.java
@@ -328,4 +328,20 @@ public class ShadedAccessControlUtil {
}
return builder.build();
}
+
+ public static Permission.Scope
toPermissionScope(AccessControlProtos.Permission.Type protoType) {
+ if (protoType == null) {
+ return null;
+ }
+ switch (protoType) {
+ case Global:
+ return Permission.Scope.GLOBAL;
+ case Namespace:
+ return Permission.Scope.NAMESPACE;
+ case Table:
+ return Permission.Scope.TABLE;
+ default:
+ return null;
+ }
+ }
}
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/HBaseRpcServicesBase.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/HBaseRpcServicesBase.java
index d6d27780883..13c06809eda 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/HBaseRpcServicesBase.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/HBaseRpcServicesBase.java
@@ -20,10 +20,8 @@ package org.apache.hadoop.hbase;
import com.google.errorprone.annotations.RestrictedApi;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.net.BindException;
import java.net.InetSocketAddress;
-import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -43,9 +41,6 @@ import org.apache.hadoop.hbase.ipc.RpcServerFactory;
import org.apache.hadoop.hbase.ipc.RpcServerInterface;
import org.apache.hadoop.hbase.namequeues.NamedQueuePayload;
import org.apache.hadoop.hbase.namequeues.NamedQueueRecorder;
-import org.apache.hadoop.hbase.namequeues.RpcLogDetails;
-import org.apache.hadoop.hbase.namequeues.request.NamedQueueGetRequest;
-import org.apache.hadoop.hbase.namequeues.response.NamedQueueGetResponse;
import org.apache.hadoop.hbase.net.Address;
import org.apache.hadoop.hbase.regionserver.RpcSchedulerFactory;
import org.apache.hadoop.hbase.security.User;
@@ -62,7 +57,6 @@ import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.hbase.thirdparty.com.google.protobuf.ByteString;
import org.apache.hbase.thirdparty.com.google.protobuf.Message;
import org.apache.hbase.thirdparty.com.google.protobuf.RpcController;
import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException;
@@ -71,11 +65,8 @@ import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.AdminService;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.ClearSlowLogResponseRequest;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.ClearSlowLogResponses;
-import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.SlowLogResponseRequest;
-import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.SlowLogResponses;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.UpdateConfigurationRequest;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.UpdateConfigurationResponse;
-import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.RPCProtos.RequestHeader;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.ClientMetaService;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.GetActiveMasterRequest;
@@ -89,7 +80,6 @@ import
org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.GetMaste
import
org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.GetMastersResponseEntry;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.GetMetaRegionLocationsRequest;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.GetMetaRegionLocationsResponse;
-import
org.apache.hadoop.hbase.shaded.protobuf.generated.TooSlowLog.SlowLogPayload;
/**
* Base class for Master and RegionServer RpcServices.
@@ -414,49 +404,4 @@ public abstract class HBaseRpcServicesBase<S extends
HBaseServerBase<?>>
ClearSlowLogResponses.newBuilder().setIsCleaned(slowLogsCleaned).build();
return clearSlowLogResponses;
}
-
- private List<SlowLogPayload> getSlowLogPayloads(SlowLogResponseRequest
request,
- NamedQueueRecorder namedQueueRecorder) {
- if (namedQueueRecorder == null) {
- return Collections.emptyList();
- }
- List<SlowLogPayload> slowLogPayloads;
- NamedQueueGetRequest namedQueueGetRequest = new NamedQueueGetRequest();
- namedQueueGetRequest.setNamedQueueEvent(RpcLogDetails.SLOW_LOG_EVENT);
- namedQueueGetRequest.setSlowLogResponseRequest(request);
- NamedQueueGetResponse namedQueueGetResponse =
- namedQueueRecorder.getNamedQueueRecords(namedQueueGetRequest);
- slowLogPayloads = namedQueueGetResponse != null
- ? namedQueueGetResponse.getSlowLogPayloads()
- : Collections.emptyList();
- return slowLogPayloads;
- }
-
- @Override
- @QosPriority(priority = HConstants.ADMIN_QOS)
- public HBaseProtos.LogEntry getLogEntries(RpcController controller,
- HBaseProtos.LogRequest request) throws ServiceException {
- try {
- final String logClassName = request.getLogClassName();
- Class<?> logClass =
Class.forName(logClassName).asSubclass(Message.class);
- Method method = logClass.getMethod("parseFrom", ByteString.class);
- if (logClassName.contains("SlowLogResponseRequest")) {
- SlowLogResponseRequest slowLogResponseRequest =
- (SlowLogResponseRequest) method.invoke(null,
request.getLogMessage());
- final NamedQueueRecorder namedQueueRecorder =
this.server.getNamedQueueRecorder();
- final List<SlowLogPayload> slowLogPayloads =
- getSlowLogPayloads(slowLogResponseRequest, namedQueueRecorder);
- SlowLogResponses slowLogResponses =
-
SlowLogResponses.newBuilder().addAllSlowLogPayloads(slowLogPayloads).build();
- return HBaseProtos.LogEntry.newBuilder()
- .setLogClassName(slowLogResponses.getClass().getName())
- .setLogMessage(slowLogResponses.toByteString()).build();
- }
- } catch (ClassNotFoundException | NoSuchMethodException |
IllegalAccessException
- | InvocationTargetException e) {
- LOG.error("Error while retrieving log entries.", e);
- throw new ServiceException(e);
- }
- throw new ServiceException("Invalid request params");
- }
}
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/coprocessor/MasterObserver.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/coprocessor/MasterObserver.java
index d0e451508b4..43233e8d969 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/coprocessor/MasterObserver.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/coprocessor/MasterObserver.java
@@ -593,7 +593,7 @@ public interface MasterObserver {
*/
@SuppressWarnings("unused")
default void preTruncateRegionAction(final
ObserverContext<MasterCoprocessorEnvironment> c,
- final RegionInfo regionInfo) {
+ final RegionInfo regionInfo) throws IOException {
}
/**
@@ -603,7 +603,7 @@ public interface MasterObserver {
*/
@SuppressWarnings("unused")
default void preTruncateRegion(final
ObserverContext<MasterCoprocessorEnvironment> c,
- RegionInfo regionInfo) {
+ RegionInfo regionInfo) throws IOException {
}
/**
@@ -613,7 +613,7 @@ public interface MasterObserver {
*/
@SuppressWarnings("unused")
default void postTruncateRegion(final
ObserverContext<MasterCoprocessorEnvironment> c,
- RegionInfo regionInfo) {
+ RegionInfo regionInfo) throws IOException {
}
/**
@@ -623,7 +623,7 @@ public interface MasterObserver {
*/
@SuppressWarnings("unused")
default void postTruncateRegionAction(final
ObserverContext<MasterCoprocessorEnvironment> c,
- final RegionInfo regionInfo) {
+ final RegionInfo regionInfo) throws IOException {
}
/**
@@ -1834,12 +1834,34 @@ public interface MasterObserver {
* @param family the table column family, null if don't get table family
permission
* @param qualifier the table column qualifier, null if don't get table
qualifier permission
* @throws IOException if something went wrong
+ * @deprecated Since 2.5.17, 2.6.8, 2.7.0, 3.0.1 and 3.1.0, will be removed
in 4.0.0. Use
+ * {@link #preGetUserPermissions(ObserverContext, String,
String, TableName, byte[], byte[], Permission.Scope)}
+ * instead.
*/
+ @Deprecated
default void
preGetUserPermissions(ObserverContext<MasterCoprocessorEnvironment> ctx,
String userName, String namespace, TableName tableName, byte[] family,
byte[] qualifier)
throws IOException {
}
+ /**
+ * Called before getting user permissions.
+ * @param ctx the coprocessor instance's environment
+ * @param userName the user name, null if get all user permissions
+ * @param namespace the namespace, null if don't get namespace
permission
+ * @param tableName the table name, null if don't get table permission
+ * @param family the table column family, null if don't get table
family permission
+ * @param qualifier the table column qualifier, null if don't get
table qualifier permission
+ * @param permissionScope the scope of permission being requested (GLOBAL,
NAMESPACE, or TABLE),
+ * may be null for backward compatibility
+ * @throws IOException if something went wrong
+ */
+ default void
preGetUserPermissions(ObserverContext<MasterCoprocessorEnvironment> ctx,
+ String userName, String namespace, TableName tableName, byte[] family,
byte[] qualifier,
+ Permission.Scope permissionScope) throws IOException {
+ preGetUserPermissions(ctx, userName, namespace, tableName, family,
qualifier);
+ }
+
/**
* Called after getting user permissions.
* @param ctx the coprocessor instance's environment
@@ -1849,12 +1871,34 @@ public interface MasterObserver {
* @param family the table column family, null if don't get table family
permission
* @param qualifier the table column qualifier, null if don't get table
qualifier permission
* @throws IOException if something went wrong
+ * @deprecated Since 2.5.17, 2.6.8, 2.7.0, 3.0.1 and 3.1.0, will be removed
in 4.0.0. Use
+ * {@link #postGetUserPermissions(ObserverContext, String,
String, TableName, byte[], byte[], Permission.Scope)}
+ * instead.
*/
+ @Deprecated
default void
postGetUserPermissions(ObserverContext<MasterCoprocessorEnvironment> ctx,
String userName, String namespace, TableName tableName, byte[] family,
byte[] qualifier)
throws IOException {
}
+ /**
+ * Called after getting user permissions.
+ * @param ctx the coprocessor instance's environment
+ * @param userName the user name, null if get all user permissions
+ * @param namespace the namespace, null if don't get namespace
permission
+ * @param tableName the table name, null if don't get table permission
+ * @param family the table column family, null if don't get table
family permission
+ * @param qualifier the table column qualifier, null if don't get
table qualifier permission
+ * @param permissionScope the scope of permission being requested (GLOBAL,
NAMESPACE, or TABLE),
+ * may be null for backward compatibility
+ * @throws IOException if something went wrong
+ */
+ default void
postGetUserPermissions(ObserverContext<MasterCoprocessorEnvironment> ctx,
+ String userName, String namespace, TableName tableName, byte[] family,
byte[] qualifier,
+ Permission.Scope permissionScope) throws IOException {
+ postGetUserPermissions(ctx, userName, namespace, tableName, family,
qualifier);
+ }
+
/*
* Called before checking if user has permissions.
* @param ctx the coprocessor instance's environment
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterCoprocessorHost.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterCoprocessorHost.java
index 00b3e714d1d..dc68dd9ae57 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterCoprocessorHost.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterCoprocessorHost.java
@@ -873,7 +873,7 @@ public class MasterCoprocessorHost
public void preTruncateRegion(RegionInfo regionInfo) throws IOException {
execOperation(coprocEnvironments.isEmpty() ? null : new
MasterObserverOperation() {
@Override
- public void call(MasterObserver observer) {
+ public void call(MasterObserver observer) throws IOException {
observer.preTruncateRegion(this, regionInfo);
}
});
@@ -886,7 +886,7 @@ public class MasterCoprocessorHost
public void postTruncateRegion(RegionInfo regionInfo) throws IOException {
execOperation(coprocEnvironments.isEmpty() ? null : new
MasterObserverOperation() {
@Override
- public void call(MasterObserver observer) {
+ public void call(MasterObserver observer) throws IOException {
observer.postTruncateRegion(this, regionInfo);
}
});
@@ -2085,22 +2085,46 @@ public class MasterCoprocessorHost
});
}
+ /**
+ * @deprecated Since 2.5.17, 2.6.8, 2.7.0, 3.0.1 and 3.1.0, will be removed
in 4.0.0. Use
+ * {@link #preGetUserPermissions(String, String, TableName,
byte[], byte[], Permission.Scope)}
+ * instead.
+ */
+ @Deprecated
public void preGetUserPermissions(String userName, String namespace,
TableName tableName,
byte[] family, byte[] qualifier) throws IOException {
+ preGetUserPermissions(userName, namespace, tableName, family, qualifier,
null);
+ }
+
+ public void preGetUserPermissions(String userName, String namespace,
TableName tableName,
+ byte[] family, byte[] qualifier, Permission.Scope permissionScope) throws
IOException {
execOperation(coprocEnvironments.isEmpty() ? null : new
MasterObserverOperation() {
@Override
public void call(MasterObserver observer) throws IOException {
- observer.preGetUserPermissions(this, userName, namespace, tableName,
family, qualifier);
+ observer.preGetUserPermissions(this, userName, namespace, tableName,
family, qualifier,
+ permissionScope);
}
});
}
+ /**
+ * @deprecated Since 2.5.17, 2.6.8, 2.7.0, 3.0.1 and 3.1.0, will be removed
in 4.0.0. Use
+ * {@link #postGetUserPermissions(String, String, TableName,
byte[], byte[], Permission.Scope)}
+ * instead.
+ */
+ @Deprecated
public void postGetUserPermissions(String userName, String namespace,
TableName tableName,
byte[] family, byte[] qualifier) throws IOException {
+ postGetUserPermissions(userName, namespace, tableName, family, qualifier,
null);
+ }
+
+ public void postGetUserPermissions(String userName, String namespace,
TableName tableName,
+ byte[] family, byte[] qualifier, Permission.Scope permissionScope) throws
IOException {
execOperation(coprocEnvironments.isEmpty() ? null : new
MasterObserverOperation() {
@Override
public void call(MasterObserver observer) throws IOException {
- observer.postGetUserPermissions(this, userName, namespace, tableName,
family, qualifier);
+ observer.postGetUserPermissions(this, userName, namespace, tableName,
family, qualifier,
+ permissionScope);
}
});
}
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java
index bb0e14a5189..924013d159d 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java
@@ -2770,6 +2770,7 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
@Override
public MasterProtos.AssignsResponse assigns(RpcController controller,
MasterProtos.AssignsRequest request) throws ServiceException {
+ rpcPreCheck("assigns");
checkMasterProcedureExecutor();
final ProcedureExecutor<MasterProcedureEnv> pe =
server.getMasterProcedureExecutor();
final AssignmentManager am = server.getAssignmentManager();
@@ -2798,6 +2799,7 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
@Override
public MasterProtos.UnassignsResponse unassigns(RpcController controller,
MasterProtos.UnassignsRequest request) throws ServiceException {
+ rpcPreCheck("unassigns");
checkMasterProcedureExecutor();
final ProcedureExecutor<MasterProcedureEnv> pe =
server.getMasterProcedureExecutor();
final AssignmentManager am = server.getAssignmentManager();
@@ -2830,6 +2832,7 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
@Override
public MasterProtos.BypassProcedureResponse bypassProcedure(RpcController
controller,
MasterProtos.BypassProcedureRequest request) throws ServiceException {
+ rpcPreCheck("bypassProcedure");
try {
LOG.info("{} bypass procedures={}, waitTime={}, override={},
recursive={}",
server.getClientIdAuditPrefix(), request.getProcIdList(),
request.getWaitTime(),
@@ -2847,6 +2850,7 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
public MasterProtos.ScheduleServerCrashProcedureResponse
scheduleServerCrashProcedure(
RpcController controller, MasterProtos.ScheduleServerCrashProcedureRequest
request)
throws ServiceException {
+ rpcPreCheck("scheduleServerCrashProcedure");
List<Long> pids = new ArrayList<>();
for (HBaseProtos.ServerName sn : request.getServerNameList()) {
ServerName serverName = ProtobufUtil.toServerName(sn);
@@ -2865,6 +2869,7 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
public MasterProtos.ScheduleSCPsForUnknownServersResponse
scheduleSCPsForUnknownServers(
RpcController controller,
MasterProtos.ScheduleSCPsForUnknownServersRequest request)
throws ServiceException {
+ rpcPreCheck("scheduleSCPsForUnknownServers");
List<Long> pids = new ArrayList<>();
final Set<ServerName> serverNames =
server.getAssignmentManager().getRegionStates()
.getRegionStates().stream().map(RegionState::getServerName).collect(Collectors.toSet());
@@ -2993,7 +2998,10 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
byte[] cq =
request.hasColumnQualifier() ?
request.getColumnQualifier().toByteArray() : null;
Type permissionType = request.hasType() ? request.getType() : null;
- server.getMasterCoprocessorHost().preGetUserPermissions(userName,
namespace, table, cf, cq);
+ Permission.Scope permissionScope =
+ ShadedAccessControlUtil.toPermissionScope(permissionType);
+ server.getMasterCoprocessorHost().preGetUserPermissions(userName,
namespace, table, cf, cq,
+ permissionScope);
List<UserPermission> perms = null;
if (permissionType == Type.Table) {
@@ -3018,8 +3026,8 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
}
}
- server.getMasterCoprocessorHost().postGetUserPermissions(userName,
namespace, table, cf,
- cq);
+ server.getMasterCoprocessorHost().postGetUserPermissions(userName,
namespace, table, cf, cq,
+ permissionScope);
AccessControlProtos.GetUserPermissionsResponse response =
ShadedAccessControlUtil.buildGetUserPermissionsResponse(perms);
return response;
@@ -3436,6 +3444,7 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
public HBaseProtos.LogEntry getLogEntries(RpcController controller,
HBaseProtos.LogRequest request) throws ServiceException {
try {
+ requirePermission("getLogEntries", Permission.Action.ADMIN);
final String logClassName = request.getLogClassName();
Class<?> logClass =
Class.forName(logClassName).asSubclass(Message.class);
Method method = logClass.getMethod("parseFrom", ByteString.class);
@@ -3457,7 +3466,7 @@ public class MasterRpcServices extends
HBaseRpcServicesBase<HMaster>
.setLogMessage(balancerRejectionsResponse.toByteString()).build();
}
} catch (ClassNotFoundException | NoSuchMethodException |
IllegalAccessException
- | InvocationTargetException e) {
+ | InvocationTargetException | IOException e) {
LOG.error("Error while retrieving log entries.", e);
throw new ServiceException(e);
}
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java
index 39968843c4a..c45e1a36faf 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java
@@ -21,6 +21,8 @@ import com.google.errorprone.annotations.RestrictedApi;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -104,6 +106,10 @@ import org.apache.hadoop.hbase.ipc.RpcServerInterface;
import org.apache.hadoop.hbase.ipc.ServerNotRunningYetException;
import org.apache.hadoop.hbase.ipc.ServerRpcController;
import org.apache.hadoop.hbase.monitoring.ThreadLocalServerSideScanMetrics;
+import org.apache.hadoop.hbase.namequeues.NamedQueueRecorder;
+import org.apache.hadoop.hbase.namequeues.RpcLogDetails;
+import org.apache.hadoop.hbase.namequeues.request.NamedQueueGetRequest;
+import org.apache.hadoop.hbase.namequeues.response.NamedQueueGetResponse;
import org.apache.hadoop.hbase.net.Address;
import org.apache.hadoop.hbase.procedure2.RSProcedureCallable;
import org.apache.hadoop.hbase.quotas.ActivePolicyEnforcement;
@@ -195,6 +201,8 @@ import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.ReplicateWA
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.ReplicateWALEntryResponse;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.RollWALWriterRequest;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.RollWALWriterResponse;
+import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.SlowLogResponseRequest;
+import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.SlowLogResponses;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.StopServerRequest;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.StopServerResponse;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.UpdateFavoredNodesRequest;
@@ -234,6 +242,7 @@ import
org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.ScanReques
import
org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.ScanResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos.RegionLoad;
+import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BooleanMsg;
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest;
@@ -246,6 +255,7 @@ import
org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuo
import
org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaSnapshotsResponse;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaSnapshotsResponse.TableQuotaSnapshot;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.RegistryProtos.ClientMetaService;
+import
org.apache.hadoop.hbase.shaded.protobuf.generated.TooSlowLog.SlowLogPayload;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.WALProtos.BulkLoadDescriptor;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.WALProtos.CompactionDescriptor;
import
org.apache.hadoop.hbase.shaded.protobuf.generated.WALProtos.FlushDescriptor;
@@ -4118,4 +4128,50 @@ public class RSRpcServices extends
HBaseRpcServicesBase<HRegionServer>
Pair<String, RegionScannerHolder> pair = newRegionScanner(request, region,
builder);
return new RegionScannerContext(pair.getFirst(), pair.getSecond(), quota);
}
+
+ private List<SlowLogPayload> getSlowLogPayloads(SlowLogResponseRequest
request,
+ NamedQueueRecorder namedQueueRecorder) {
+ if (namedQueueRecorder == null) {
+ return Collections.emptyList();
+ }
+ List<SlowLogPayload> slowLogPayloads;
+ NamedQueueGetRequest namedQueueGetRequest = new NamedQueueGetRequest();
+ namedQueueGetRequest.setNamedQueueEvent(RpcLogDetails.SLOW_LOG_EVENT);
+ namedQueueGetRequest.setSlowLogResponseRequest(request);
+ NamedQueueGetResponse namedQueueGetResponse =
+ namedQueueRecorder.getNamedQueueRecords(namedQueueGetRequest);
+ slowLogPayloads = namedQueueGetResponse != null
+ ? namedQueueGetResponse.getSlowLogPayloads()
+ : Collections.emptyList();
+ return slowLogPayloads;
+ }
+
+ @Override
+ @QosPriority(priority = HConstants.ADMIN_QOS)
+ public HBaseProtos.LogEntry getLogEntries(RpcController controller,
+ HBaseProtos.LogRequest request) throws ServiceException {
+ try {
+ requirePermission("getLogEntries", Permission.Action.ADMIN);
+ final String logClassName = request.getLogClassName();
+ Class<?> logClass =
Class.forName(logClassName).asSubclass(Message.class);
+ Method method = logClass.getMethod("parseFrom", ByteString.class);
+ if (logClassName.contains("SlowLogResponseRequest")) {
+ SlowLogResponseRequest slowLogResponseRequest =
+ (SlowLogResponseRequest) method.invoke(null,
request.getLogMessage());
+ final NamedQueueRecorder namedQueueRecorder =
this.server.getNamedQueueRecorder();
+ final List<SlowLogPayload> slowLogPayloads =
+ getSlowLogPayloads(slowLogResponseRequest, namedQueueRecorder);
+ SlowLogResponses slowLogResponses =
+
SlowLogResponses.newBuilder().addAllSlowLogPayloads(slowLogPayloads).build();
+ return HBaseProtos.LogEntry.newBuilder()
+ .setLogClassName(slowLogResponses.getClass().getName())
+ .setLogMessage(slowLogResponses.toByteString()).build();
+ }
+ } catch (ClassNotFoundException | NoSuchMethodException |
IllegalAccessException
+ | InvocationTargetException | IOException e) {
+ LOG.error("Error while retrieving log entries.", e);
+ throw new ServiceException(e);
+ }
+ throw new ServiceException("Invalid request params");
+ }
}
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java
index 3b45e0175cc..13c8639c33d 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java
@@ -874,6 +874,13 @@ public class AccessController implements
MasterCoprocessor, RegionCoprocessor,
});
}
+ @Override
+ public void preTruncateRegion(final
ObserverContext<MasterCoprocessorEnvironment> ctx,
+ final RegionInfo regionInfo) throws IOException {
+ requirePermission(ctx, "truncateRegion", regionInfo.getTable(), null,
null, Action.ADMIN,
+ Action.CREATE);
+ }
+
@Override
public TableDescriptor
preModifyTable(ObserverContext<MasterCoprocessorEnvironment> c,
TableName tableName, TableDescriptor currentDesc, TableDescriptor newDesc)
throws IOException {
@@ -1990,7 +1997,10 @@ public class AccessController implements
MasterCoprocessor, RegionCoprocessor,
request.hasColumnFamily() ? request.getColumnFamily().toByteArray()
: null;
final byte[] cq =
request.hasColumnQualifier() ?
request.getColumnQualifier().toByteArray() : null;
- preGetUserPermissions(caller, userName, namespace, table, cf, cq);
+ AccessControlProtos.Permission.Type protoType =
+ request.hasType() ? request.getType() : null;
+ Permission.Scope permissionScope =
ShadedAccessControlUtil.toPermissionScope(protoType);
+ preGetUserPermissions(caller, userName, namespace, table, cf, cq,
permissionScope);
GetUserPermissionsRequest getUserPermissionsRequest = null;
if (request.getType() == AccessControlProtos.Permission.Type.Table) {
getUserPermissionsRequest =
GetUserPermissionsRequest.newBuilder(table).withFamily(cf)
@@ -2418,9 +2428,26 @@ public class AccessController implements
MasterCoprocessor, RegionCoprocessor,
@Override
public void
preGetUserPermissions(ObserverContext<MasterCoprocessorEnvironment> ctx,
- String userName, String namespace, TableName tableName, byte[] family,
byte[] qualifier)
+ String userName, String namespace, TableName tableName, byte[] family,
byte[] qualifier,
+ Permission.Scope permissionScope) throws IOException {
+ preGetUserPermissions(getActiveUser(ctx), userName, namespace, tableName,
family, qualifier,
+ permissionScope);
+ }
+
+ private void preGetUserPermissions(User caller, String userName, String
namespace,
+ TableName tableName, byte[] family, byte[] qualifier, Permission.Scope
permissionScope)
throws IOException {
- preGetUserPermissions(getActiveUser(ctx), userName, namespace, tableName,
family, qualifier);
+ if (permissionScope == Permission.Scope.TABLE) {
+ accessChecker.requirePermission(caller, "getUserPermissions", tableName,
family, qualifier,
+ userName, Action.ADMIN);
+ } else if (permissionScope == Permission.Scope.NAMESPACE) {
+ accessChecker.requireNamespacePermission(caller, "getUserPermissions",
namespace, userName,
+ Action.ADMIN);
+ } else if (permissionScope == Permission.Scope.GLOBAL) {
+ accessChecker.requirePermission(caller, "getUserPermissions", userName,
Action.ADMIN);
+ } else {
+ preGetUserPermissions(caller, userName, namespace, tableName, family,
qualifier);
+ }
}
private void preGetUserPermissions(User caller, String userName, String
namespace,
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/MasterReadOnlyController.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/MasterReadOnlyController.java
index e6efcac856e..a1feceb4d8e 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/MasterReadOnlyController.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/MasterReadOnlyController.java
@@ -169,7 +169,7 @@ public class MasterReadOnlyController extends
AbstractReadOnlyController
@Override
public void preTruncateRegion(ObserverContext<MasterCoprocessorEnvironment>
c,
- RegionInfo regionInfo) {
+ RegionInfo regionInfo) throws IOException {
try {
internalReadOnlyGuard();
} catch (IOException e) {
@@ -181,7 +181,7 @@ public class MasterReadOnlyController extends
AbstractReadOnlyController
@Override
public void
preTruncateRegionAction(ObserverContext<MasterCoprocessorEnvironment> c,
- RegionInfo regionInfo) {
+ RegionInfo regionInfo) throws IOException {
try {
internalReadOnlyGuard();
} catch (IOException e) {
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java
index 8a32ca8c0d7..2935f5565bc 100644
---
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java
@@ -3418,6 +3418,68 @@ public class TestAccessController extends SecureTestUtil
{
}
}
+ @Test
+ public void testGetUserPermissionsScopeBasedAuthorization() throws Throwable
{
+ // Regression test: authorization must be based on permissionScope
(derived from
+ // the type field), not on the tableName field. A user with only
table-level ADMIN
+ // should not be able to request global permissions by passing tableName
with
+ // permissionScope=GLOBAL.
+ AccessTestAction tablePermWithGlobalScope = new AccessTestAction() {
+ @Override
+ public Object run() throws Exception {
+
ACCESS_CONTROLLER.preGetUserPermissions(ObserverContextImpl.createAndPrepare(CP_ENV),
null,
+ null, TEST_TABLE, null, null, Permission.Scope.GLOBAL);
+ return null;
+ }
+ };
+
+ // USER_ADMIN has global ADMIN, should be allowed
+ verifyAllowed(tablePermWithGlobalScope, SUPERUSER, USER_ADMIN);
+ // Users with only table-level ADMIN should be denied
+ verifyDenied(tablePermWithGlobalScope, USER_OWNER, USER_ADMIN_CF,
USER_CREATE, USER_RW, USER_RO,
+ USER_NONE);
+
+ AccessTestAction tablePermWithNamespaceScope = new AccessTestAction() {
+ @Override
+ public Object run() throws Exception {
+
ACCESS_CONTROLLER.preGetUserPermissions(ObserverContextImpl.createAndPrepare(CP_ENV),
null,
+ TEST_TABLE.getNamespaceAsString(), TEST_TABLE, null, null,
Permission.Scope.NAMESPACE);
+ return null;
+ }
+ };
+
+ // Users without namespace ADMIN should be denied
+ verifyDenied(tablePermWithNamespaceScope, USER_OWNER, USER_ADMIN_CF,
USER_CREATE, USER_RW,
+ USER_RO, USER_NONE);
+
+ AccessTestAction tablePermWithTableScope = new AccessTestAction() {
+ @Override
+ public Object run() throws Exception {
+
ACCESS_CONTROLLER.preGetUserPermissions(ObserverContextImpl.createAndPrepare(CP_ENV),
null,
+ null, TEST_TABLE, null, null, Permission.Scope.TABLE);
+ return null;
+ }
+ };
+
+ // Users with table ADMIN should be allowed
+ verifyAllowed(tablePermWithTableScope, SUPERUSER, USER_ADMIN, USER_OWNER);
+ verifyDenied(tablePermWithTableScope, USER_ADMIN_CF, USER_CREATE, USER_RW,
USER_RO, USER_NONE);
+
+ AccessTestAction globalPermWithNullScope = new AccessTestAction() {
+ @Override
+ public Object run() throws Exception {
+
ACCESS_CONTROLLER.preGetUserPermissions(ObserverContextImpl.createAndPrepare(CP_ENV),
null,
+ null, null, null, null, null);
+ return null;
+ }
+ };
+
+ // null scope falls back to old behavior (no tableName -> require global
ADMIN)
+ verifyAllowed(globalPermWithNullScope, SUPERUSER, USER_ADMIN);
+ verifyDenied(globalPermWithNullScope, USER_OWNER, USER_ADMIN_CF,
USER_CREATE, USER_RW, USER_RO,
+ USER_NONE);
+ }
+
@Test
public void testHasPermission() throws Throwable {
Connection conn = null;
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessControllerObserverCoverage.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessControllerObserverCoverage.java
new file mode 100644
index 00000000000..dafe9cf065e
--- /dev/null
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessControllerObserverCoverage.java
@@ -0,0 +1,222 @@
+/*
+ * 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.junit.jupiter.api.Assertions.fail;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import org.apache.hadoop.hbase.coprocessor.BulkLoadObserver;
+import org.apache.hadoop.hbase.coprocessor.EndpointObserver;
+import org.apache.hadoop.hbase.coprocessor.MasterObserver;
+import org.apache.hadoop.hbase.coprocessor.RegionObserver;
+import org.apache.hadoop.hbase.coprocessor.RegionServerObserver;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that AccessController implements every security-relevant method
declared in the five
+ * observer interfaces it claims to implement: MasterObserver, RegionObserver,
RegionServerObserver,
+ * EndpointObserver, BulkLoadObserver.
+ * <p>
+ * If a new hook is added to any of these interfaces and AccessController does
not override it, the
+ * default no-op implementation will silently skip the permission check — a
potential privilege
+ * escalation. This test catches that at build time.
+ * <p>
+ * Skipped methods are determined by two mechanisms:
+ * <ul>
+ * <li><b>Pattern rules</b> — methods matching these patterns are always safe
to skip:
+ * <ul>
+ * <li>{@code post*} — post-operation notifications; the operation has already
been authorized and
+ * executed.</li>
+ * <li>{@code pre*Action} — procedure-level action hooks; authorization
happens at the RPC layer in
+ * the corresponding {@code pre*} hook.</li>
+ * </ul>
+ * </li>
+ * <li><b>Explicit whitelist</b> — methods that don't match the above rules
but are still safe to
+ * skip (internal lifecycle hooks, deprecated overloads with default
delegation, read-only queries).
+ * Each entry has a justification comment.</li>
+ * </ul>
+ */
+@Tag(SecurityTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestAccessControllerObserverCoverage {
+
+ /**
+ * Explicit whitelist for methods that don't match the pattern rules but are
intentionally not
+ * overridden in AccessController.
+ * <p>
+ * Use simple method name (covers all overloads) or full signature key
+ * "methodName(ParamType1,ParamType2,..." using simple class names.
+ */
+ private static final Set<String> WHITELIST = new HashSet<>(Arrays.asList(
+
+ // --- Internal lifecycle hooks (not client-facing RPCs) ---
+ // Store file / WAL internal hooks
+ "preStoreFileReaderOpen", "preStoreScannerOpen", "preCommitStoreFile",
"preReplayWALs",
+ "preWALRestore",
+ // Master internal
+ "preMasterStoreFlush",
+ // Lifecycle markers (not triggered by client RPC)
+ "preMasterInitialization", "preCreateTableRegionsInfos",
+ // --- Read-only query hooks (no mutation, no authorization needed) ---
+ "preGetClusterMetrics", "preGetTableNames", "preListNamespaceDescriptors",
"preListNamespaces",
+
+ // --- Deprecated overloads: interface default delegates to non-deprecated
---
+ // prePut(3-arg) delegates to prePut(4-arg Durability)
+
"prePut(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Put,org.apache.hadoop.hbase.wal.WALEdit)",
+ // preDelete(3-arg) delegates to preDelete(4-arg Durability)
+
"preDelete(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Delete,org.apache.hadoop.hbase.wal.WALEdit)",
+ // preAppend(3-arg) delegates to preAppend(2-arg deprecated)
+
"preAppend(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Append,org.apache.hadoop.hbase.wal.WALEdit)",
+ // preAppendAfterRowLock(2-arg) delegates to preAppendAfterRowLock(1-arg
deprecated)
+
"preAppendAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Append)",
+ // preIncrement(3-arg) delegates to preIncrement(2-arg deprecated)
+
"preIncrement(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Increment,org.apache.hadoop.hbase.wal.WALEdit)",
+ // preIncrementAfterRowLock(2-arg) delegates to
preIncrementAfterRowLock(1-arg deprecated)
+
"preIncrementAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Increment)",
+ // preCheckAndMutate delegates to preCheckAndPut/preCheckAndDelete
+
"preCheckAndMutate(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.CheckAndMutate,org.apache.hadoop.hbase.client.CheckAndMutateResult)",
+ // preCheckAndMutateAfterRowLock delegates to
+ // preCheckAndPutAfterRowLock/preCheckAndDeleteAfterRowLock
+
"preCheckAndMutateAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.CheckAndMutate,org.apache.hadoop.hbase.client.CheckAndMutateResult)",
+ // Filter-based CheckAnd* are deprecated; framework uses byte[] overloads
+ // Note: Class.getName() returns "[B" for byte[], not "byte[]"
+
"preCheckAndPut(org.apache.hadoop.hbase.coprocessor.ObserverContext,[B,org.apache.hadoop.hbase.filter.Filter,org.apache.hadoop.hbase.client.Put,boolean)",
+
"preCheckAndPutAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,[B,org.apache.hadoop.hbase.filter.Filter,org.apache.hadoop.hbase.client.Put,boolean)",
+
"preCheckAndDelete(org.apache.hadoop.hbase.coprocessor.ObserverContext,[B,org.apache.hadoop.hbase.filter.Filter,org.apache.hadoop.hbase.client.Delete,boolean)",
+
"preCheckAndDeleteAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,[B,org.apache.hadoop.hbase.filter.Filter,org.apache.hadoop.hbase.client.Delete,boolean)",
+ // Deprecated preGetUserPermissions(6-arg) delegates to
preGetUserPermissions(7-arg with Scope)
+
"preGetUserPermissions(ObserverContext,String,String,TableName,byte[],byte[])",
+ // Deprecated WAL-append / timestamp hooks
+ "prePrepareTimeStampForDeleteVersion", "preWALAppend",
+ // --- Replication sink is a trusted internal cluster-to-cluster operation
---
+ "preReplicationSinkBatchMutate"));
+
+ private static final Class<?>[] OBSERVER_INTERFACES =
+ { MasterObserver.class, RegionObserver.class, RegionServerObserver.class,
+ EndpointObserver.class, BulkLoadObserver.class };
+
+ /**
+ * Returns true if the method matches a pattern rule that makes it safe to
skip without an
+ * explicit whitelist entry.
+ */
+ private static boolean matchesSkipPattern(Class<?> iface, Method m) {
+ String name = m.getName();
+ // All post* methods are post-operation notifications.
+ // Permission checks must happen before the operation, not after.
+ if (name.startsWith("post")) {
+ return true;
+ }
+ // *Action suffix on pre* hooks are procedure-level callbacks.
+ // Authorization is done at the RPC layer in the corresponding pre* hook.
+ if (name.endsWith("Action")) {
+ return true;
+ }
+ // RegionObserver internal storage hooks: flush, compaction, in-memory
+ // compaction. These are sub-step callbacks within a region storage
+ // operation. The region-level entry hook (preFlush, preCompact) already
+ // handles authorization in AccessController.
+ if (
+ iface == RegionObserver.class && (name.startsWith("preFlush") ||
name.startsWith("preCompact")
+ || name.startsWith("preMemStore"))
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ private static String methodSignatureKey(Method m) {
+ String paramTypes =
Arrays.stream(m.getParameterTypes()).map(Class::getSimpleName)
+ .collect(Collectors.joining(","));
+ return m.getName() + "(" + paramTypes + ")";
+ }
+
+ private static String fullMethodSignatureKey(Method m) {
+ String paramTypes =
+
Arrays.stream(m.getParameterTypes()).map(Class::getName).collect(Collectors.joining(","));
+ return m.getName() + "(" + paramTypes + ")";
+ }
+
+ private static boolean isMethodImplemented(Class<?> implClass, Method
ifaceMethod) {
+ Class<?> clazz = implClass;
+ while (clazz != null) {
+ for (Method m : clazz.getDeclaredMethods()) {
+ if (
+ m.getName().equals(ifaceMethod.getName())
+ && Arrays.equals(m.getParameterTypes(),
ifaceMethod.getParameterTypes())
+ ) {
+ return true;
+ }
+ }
+ clazz = clazz.getSuperclass();
+ }
+ return false;
+ }
+
+ @Test
+ public void testAllObserverMethodsAreImplemented() {
+ Set<String> missing = new TreeSet<>();
+
+ for (Class<?> iface : OBSERVER_INTERFACES) {
+ for (Method m : iface.getMethods()) {
+ if (m.getDeclaringClass() == Object.class) {
+ continue;
+ }
+ if (!m.getDeclaringClass().equals(iface)) {
+ continue;
+ }
+
+ if (matchesSkipPattern(iface, m)) {
+ continue;
+ }
+
+ String simpleName = m.getName();
+ String simpleKey = methodSignatureKey(m);
+ String fullKey = fullMethodSignatureKey(m);
+
+ if (
+ WHITELIST.contains(simpleName) || WHITELIST.contains(simpleKey)
+ || WHITELIST.contains(fullKey)
+ ) {
+ continue;
+ }
+
+ if (!isMethodImplemented(AccessController.class, m)) {
+ missing.add(" " + iface.getSimpleName() + "." + simpleKey);
+ }
+ }
+ }
+
+ if (!missing.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("AccessController does not implement the following observer
methods.\n");
+ sb.append("Either override them in AccessController (with permission
checks),\n");
+ sb.append("or add them to the WHITELIST with a justification
comment.\n\n");
+ sb.append("Missing methods:\n");
+ missing.forEach(m -> sb.append(m).append("\n"));
+ fail(sb.toString());
+ }
+ }
+}