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}