001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.security.access; 019 020import static org.junit.jupiter.api.Assertions.fail; 021 022import java.lang.reflect.Method; 023import java.util.Arrays; 024import java.util.HashSet; 025import java.util.Set; 026import java.util.TreeSet; 027import java.util.stream.Collectors; 028import org.apache.hadoop.hbase.coprocessor.BulkLoadObserver; 029import org.apache.hadoop.hbase.coprocessor.EndpointObserver; 030import org.apache.hadoop.hbase.coprocessor.MasterObserver; 031import org.apache.hadoop.hbase.coprocessor.RegionObserver; 032import org.apache.hadoop.hbase.coprocessor.RegionServerObserver; 033import org.apache.hadoop.hbase.testclassification.SecurityTests; 034import org.apache.hadoop.hbase.testclassification.SmallTests; 035import org.junit.jupiter.api.Tag; 036import org.junit.jupiter.api.Test; 037 038/** 039 * Verifies that AccessController implements every security-relevant method declared in the five 040 * observer interfaces it claims to implement: MasterObserver, RegionObserver, RegionServerObserver, 041 * EndpointObserver, BulkLoadObserver. 042 * <p> 043 * If a new hook is added to any of these interfaces and AccessController does not override it, the 044 * default no-op implementation will silently skip the permission check — a potential privilege 045 * escalation. This test catches that at build time. 046 * <p> 047 * Skipped methods are determined by two mechanisms: 048 * <ul> 049 * <li><b>Pattern rules</b> — methods matching these patterns are always safe to skip: 050 * <ul> 051 * <li>{@code post*} — post-operation notifications; the operation has already been authorized and 052 * executed.</li> 053 * <li>{@code pre*Action} — procedure-level action hooks; authorization happens at the RPC layer in 054 * the corresponding {@code pre*} hook.</li> 055 * </ul> 056 * </li> 057 * <li><b>Explicit whitelist</b> — methods that don't match the above rules but are still safe to 058 * skip (internal lifecycle hooks, deprecated overloads with default delegation, read-only queries). 059 * Each entry has a justification comment.</li> 060 * </ul> 061 */ 062@Tag(SecurityTests.TAG) 063@Tag(SmallTests.TAG) 064public class TestAccessControllerObserverCoverage { 065 066 /** 067 * Explicit whitelist for methods that don't match the pattern rules but are intentionally not 068 * overridden in AccessController. 069 * <p> 070 * Use simple method name (covers all overloads) or full signature key 071 * "methodName(ParamType1,ParamType2,..." using simple class names. 072 */ 073 private static final Set<String> WHITELIST = new HashSet<>(Arrays.asList( 074 075 // --- Internal lifecycle hooks (not client-facing RPCs) --- 076 // Store file / WAL internal hooks 077 "preStoreFileReaderOpen", "preStoreScannerOpen", "preCommitStoreFile", "preReplayWALs", 078 "preWALRestore", 079 // Master internal 080 "preMasterStoreFlush", 081 // Lifecycle markers (not triggered by client RPC) 082 "preMasterInitialization", "preCreateTableRegionsInfos", 083 // --- Read-only query hooks (no mutation, no authorization needed) --- 084 "preGetClusterMetrics", "preGetTableNames", "preListNamespaceDescriptors", "preListNamespaces", 085 086 // --- Deprecated overloads: interface default delegates to non-deprecated --- 087 // prePut(3-arg) delegates to prePut(4-arg Durability) 088 "prePut(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Put,org.apache.hadoop.hbase.wal.WALEdit)", 089 // preDelete(3-arg) delegates to preDelete(4-arg Durability) 090 "preDelete(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Delete,org.apache.hadoop.hbase.wal.WALEdit)", 091 // preAppend(3-arg) delegates to preAppend(2-arg deprecated) 092 "preAppend(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Append,org.apache.hadoop.hbase.wal.WALEdit)", 093 // preAppendAfterRowLock(2-arg) delegates to preAppendAfterRowLock(1-arg deprecated) 094 "preAppendAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Append)", 095 // preIncrement(3-arg) delegates to preIncrement(2-arg deprecated) 096 "preIncrement(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Increment,org.apache.hadoop.hbase.wal.WALEdit)", 097 // preIncrementAfterRowLock(2-arg) delegates to preIncrementAfterRowLock(1-arg deprecated) 098 "preIncrementAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.Increment)", 099 // preCheckAndMutate delegates to preCheckAndPut/preCheckAndDelete 100 "preCheckAndMutate(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.CheckAndMutate,org.apache.hadoop.hbase.client.CheckAndMutateResult)", 101 // preCheckAndMutateAfterRowLock delegates to 102 // preCheckAndPutAfterRowLock/preCheckAndDeleteAfterRowLock 103 "preCheckAndMutateAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,org.apache.hadoop.hbase.client.CheckAndMutate,org.apache.hadoop.hbase.client.CheckAndMutateResult)", 104 // Filter-based CheckAnd* are deprecated; framework uses byte[] overloads 105 // Note: Class.getName() returns "[B" for byte[], not "byte[]" 106 "preCheckAndPut(org.apache.hadoop.hbase.coprocessor.ObserverContext,[B,org.apache.hadoop.hbase.filter.Filter,org.apache.hadoop.hbase.client.Put,boolean)", 107 "preCheckAndPutAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,[B,org.apache.hadoop.hbase.filter.Filter,org.apache.hadoop.hbase.client.Put,boolean)", 108 "preCheckAndDelete(org.apache.hadoop.hbase.coprocessor.ObserverContext,[B,org.apache.hadoop.hbase.filter.Filter,org.apache.hadoop.hbase.client.Delete,boolean)", 109 "preCheckAndDeleteAfterRowLock(org.apache.hadoop.hbase.coprocessor.ObserverContext,[B,org.apache.hadoop.hbase.filter.Filter,org.apache.hadoop.hbase.client.Delete,boolean)", 110 // Deprecated preGetUserPermissions(6-arg) delegates to preGetUserPermissions(7-arg with Scope) 111 "preGetUserPermissions(ObserverContext,String,String,TableName,byte[],byte[])", 112 // Deprecated WAL-append / timestamp hooks 113 "prePrepareTimeStampForDeleteVersion", "preWALAppend", 114 // --- Replication sink is a trusted internal cluster-to-cluster operation --- 115 "preReplicationSinkBatchMutate")); 116 117 private static final Class<?>[] OBSERVER_INTERFACES = 118 { MasterObserver.class, RegionObserver.class, RegionServerObserver.class, 119 EndpointObserver.class, BulkLoadObserver.class }; 120 121 /** 122 * Returns true if the method matches a pattern rule that makes it safe to skip without an 123 * explicit whitelist entry. 124 */ 125 private static boolean matchesSkipPattern(Class<?> iface, Method m) { 126 String name = m.getName(); 127 // All post* methods are post-operation notifications. 128 // Permission checks must happen before the operation, not after. 129 if (name.startsWith("post")) { 130 return true; 131 } 132 // *Action suffix on pre* hooks are procedure-level callbacks. 133 // Authorization is done at the RPC layer in the corresponding pre* hook. 134 if (name.endsWith("Action")) { 135 return true; 136 } 137 // RegionObserver internal storage hooks: flush, compaction, in-memory 138 // compaction. These are sub-step callbacks within a region storage 139 // operation. The region-level entry hook (preFlush, preCompact) already 140 // handles authorization in AccessController. 141 if ( 142 iface == RegionObserver.class && (name.startsWith("preFlush") || name.startsWith("preCompact") 143 || name.startsWith("preMemStore")) 144 ) { 145 return true; 146 } 147 return false; 148 } 149 150 private static String methodSignatureKey(Method m) { 151 String paramTypes = Arrays.stream(m.getParameterTypes()).map(Class::getSimpleName) 152 .collect(Collectors.joining(",")); 153 return m.getName() + "(" + paramTypes + ")"; 154 } 155 156 private static String fullMethodSignatureKey(Method m) { 157 String paramTypes = 158 Arrays.stream(m.getParameterTypes()).map(Class::getName).collect(Collectors.joining(",")); 159 return m.getName() + "(" + paramTypes + ")"; 160 } 161 162 private static boolean isMethodImplemented(Class<?> implClass, Method ifaceMethod) { 163 Class<?> clazz = implClass; 164 while (clazz != null) { 165 for (Method m : clazz.getDeclaredMethods()) { 166 if ( 167 m.getName().equals(ifaceMethod.getName()) 168 && Arrays.equals(m.getParameterTypes(), ifaceMethod.getParameterTypes()) 169 ) { 170 return true; 171 } 172 } 173 clazz = clazz.getSuperclass(); 174 } 175 return false; 176 } 177 178 @Test 179 public void testAllObserverMethodsAreImplemented() { 180 Set<String> missing = new TreeSet<>(); 181 182 for (Class<?> iface : OBSERVER_INTERFACES) { 183 for (Method m : iface.getMethods()) { 184 if (m.getDeclaringClass() == Object.class) { 185 continue; 186 } 187 if (!m.getDeclaringClass().equals(iface)) { 188 continue; 189 } 190 191 if (matchesSkipPattern(iface, m)) { 192 continue; 193 } 194 195 String simpleName = m.getName(); 196 String simpleKey = methodSignatureKey(m); 197 String fullKey = fullMethodSignatureKey(m); 198 199 if ( 200 WHITELIST.contains(simpleName) || WHITELIST.contains(simpleKey) 201 || WHITELIST.contains(fullKey) 202 ) { 203 continue; 204 } 205 206 if (!isMethodImplemented(AccessController.class, m)) { 207 missing.add(" " + iface.getSimpleName() + "." + simpleKey); 208 } 209 } 210 } 211 212 if (!missing.isEmpty()) { 213 StringBuilder sb = new StringBuilder(); 214 sb.append("AccessController does not implement the following observer methods.\n"); 215 sb.append("Either override them in AccessController (with permission checks),\n"); 216 sb.append("or add them to the WHITELIST with a justification comment.\n\n"); 217 sb.append("Missing methods:\n"); 218 missing.forEach(m -> sb.append(m).append("\n")); 219 fail(sb.toString()); 220 } 221 } 222}