toProcess;
+ synchronized (OpaFixtureWriter.class) {
+ toProcess = new ArrayList<>(captured);
+ captured.clear();
+ }
+
+ synchronized (OpaFixtureWriter.class) {
+ for (String[] pair : toProcess) {
+ String requestBody = pair[0];
+ String responseBody = pair[1];
+
+ // Only capture requests from the standard allow/deny test users defined in TestUtils.
+ // Requests from other named users (e.g. Variants-specific users) and cluster-internal
+ // traffic are intentionally skipped — they are not useful for Rego policy validation.
+ if (!requestBody.contains("allowedUser")
+ && !requestBody.contains("deniedUser")
+ && !requestBody.contains("readonlyUser")) {
+ continue;
+ }
+ String remapped =
+ requestBody
+ .replace("allowedUser", OPA_REMAP_ALLOWED)
+ .replace("deniedUser", OPA_REMAP_DENIED)
+ .replace("readonlyUser", OPA_REMAP_READONLY);
+
+ // WireMock stubs in this test suite always return {"result": "true"} or {"result":
+ // "false"}.
+ boolean allowed = responseBody.contains("\"true\"");
+
+ if (allowed) {
+ if (seenAllowed.add(remapped)) {
+ allowedFixtures.add(remapped);
+ }
+ } else {
+ if (seenDenied.add(remapped)) {
+ deniedFixtures.add(remapped);
+ }
+ }
+ }
+
+ Files.createDirectories(FIXTURES_FILE.getParent());
+ Files.writeString(FIXTURES_FILE, buildFixturesJson());
+ }
+ }
+
+ private static String buildFixturesJson() {
+ String allowed = allowedFixtures.stream().collect(Collectors.joining(","));
+ String denied = deniedFixtures.stream().collect(Collectors.joining(","));
+ return "{\"fixtures\":{\"allowed\":[" + allowed + "],\"denied\":[" + denied + "]}}";
+ }
+}
diff --git a/src/test/java/tech/stackable/hbase/TestCoprocessorInterfaceCoverage.java b/src/test/java/tech/stackable/hbase/TestCoprocessorInterfaceCoverage.java
new file mode 100644
index 0000000..7ee2f32
--- /dev/null
+++ b/src/test/java/tech/stackable/hbase/TestCoprocessorInterfaceCoverage.java
@@ -0,0 +1,195 @@
+package tech.stackable.hbase;
+
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+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.junit.Test;
+
+/**
+ * Verifies that every method in the coprocessor observer interfaces is either explicitly overridden
+ * in OpenPolicyAgentAccessController or listed in the known exclusion set below.
+ *
+ * Because all HBase observer interface methods have default implementations, adding a new hook
+ * upstream would not cause a compile error. This test catches that: any new method that appears in
+ * an observer interface and is not in our class or in EXCLUDED will cause a test failure.
+ *
+ *
When upgrading HBase, if this test fails, review the new method(s) and either:
+ *
+ *
+ * - Implement them in OpenPolicyAgentAccessController, or
+ *
- Add them to the appropriate section of EXCLUDED with a justification comment.
+ *
+ *
+ * Note: all {@code post*} methods are excluded automatically — they fire after the operation has
+ * already been permitted and cannot block it, so OPA enforcement is never applicable.
+ */
+public class TestCoprocessorInterfaceCoverage {
+
+ private static final Class>[] OBSERVER_INTERFACES = {
+ MasterObserver.class,
+ RegionObserver.class,
+ RegionServerObserver.class,
+ EndpointObserver.class,
+ BulkLoadObserver.class,
+ };
+
+ /**
+ * Pre-hooks that are deliberately not overridden. Organised by reason. When a new HBase version
+ * adds a method that belongs here, add it with a comment explaining why.
+ */
+ private static final Set EXCLUDED =
+ new HashSet<>(
+ Arrays.asList(
+
+ // --- deprecated overloads superseded by variants we do override ---
+ // The WALEdit-carrying variants of data-write hooks were deprecated in HBase 2.x;
+ // the 2-arg versions (which we do override) are the current API.
+ "preAppend(ObserverContext, Append, WALEdit)",
+ "preDelete(ObserverContext, Delete, WALEdit)",
+ "preIncrement(ObserverContext, Increment, WALEdit)",
+ "prePut(ObserverContext, Put, WALEdit)",
+ // Old single-descriptor overload; we override the 4-arg (old + new) variant.
+ "preModifyTable(ObserverContext, TableName, TableDescriptor)",
+ // Old 2-descriptor namespace overload; we override the 2-arg (new descriptor only)
+ // variant.
+ "preModifyNamespace(ObserverContext, NamespaceDescriptor, NamespaceDescriptor)",
+ // Old 3-arg unassign with boolean; we override the 2-arg variant.
+ "preUnassign(ObserverContext, RegionInfo, boolean)",
+
+ // --- after-row-lock variants where we check at the pre-lock level ---
+ // HBase calls the pre-lock hook before acquiring the row lock and the after-lock hook
+ // after.
+ // We enforce permissions at the pre-lock level (preAppend, preIncrement), so the
+ // after-lock
+ // variants are redundant for OPA authorization.
+ "preAppendAfterRowLock(ObserverContext, Append)",
+ "preIncrementAfterRowLock(ObserverContext, Increment)",
+
+ // --- internal HBase multi-step DDL action hooks ---
+ // These are called internally by the HBase master during multi-step DDL procedures.
+ // They are not triggered by direct client calls; the public pre* hooks (e.g.
+ // preCreateTable)
+ // are already checked before these fire.
+ "preCreateTableAction(ObserverContext, TableDescriptor, RegionInfo[])",
+ "preCreateTableRegionsInfos(ObserverContext, TableDescriptor)",
+ "preDeleteTableAction(ObserverContext, TableName)",
+ "preEnableTableAction(ObserverContext, TableName)",
+ "preDisableTableAction(ObserverContext, TableName)",
+ "preTruncateTableAction(ObserverContext, TableName)",
+ "preTruncateRegion(ObserverContext, RegionInfo)",
+ "preTruncateRegionAction(ObserverContext, RegionInfo)",
+ "preModifyTableAction(ObserverContext, TableName, TableDescriptor)",
+ "preModifyTableAction(ObserverContext, TableName, TableDescriptor, TableDescriptor)",
+ "preMergeRegionsAction(ObserverContext, RegionInfo[])",
+ "preMergeRegionsCommitAction(ObserverContext, RegionInfo[], List)",
+ "preSplitRegionAction(ObserverContext, TableName, byte[])",
+ "preSplitRegionBeforeMETAAction(ObserverContext, byte[], List)",
+ "preSplitRegionAfterMETAAction(ObserverContext)",
+
+ // --- internal storage, compaction, and scan hooks ---
+ // These are called by HBase's internal storage engine for compaction, flush, and scan
+ // operations. They are not user-initiated and carry no meaningful authorization
+ // context.
+ "preClose(ObserverContext, boolean)",
+ "preCommitStoreFile(ObserverContext, byte[], List)",
+ "preCompactScannerOpen(ObserverContext, Store, ScanType, ScanOptions, CompactionLifeCycleTracker, CompactionRequest)",
+ "preCompactSelection(ObserverContext, Store, List, CompactionLifeCycleTracker)",
+ "preFlush(ObserverContext, Store, InternalScanner, FlushLifeCycleTracker)",
+ "preFlushScannerOpen(ObserverContext, Store, ScanOptions, FlushLifeCycleTracker)",
+ "preMemStoreCompaction(ObserverContext, Store)",
+ "preMemStoreCompactionCompact(ObserverContext, Store, InternalScanner)",
+ "preMemStoreCompactionCompactScannerOpen(ObserverContext, Store, ScanOptions)",
+ "prePrepareTimeStampForDeleteVersion(ObserverContext, Mutation, Cell, byte[], Get)",
+ "preStoreFileReaderOpen(ObserverContext, FileSystem, Path, FSDataInputStreamWrapper, long, CacheConfig, Reference, StoreFileReader)",
+ "preStoreScannerOpen(ObserverContext, Store, ScanOptions)",
+
+ // --- WAL, replication, and master lifecycle hooks ---
+ // Triggered by HBase internals (WAL writers, replication pipeline, master startup),
+ // not by user requests.
+ "preMasterInitialization(ObserverContext)",
+ "preMasterStoreFlush(ObserverContext)",
+ "preReplayWALs(ObserverContext, RegionInfo, Path)",
+ "preWALAppend(ObserverContext, WALKey, WALEdit)",
+ "preWALRestore(ObserverContext, RegionInfo, WALKey, WALEdit)",
+ "preReplicationSinkBatchMutate(ObserverContext, WALEntry, Mutation)",
+
+ // --- RSGroup management ---
+ // RSGroups are an optional HBase feature for grouping RegionServers. Not yet
+ // implemented.
+ // All RSGroup operations should require ADMIN when implemented.
+ "preAddRSGroup(ObserverContext, String)",
+ "preRemoveRSGroup(ObserverContext, String)",
+ "preBalanceRSGroup(ObserverContext, String, BalanceRequest)",
+ "preGetRSGroupInfo(ObserverContext, String)",
+ "preGetRSGroupInfoOfServer(ObserverContext, Address)",
+ "preGetRSGroupInfoOfTable(ObserverContext, TableName)",
+ "preListRSGroups(ObserverContext)",
+ "preMoveServers(ObserverContext, Set, String)",
+ "preMoveServersAndTables(ObserverContext, Set, Set, String)",
+ "preMoveTables(ObserverContext, Set, String)",
+ "preRemoveServers(ObserverContext, Set)",
+ "preRenameRSGroup(ObserverContext, String, String)",
+ "preUpdateRSGroupConfig(ObserverContext, String, Map)",
+
+ // --- TODO: genuine gaps that need OPA implementation ---
+ // These pre-hooks are user-facing and should enforce OPA permissions, but are not yet
+ // implemented. They are excluded here to keep this test focused on detecting new
+ // upstream methods.
+ //
+ // Metadata listing hooks: getTableNames is covered post-hoc by postGetTableNames
+ // filtering; listNamespace* hooks are not currently enforced.
+ "preGetTableNames(ObserverContext, List, String)",
+ "preListNamespaceDescriptors(ObserverContext, List)",
+ "preListNamespaces(ObserverContext, List)",
+ // Cluster metrics: currently unenforced; reference AC requires ADMIN.
+ "preGetClusterMetrics(ObserverContext)"));
+
+ @Test
+ public void testAllObserverMethodsAreExplicitlyOverridden() {
+ Set interfaceMethodSigs =
+ Arrays.stream(OBSERVER_INTERFACES)
+ .flatMap(iface -> Arrays.stream(iface.getDeclaredMethods()))
+ .filter(m -> !m.isSynthetic() && !Modifier.isStatic(m.getModifiers()))
+ .map(TestCoprocessorInterfaceCoverage::signature)
+ .collect(Collectors.toSet());
+
+ Set ourMethodSigs =
+ Arrays.stream(OpenPolicyAgentAccessController.class.getDeclaredMethods())
+ .filter(m -> !m.isSynthetic())
+ .map(TestCoprocessorInterfaceCoverage::signature)
+ .collect(Collectors.toSet());
+
+ List unhandled =
+ interfaceMethodSigs.stream()
+ .filter(sig -> !sig.startsWith("post")) // post hooks can never block operations
+ .filter(sig -> !EXCLUDED.contains(sig))
+ .filter(sig -> !ourMethodSigs.contains(sig))
+ .sorted()
+ .collect(Collectors.toList());
+
+ assertTrue(
+ "Observer interface pre-hooks found that are neither overridden nor in the exclusion list"
+ + " — review each and either implement it or add it to EXCLUDED with a justification:\n"
+ + String.join("\n", unhandled),
+ unhandled.isEmpty());
+ }
+
+ private static String signature(Method m) {
+ String params =
+ Arrays.stream(m.getParameterTypes())
+ .map(Class::getSimpleName)
+ .collect(Collectors.joining(", "));
+ return m.getName() + "(" + params + ")";
+ }
+}
diff --git a/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessController.java b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessController.java
index 7b69db0..6b62cdd 100644
--- a/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessController.java
+++ b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessController.java
@@ -6,44 +6,82 @@
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static org.apache.hadoop.hbase.security.access.SecureTestUtil.createTable;
import static org.apache.hadoop.hbase.security.access.SecureTestUtil.deleteTable;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
+import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
-import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.NamespaceDescriptor;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.client.BalanceRequest;
+import org.apache.hadoop.hbase.client.MasterSwitchType;
import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.RegionInfo;
+import org.apache.hadoop.hbase.client.RegionInfoBuilder;
+import org.apache.hadoop.hbase.client.SnapshotDescription;
import org.apache.hadoop.hbase.client.Table;
+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.coprocessor.ObserverContextImpl;
import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
+import org.apache.hadoop.hbase.quotas.GlobalQuotaSettings;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.access.SecureTestUtil;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.security.AccessControlException;
-import org.junit.Rule;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
import org.junit.Test;
public class TestOpenPolicyAgentAccessController extends TestUtils {
public static final String OPA_URL = "http://localhost:8089";
- @Rule public WireMockRule wireMockRule = new WireMockRule(8089);
+ @ClassRule public static WireMockRule wireMockRule = new WireMockRule(8089);
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ wireMockRule.addMockServiceRequestListener(OpaFixtureWriter::capture);
+ stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
+ setup(OpenPolicyAgentAccessController.class, false, OPA_URL);
+ }
+
+ @Before
+ public void resetStubs() {
+ WireMock.reset();
+ stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws Exception {
+ tearDown();
+ }
+
+ // --- helpers ---
+
+ private ObserverContext ctx() {
+ return ObserverContextImpl.createAndPrepare(CP_ENV);
+ }
+
+ private OpenPolicyAgentAccessController getOpaController() {
+ MasterCoprocessorHost masterCpHost =
+ TEST_UTIL.getMiniHBaseCluster().getMaster().getMasterCoprocessorHost();
+ return masterCpHost.findCoprocessor(OpenPolicyAgentAccessController.class);
+ }
+
+ // --- original tests (non-standard allow/deny patterns) ---
@Test
public void testCreateAndPut() throws Exception {
LOG.info("testCreateAndPut - start");
- stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
- setup(OpenPolicyAgentAccessController.class, false, OPA_URL);
-
HTableDescriptor htd = getHTableDescriptor();
-
createTable(TEST_UTIL, TEST_UTIL.getAdmin(), htd, new byte[][] {Bytes.toBytes("s")});
- // put some test data
List puts = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
Put p = new Put(Bytes.toBytes(i));
@@ -54,21 +92,13 @@ public void testCreateAndPut() throws Exception {
table.put(puts);
deleteTable(TEST_UTIL, TEST_TABLE);
-
- tearDown();
LOG.info("testCreateAndPut - complete");
}
@Test
public void testDeniedCreate() throws Exception {
LOG.info("testDeniedCreate - start");
-
- // let all set-up calls succeed
- stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
- setup(OpenPolicyAgentAccessController.class, false, OPA_URL);
-
try {
- // re-stub so that any subsequent calls will fail
stubFor(post("/").willReturn(ok().withBody("{\"result\": \"false\"}")));
HTableDescriptor htd = getHTableDescriptor();
createTable(TEST_UTIL, TEST_UTIL.getAdmin(), htd, new byte[][] {Bytes.toBytes("s")});
@@ -76,157 +106,613 @@ public void testDeniedCreate() throws Exception {
} catch (AccessControlException e) {
logOk(e);
}
-
- tearDown();
LOG.info("testDeniedCreate - complete");
}
@Test
public void testDeniedCreateByUser() throws Exception {
- stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
- setup(OpenPolicyAgentAccessController.class, false, OPA_URL);
-
User userDenied = User.createUserForTesting(conf, "cannotCreateTables", new String[0]);
-
SecureTestUtil.AccessTestAction createTable =
() -> {
- HTableDescriptor htd = getHTableDescriptor();
- getOpaController()
- .preCreateTable(ObserverContextImpl.createAndPrepare(CP_ENV), htd, null);
+ getOpaController().preCreateTable(ctx(), getHTableDescriptor(), null);
return null;
};
-
- // re-stub so that the call fails for the given user
stubFor(
post("/")
.withRequestBody(
matchingJsonPath("$.input.callerUgi[?(@.userName == 'cannotCreateTables')]"))
.willReturn(ok().withBody("{\"result\": \"false\"}")));
-
try {
userDenied.runAs(createTable);
fail("AccessControlException should have been thrown");
} catch (AccessControlException e) {
logOk(e);
}
-
- tearDown();
}
@Test
- public void testDryRun() throws Exception {
- stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
- setup(OpenPolicyAgentAccessController.class, false, OPA_URL, true, false);
-
- User userDenied = User.createUserForTesting(conf, "cannotCreateTables", new String[0]);
-
- SecureTestUtil.AccessTestAction createTable =
+ public void testCreateNamespace() throws Exception {
+ User userCreater = User.createUserForTesting(conf, "nsCreator", new String[0]);
+ User userDenied = User.createUserForTesting(conf, "nsNonCreator", new String[0]);
+ SecureTestUtil.AccessTestAction createNamespace =
() -> {
- HTableDescriptor htd = getHTableDescriptor();
- getOpaController()
- .preCreateTable(ObserverContextImpl.createAndPrepare(CP_ENV), htd, null);
+ NamespaceDescriptor nsd = NamespaceDescriptor.create("new_ns").build();
+ getOpaController().preCreateNamespace(ctx(), nsd);
return null;
};
-
- // re-stub so that the call would fail for the given user in *non*-dryRun mode
+ try {
+ userCreater.runAs(createNamespace);
+ } catch (AccessControlException e) {
+ throw new AssertionError("AccessControlException should not have been thrown", e);
+ }
stubFor(
post("/")
- .withRequestBody(
- matchingJsonPath("$.input.callerUgi[?(@.userName == 'cannotCreateTables')]"))
+ .withRequestBody(matchingJsonPath("$.input.callerUgi[?(@.userName == 'nsNonCreator')]"))
.willReturn(ok().withBody("{\"result\": \"false\"}")));
-
try {
- userDenied.runAs(createTable);
- LOG.info("Action runs as expected due to being in dryRun mode");
+ userDenied.runAs(createNamespace);
+ fail("AccessControlException should have been thrown");
} catch (AccessControlException e) {
- throw new AssertionError("AccessControlException should not have been thrown", e);
+ logOk(e);
}
+ }
- tearDown();
+ // --- namespace hooks ---
+
+ @Test
+ public void testPreDeleteNamespace() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preDeleteNamespace(ctx(), "default");
+ return null;
+ });
}
@Test
- public void testUseCache() throws Exception {
- stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
- setup(OpenPolicyAgentAccessController.class, false, OPA_URL, false, true);
+ public void testPreModifyNamespace() throws Exception {
+ NamespaceDescriptor nsd = NamespaceDescriptor.create("default").build();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preModifyNamespace(ctx(), nsd);
+ return null;
+ });
+ }
- User userDenied = User.createUserForTesting(conf, "useCacheUser", new String[0]);
+ @Test
+ public void testPreGetNamespaceDescriptor() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preGetNamespaceDescriptor(ctx(), "default");
+ return null;
+ });
+ }
- // create a table explicitly using the cache from the cp-processor on the master...
- SecureTestUtil.AccessTestAction createTable =
+ // --- table DDL hooks ---
+
+ @Test
+ public void testPreDeleteTable() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preDeleteTable(ctx(), TEST_TABLE);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreEnableTable() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preEnableTable(ctx(), TEST_TABLE);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreDisableTable() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preDisableTable(ctx(), TEST_TABLE);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreTruncateTable() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preTruncateTable(ctx(), TEST_TABLE);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreModifyTable() throws Exception {
+ TableDescriptor td = getHTableDescriptor();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preModifyTable(ctx(), TEST_TABLE, td, td);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreModifyColumnFamilyStoreFileTracker() throws Exception {
+ assertAllowedThenDenied(
() -> {
- HTableDescriptor htd = getHTableDescriptor();
getOpaController()
- .preCreateTable(ObserverContextImpl.createAndPrepare(CP_ENV), htd, null);
+ .preModifyColumnFamilyStoreFileTracker(ctx(), TEST_TABLE, TEST_FAMILY, "FILE");
return null;
- };
+ });
+ }
- try {
- userDenied.runAs(createTable);
- } catch (AccessControlException e) {
- throw new AssertionError("AccessControlException should not have been thrown", e);
- }
+ // --- flush / quota hooks ---
- // we should have only a single entry for this user as subsequent calls will hit the cache
- assertEquals(Optional.of(1L), getOpaController().getAclCacheSize());
+ @Test
+ public void testPreTableFlush() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preTableFlush(ctx(), TEST_TABLE);
+ return null;
+ });
+ }
- tearDown();
+ @Test
+ public void testPreSetUserQuotaTableScope() throws Exception {
+ GlobalQuotaSettings quotas = null;
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSetUserQuota(ctx(), "u", TEST_TABLE, quotas);
+ return null;
+ });
}
@Test
- public void testCreateNamespace() throws Exception {
- stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
- setup(OpenPolicyAgentAccessController.class, false, OPA_URL, false, false);
+ public void testPreSetUserQuotaNamespaceScope() throws Exception {
+ GlobalQuotaSettings quotas = null;
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController()
+ .preSetUserQuota(ctx(), "u", NamespaceDescriptor.DEFAULT_NAMESPACE_NAME_STR, quotas);
+ return null;
+ });
+ }
- User userCreater = User.createUserForTesting(conf, "nsCreator", new String[0]);
- User userDenied = User.createUserForTesting(conf, "nsNonCreator", new String[0]);
+ // --- region assignment / snapshot / quota hooks (existing) ---
- SecureTestUtil.AccessTestAction createNamespace =
+ @Test
+ public void testPreMove() throws Exception {
+ RegionInfo regionInfo = RegionInfoBuilder.newBuilder(TEST_TABLE).build();
+ assertAllowedThenDenied(
() -> {
- NamespaceDescriptor nsd = NamespaceDescriptor.create("new_ns").build();
- getOpaController().preCreateNamespace(ObserverContextImpl.createAndPrepare(CP_ENV), nsd);
+ getOpaController().preMove(ctx(), regionInfo, null, null);
return null;
- };
+ });
+ }
- try {
- userCreater.runAs(createNamespace);
- } catch (AccessControlException e) {
- throw new AssertionError("AccessControlException should not have been thrown", e);
- }
+ @Test
+ public void testPreAssign() throws Exception {
+ RegionInfo regionInfo = RegionInfoBuilder.newBuilder(TEST_TABLE).build();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preAssign(ctx(), regionInfo);
+ return null;
+ });
+ }
- // re-stub so that the call would fail for the given user in *non*-dryRun mode
- stubFor(
- post("/")
- .withRequestBody(matchingJsonPath("$.input.callerUgi[?(@.userName == 'nsNonCreator')]"))
- .willReturn(ok().withBody("{\"result\": \"false\"}")));
+ @Test
+ public void testPreUnassign() throws Exception {
+ RegionInfo regionInfo = RegionInfoBuilder.newBuilder(TEST_TABLE).build();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preUnassign(ctx(), regionInfo);
+ return null;
+ });
+ }
- try {
- userDenied.runAs(createNamespace);
- fail("AccessControlException should have been thrown");
- } catch (AccessControlException e) {
- logOk(e);
- }
+ @Test
+ public void testPreRegionOffline() throws Exception {
+ RegionInfo regionInfo = RegionInfoBuilder.newBuilder(TEST_TABLE).build();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preRegionOffline(ctx(), regionInfo);
+ return null;
+ });
+ }
- tearDown();
+ @Test
+ public void testPreSplitRegion() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSplitRegion(ctx(), TEST_TABLE, null);
+ return null;
+ });
}
- private static void logOk(AccessControlException e) {
- LOG.info("AccessControlException as expected: [{}]", e.getMessage());
+ @Test
+ public void testPreMergeRegions() throws Exception {
+ RegionInfo ri = RegionInfoBuilder.newBuilder(TEST_TABLE).build();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preMergeRegions(ctx(), new RegionInfo[] {ri, ri});
+ return null;
+ });
}
- private static HTableDescriptor getHTableDescriptor() {
- HTableDescriptor htd = new HTableDescriptor(TEST_TABLE);
- HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY);
- hcd.setMaxVersions(100);
- htd.addFamily(hcd);
- htd.setOwner(USER_OWNER);
+ @Test
+ public void testPreModifyTableStoreFileTracker() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preModifyTableStoreFileTracker(ctx(), TEST_TABLE, "FILE");
+ return null;
+ });
+ }
- return htd;
+ @Test
+ public void testPreSnapshot() throws Exception {
+ SnapshotDescription snap = new SnapshotDescription("snap", TEST_TABLE);
+ TableDescriptor td = getHTableDescriptor();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSnapshot(ctx(), snap, td);
+ return null;
+ });
}
- private OpenPolicyAgentAccessController getOpaController() {
- MasterCoprocessorHost masterCpHost =
- TEST_UTIL.getMiniHBaseCluster().getMaster().getMasterCoprocessorHost();
- return masterCpHost.findCoprocessor(OpenPolicyAgentAccessController.class);
+ @Test
+ public void testPreListSnapshot() throws Exception {
+ SnapshotDescription snap = new SnapshotDescription("snap", TEST_TABLE);
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preListSnapshot(ctx(), snap);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCloneSnapshot() throws Exception {
+ SnapshotDescription snap = new SnapshotDescription("snap", TEST_TABLE);
+ TableDescriptor td = getHTableDescriptor();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preCloneSnapshot(ctx(), snap, td);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreRestoreSnapshot() throws Exception {
+ SnapshotDescription snap = new SnapshotDescription("snap", TEST_TABLE);
+ TableDescriptor td = getHTableDescriptor();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preRestoreSnapshot(ctx(), snap, td);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreDeleteSnapshot() throws Exception {
+ SnapshotDescription snap = new SnapshotDescription("snap", TEST_TABLE);
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preDeleteSnapshot(ctx(), snap);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreSetTableQuota() throws Exception {
+ GlobalQuotaSettings quotas = null;
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSetTableQuota(ctx(), TEST_TABLE, quotas);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreSetNamespaceQuota() throws Exception {
+ GlobalQuotaSettings quotas = null;
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController()
+ .preSetNamespaceQuota(ctx(), NamespaceDescriptor.DEFAULT_NAMESPACE_NAME_STR, quotas);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreGetUserPermissionsTableScope() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preGetUserPermissions(ctx(), "u", null, TEST_TABLE, null, null);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreGetUserPermissionsNamespaceScope() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController()
+ .preGetUserPermissions(
+ ctx(), "u", NamespaceDescriptor.DEFAULT_NAMESPACE_NAME_STR, null, null, null);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreBalance() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preBalance(ctx(), BalanceRequest.defaultInstance());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreBalanceSwitch() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preBalanceSwitch(ctx(), true);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreShutdown() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preShutdown(ctx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreStopMaster() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preStopMaster(ctx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreClearDeadServers() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preClearDeadServers(ctx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreDecommissionRegionServers() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preDecommissionRegionServers(ctx(), List.of(), false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreListDecommissionedRegionServers() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preListDecommissionedRegionServers(ctx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreRecommissionRegionServer() throws Exception {
+ ServerName serverName = ServerName.valueOf("localhost", 16010, 12345L);
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preRecommissionRegionServer(ctx(), serverName, List.of());
+ return null;
+ });
+ }
+
+ // --- procedure / lock hooks ---
+
+ @Test
+ public void testPreAbortProcedure() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preAbortProcedure(ctx(), 1L);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreGetProcedures() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preGetProcedures(ctx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreGetLocks() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preGetLocks(ctx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreRequestLockTableScope() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preRequestLock(ctx(), null, TEST_TABLE, null, "desc");
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreRequestLockNamespaceScope() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController()
+ .preRequestLock(
+ ctx(), NamespaceDescriptor.DEFAULT_NAMESPACE_NAME_STR, null, null, "desc");
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreRequestLockRegionScope() throws Exception {
+ RegionInfo ri = RegionInfoBuilder.newBuilder(TEST_TABLE).build();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preRequestLock(ctx(), null, null, new RegionInfo[] {ri}, "desc");
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreLockHeartbeat() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preLockHeartbeat(ctx(), TEST_TABLE, "desc");
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreSetSplitOrMergeEnabled() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSetSplitOrMergeEnabled(ctx(), true, MasterSwitchType.SPLIT);
+ return null;
+ });
+ }
+
+ // --- quota hooks (global scope) ---
+
+ @Test
+ public void testPreSetUserQuotaGlobalScope() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSetUserQuota(ctx(), "u", (GlobalQuotaSettings) null);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreSetRegionServerQuota() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSetRegionServerQuota(ctx(), "rs1", null);
+ return null;
+ });
+ }
+
+ // --- replication peer hooks ---
+
+ @Test
+ public void testPreAddReplicationPeer() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preAddReplicationPeer(ctx(), "peer1", null);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreRemoveReplicationPeer() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preRemoveReplicationPeer(ctx(), "peer1");
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreEnableReplicationPeer() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preEnableReplicationPeer(ctx(), "peer1");
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreDisableReplicationPeer() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preDisableReplicationPeer(ctx(), "peer1");
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreGetReplicationPeerConfig() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preGetReplicationPeerConfig(ctx(), "peer1");
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreUpdateReplicationPeerConfig() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preUpdateReplicationPeerConfig(ctx(), "peer1", null);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreListReplicationPeers() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preListReplicationPeers(ctx(), ".*");
+ return null;
+ });
+ }
+
+ // --- throttle hooks ---
+
+ @Test
+ public void testPreSwitchRpcThrottle() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSwitchRpcThrottle(ctx(), true);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreIsRpcThrottleEnabled() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preIsRpcThrottleEnabled(ctx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreSwitchExceedThrottleQuota() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preSwitchExceedThrottleQuota(ctx(), true);
+ return null;
+ });
+ }
+
+ // --- configuration hooks ---
+
+ @Test
+ public void testPreUpdateMasterConfiguration() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preUpdateMasterConfiguration(ctx(), conf);
+ return null;
+ });
}
}
diff --git a/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessControllerRegion.java b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessControllerRegion.java
new file mode 100644
index 0000000..01f3690
--- /dev/null
+++ b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessControllerRegion.java
@@ -0,0 +1,481 @@
+package tech.stackable.hbase;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static org.apache.hadoop.hbase.security.access.SecureTestUtil.createTable;
+import static org.apache.hadoop.hbase.security.access.SecureTestUtil.deleteTable;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import java.util.Collections;
+import org.apache.hadoop.hbase.CompareOperator;
+import org.apache.hadoop.hbase.Coprocessor;
+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.Get;
+import org.apache.hadoop.hbase.client.Increment;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.RowMutations;
+import org.apache.hadoop.hbase.client.Scan;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.ObserverContextImpl;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
+import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessorEnvironment;
+import org.apache.hadoop.hbase.filter.Filter;
+import org.apache.hadoop.hbase.regionserver.HRegion;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
+import org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost;
+import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost;
+import org.apache.hadoop.hbase.regionserver.ScanType;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class TestOpenPolicyAgentAccessControllerRegion extends TestUtils {
+ public static final String OPA_URL = "http://localhost:8089";
+
+ private static final byte[] TEST_ROW = Bytes.toBytes("testRow");
+ private static RegionCoprocessorEnvironment REGION_CP_ENV;
+ private static RegionServerCoprocessorEnvironment RS_CP_ENV;
+
+ @ClassRule public static WireMockRule wireMockRule = new WireMockRule(8089);
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ wireMockRule.addMockServiceRequestListener(OpaFixtureWriter::capture);
+ stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
+ setup(OpenPolicyAgentAccessController.class, false, OPA_URL);
+
+ createTable(
+ TEST_UTIL, TEST_UTIL.getAdmin(), getHTableDescriptor(), new byte[][] {Bytes.toBytes("s")});
+
+ HRegion region = TEST_UTIL.getHBaseCluster().getRegions(TEST_TABLE).get(0);
+ RegionCoprocessorHost rcpHost = region.getCoprocessorHost();
+ OpenPolicyAgentAccessController regionController =
+ rcpHost.findCoprocessor(OpenPolicyAgentAccessController.class);
+ REGION_CP_ENV =
+ (RegionCoprocessorEnvironment)
+ rcpHost.createEnvironment(regionController, Coprocessor.PRIORITY_HIGHEST, 1, conf);
+
+ HRegionServer rs = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
+ RegionServerCoprocessorHost rsCpHost = rs.getRegionServerCoprocessorHost();
+ OpenPolicyAgentAccessController rsController =
+ rsCpHost.findCoprocessor(OpenPolicyAgentAccessController.class);
+ RS_CP_ENV =
+ (RegionServerCoprocessorEnvironment)
+ rsCpHost.createEnvironment(rsController, Coprocessor.PRIORITY_HIGHEST, 1, conf);
+ }
+
+ @Before
+ public void resetStubs() {
+ WireMock.reset();
+ stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws Exception {
+ deleteTable(TEST_UTIL, TEST_TABLE);
+ tearDown();
+ }
+
+ // --- helpers ---
+
+ private ObserverContext regionCtx() {
+ return ObserverContextImpl.createAndPrepare(REGION_CP_ENV);
+ }
+
+ private ObserverContext rsCtx() {
+ return ObserverContextImpl.createAndPrepare(RS_CP_ENV);
+ }
+
+ private OpenPolicyAgentAccessController getRegionController() {
+ HRegion region = TEST_UTIL.getHBaseCluster().getRegions(TEST_TABLE).get(0);
+ return region.getCoprocessorHost().findCoprocessor(OpenPolicyAgentAccessController.class);
+ }
+
+ // --- read hooks ---
+
+ @Test
+ public void testPreGetOp() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preGetOp(regionCtx(), new Get(TEST_ROW), null);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreExists() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preExists(regionCtx(), new Get(TEST_ROW), false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreScannerOpen() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preScannerOpen(regionCtx(), new Scan());
+ return null;
+ });
+ }
+
+ // --- write hooks ---
+
+ @Test
+ public void testPrePut() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .prePut(regionCtx(), new Put(TEST_ROW), null, Durability.USE_DEFAULT);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreDelete() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .preDelete(regionCtx(), new Delete(TEST_ROW), null, Durability.USE_DEFAULT);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreBatchMutate() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preBatchMutate(regionCtx(), null);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreFlush() throws Exception {
+ // preFlush is an internal storage engine hook; no authorization check is applied.
+ getRegionController().preFlush(regionCtx(), null);
+ }
+
+ @Test
+ public void testPreCompact() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preCompact(regionCtx(), null, null, ScanType.USER_SCAN, null, null);
+ return null;
+ });
+ }
+
+ // --- read+write hooks (require WRITE or READ) ---
+
+ @Test
+ public void testPreAppend() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preAppend(regionCtx(), new Append(TEST_ROW));
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreIncrement() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preIncrement(regionCtx(), new Increment(TEST_ROW));
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndPut() throws Exception {
+ Put put = new Put(TEST_ROW);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .preCheckAndPut(
+ regionCtx(),
+ TEST_ROW,
+ TEST_FAMILY,
+ TEST_QUALIFIER,
+ CompareOperator.EQUAL,
+ null,
+ put,
+ false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndPutAfterRowLock() throws Exception {
+ Put put = new Put(TEST_ROW);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .preCheckAndPutAfterRowLock(
+ regionCtx(),
+ TEST_ROW,
+ TEST_FAMILY,
+ TEST_QUALIFIER,
+ CompareOperator.EQUAL,
+ null,
+ put,
+ false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndDelete() throws Exception {
+ Delete delete = new Delete(TEST_ROW);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .preCheckAndDelete(
+ regionCtx(),
+ TEST_ROW,
+ TEST_FAMILY,
+ TEST_QUALIFIER,
+ CompareOperator.EQUAL,
+ null,
+ delete,
+ false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndDeleteAfterRowLock() throws Exception {
+ Delete delete = new Delete(TEST_ROW);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .preCheckAndDeleteAfterRowLock(
+ regionCtx(),
+ TEST_ROW,
+ TEST_FAMILY,
+ TEST_QUALIFIER,
+ CompareOperator.EQUAL,
+ null,
+ delete,
+ false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndPutWithFilter() throws Exception {
+ Put put = new Put(TEST_ROW);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preCheckAndPut(regionCtx(), TEST_ROW, (Filter) null, put, false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndPutAfterRowLockWithFilter() throws Exception {
+ Put put = new Put(TEST_ROW);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .preCheckAndPutAfterRowLock(regionCtx(), TEST_ROW, (Filter) null, put, false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndDeleteWithFilter() throws Exception {
+ Delete delete = new Delete(TEST_ROW);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .preCheckAndDelete(regionCtx(), TEST_ROW, (Filter) null, delete, false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndDeleteAfterRowLockWithFilter() throws Exception {
+ Delete delete = new Delete(TEST_ROW);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController()
+ .preCheckAndDeleteAfterRowLock(regionCtx(), TEST_ROW, (Filter) null, delete, false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndMutateWithRowMutations() throws Exception {
+ RowMutations rowMutations = new RowMutations(TEST_ROW);
+ rowMutations.add(new Put(TEST_ROW));
+ CheckAndMutate checkAndMutate =
+ CheckAndMutate.newBuilder(TEST_ROW)
+ .ifNotExists(TEST_FAMILY, TEST_QUALIFIER)
+ .build(rowMutations);
+ CheckAndMutateResult result = new CheckAndMutateResult(true, null);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preCheckAndMutate(regionCtx(), checkAndMutate, result);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCheckAndMutateAfterRowLockWithRowMutations() throws Exception {
+ RowMutations rowMutations = new RowMutations(TEST_ROW);
+ rowMutations.add(new Put(TEST_ROW));
+ CheckAndMutate checkAndMutate =
+ CheckAndMutate.newBuilder(TEST_ROW)
+ .ifNotExists(TEST_FAMILY, TEST_QUALIFIER)
+ .build(rowMutations);
+ CheckAndMutateResult result = new CheckAndMutateResult(true, null);
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preCheckAndMutateAfterRowLock(regionCtx(), checkAndMutate, result);
+ return null;
+ });
+ }
+
+ // --- bulk load hooks ---
+
+ @Test
+ public void testPreBulkLoadHFile() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preBulkLoadHFile(regionCtx(), Collections.emptyList());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPrePrepareBulkLoad() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().prePrepareBulkLoad(regionCtx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreCleanupBulkLoad() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRegionController().preCleanupBulkLoad(regionCtx());
+ return null;
+ });
+ }
+
+ // --- OPA fixture coverage: readonlyUser principal ---
+ // These tests generate fixtures for the readonlyuser Kerberos principal (which has an ACL with
+ // operations and families restrictions in the Rego policy), exercising the matches_operation and
+ // matches_families non-null branches that the allowedUser/deniedUser fixtures never reach.
+
+ @Test
+ public void testReadonlyUserScanAllowed() throws Exception {
+ assertReadonlyUserAllowed(
+ () -> {
+ getRegionController().preScannerOpen(regionCtx(), new Scan());
+ return null;
+ });
+ }
+
+ @Test
+ public void testReadonlyUserGetAllowed() throws Exception {
+ assertReadonlyUserAllowed(
+ () -> {
+ getRegionController().preGetOp(regionCtx(), new Get(TEST_ROW), null);
+ return null;
+ });
+ }
+
+ @Test
+ public void testReadonlyUserExistsAllowed() throws Exception {
+ assertReadonlyUserAllowed(
+ () -> {
+ getRegionController().preExists(regionCtx(), new Get(TEST_ROW), false);
+ return null;
+ });
+ }
+
+ @Test
+ public void testReadonlyUserPutDenied() throws Exception {
+ assertReadonlyUserDenied(
+ () -> {
+ getRegionController()
+ .prePut(regionCtx(), new Put(TEST_ROW), null, Durability.USE_DEFAULT);
+ return null;
+ });
+ }
+
+ // --- RegionServer hooks ---
+
+ private OpenPolicyAgentAccessController getRsController() {
+ HRegionServer rs = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
+ return rs.getRegionServerCoprocessorHost()
+ .findCoprocessor(OpenPolicyAgentAccessController.class);
+ }
+
+ @Test
+ public void testPreRollWALWriterRequest() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRsController().preRollWALWriterRequest(rsCtx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreReplicateLogEntries() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRsController().preReplicateLogEntries(rsCtx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreClearCompactionQueues() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRsController().preClearCompactionQueues(rsCtx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreClearRegionBlockCache() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRsController().preClearRegionBlockCache(rsCtx());
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreUpdateRegionServerConfiguration() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRsController().preUpdateRegionServerConfiguration(rsCtx(), conf);
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreStopRegionServer() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getRsController().preStopRegionServer(rsCtx());
+ return null;
+ });
+ }
+}
diff --git a/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessControllerVariants.java b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessControllerVariants.java
new file mode 100644
index 0000000..0911e6c
--- /dev/null
+++ b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessControllerVariants.java
@@ -0,0 +1,99 @@
+package tech.stackable.hbase;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath;
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static org.junit.Assert.assertEquals;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import java.util.Optional;
+import org.apache.hadoop.hbase.HTableDescriptor;
+import org.apache.hadoop.hbase.coprocessor.ObserverContextImpl;
+import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
+import org.apache.hadoop.hbase.security.User;
+import org.apache.hadoop.hbase.security.access.SecureTestUtil;
+import org.apache.hadoop.security.AccessControlException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Tests for non-default coprocessor configurations (dryRun, cache). Each test manages its own
+ * mini-cluster lifecycle since the coprocessor config differs per test.
+ */
+public class TestOpenPolicyAgentAccessControllerVariants extends TestUtils {
+ public static final String OPA_URL = "http://localhost:8089";
+
+ // @Rule (not @ClassRule) because each test starts and tears down its own mini-cluster
+ // (setup/tearDown are called inside the test body, not in @BeforeClass/@AfterClass).
+ @Rule public WireMockRule wireMockRule = new WireMockRule(8089);
+
+ @Before
+ public void registerOpaListener() {
+ wireMockRule.addMockServiceRequestListener(OpaFixtureWriter::capture);
+ }
+
+ @Test
+ public void testDryRun() throws Exception {
+ stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
+ setup(OpenPolicyAgentAccessController.class, false, OPA_URL, true, false);
+
+ User userDenied = User.createUserForTesting(conf, "cannotCreateTables", new String[0]);
+
+ SecureTestUtil.AccessTestAction createTable =
+ () -> {
+ HTableDescriptor htd = getHTableDescriptor();
+ getOpaController()
+ .preCreateTable(ObserverContextImpl.createAndPrepare(CP_ENV), htd, null);
+ return null;
+ };
+
+ stubFor(
+ post("/")
+ .withRequestBody(
+ matchingJsonPath("$.input.callerUgi[?(@.userName == 'cannotCreateTables')]"))
+ .willReturn(ok().withBody("{\"result\": \"false\"}")));
+
+ try {
+ userDenied.runAs(createTable);
+ LOG.info("Action runs as expected due to being in dryRun mode");
+ } catch (AccessControlException e) {
+ throw new AssertionError("AccessControlException should not have been thrown", e);
+ }
+
+ tearDown();
+ }
+
+ @Test
+ public void testUseCache() throws Exception {
+ stubFor(post("/").willReturn(ok().withBody("{\"result\": \"true\"}")));
+ setup(OpenPolicyAgentAccessController.class, false, OPA_URL, false, true);
+
+ User userDenied = User.createUserForTesting(conf, "useCacheUser", new String[0]);
+
+ SecureTestUtil.AccessTestAction createTable =
+ () -> {
+ HTableDescriptor htd = getHTableDescriptor();
+ getOpaController()
+ .preCreateTable(ObserverContextImpl.createAndPrepare(CP_ENV), htd, null);
+ return null;
+ };
+
+ try {
+ userDenied.runAs(createTable);
+ } catch (AccessControlException e) {
+ throw new AssertionError("AccessControlException should not have been thrown", e);
+ }
+
+ assertEquals(Optional.of(1L), getOpaController().getAclCacheSize());
+
+ tearDown();
+ }
+
+ private OpenPolicyAgentAccessController getOpaController() {
+ MasterCoprocessorHost masterCpHost =
+ TEST_UTIL.getMiniHBaseCluster().getMaster().getMasterCoprocessorHost();
+ return masterCpHost.findCoprocessor(OpenPolicyAgentAccessController.class);
+ }
+}
diff --git a/src/test/java/tech/stackable/hbase/TestUtils.java b/src/test/java/tech/stackable/hbase/TestUtils.java
index 54e476d..8011c1c 100644
--- a/src/test/java/tech/stackable/hbase/TestUtils.java
+++ b/src/test/java/tech/stackable/hbase/TestUtils.java
@@ -1,5 +1,9 @@
package tech.stackable.hbase;
+import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath;
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static org.apache.hadoop.hbase.AuthUtil.toGroupEntry;
import static org.apache.hadoop.hbase.security.access.SecureTestUtil.*;
import static org.junit.Assert.*;
@@ -26,6 +30,7 @@
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.security.access.*;
import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.security.UserGroupInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -59,6 +64,10 @@ public class TestUtils {
protected static final String GROUP_READ = "group_read";
protected static final String GROUP_WRITE = "group_write";
+ protected static User ALLOWED_USER;
+ protected static User DENIED_USER;
+ protected static User READONLY_USER;
+
protected static User USER_GROUP_ADMIN;
protected static User USER_GROUP_CREATE;
protected static User USER_GROUP_READ;
@@ -158,6 +167,10 @@ protected static void setup(
User.createUserForTesting(conf, "user_group_write", new String[] {GROUP_WRITE});
systemUserConnection = TEST_UTIL.getConnection();
+
+ ALLOWED_USER = User.createUserForTesting(conf, "allowedUser", new String[0]);
+ DENIED_USER = User.createUserForTesting(conf, "deniedUser", new String[0]);
+ READONLY_USER = User.createUserForTesting(conf, "readonlyUser", new String[0]);
}
protected static void setUpTables() throws Exception {
@@ -232,7 +245,44 @@ protected static void setUpTables() throws Exception {
assertEquals(5, size);
}
+ protected static void logOk(AccessControlException e) {
+ LOG.info("AccessControlException as expected: [{}]", e.getMessage());
+ }
+
+ protected void assertReadonlyUserAllowed(SecureTestUtil.AccessTestAction action)
+ throws Exception {
+ READONLY_USER.runAs(action);
+ }
+
+ protected void assertReadonlyUserDenied(SecureTestUtil.AccessTestAction action) throws Exception {
+ stubFor(
+ post("/")
+ .withRequestBody(matchingJsonPath("$.input.callerUgi[?(@.userName == 'readonlyUser')]"))
+ .willReturn(ok().withBody("{\"result\": \"false\"}")));
+ try {
+ READONLY_USER.runAs(action);
+ fail("AccessControlException should have been thrown");
+ } catch (AccessControlException e) {
+ logOk(e);
+ }
+ }
+
+ protected void assertAllowedThenDenied(SecureTestUtil.AccessTestAction action) throws Exception {
+ ALLOWED_USER.runAs(action);
+ stubFor(
+ post("/")
+ .withRequestBody(matchingJsonPath("$.input.callerUgi[?(@.userName == 'deniedUser')]"))
+ .willReturn(ok().withBody("{\"result\": \"false\"}")));
+ try {
+ DENIED_USER.runAs(action);
+ fail("AccessControlException should have been thrown");
+ } catch (AccessControlException e) {
+ logOk(e);
+ }
+ }
+
protected static void tearDown() throws Exception {
+ OpaFixtureWriter.flush();
TEST_UTIL.shutdownMiniCluster();
}
@@ -537,4 +587,13 @@ protected void createTestTable(TableName tname, byte[] cf) throws Exception {
htd.setOwner(USER_OWNER);
createTable(TEST_UTIL, TEST_UTIL.getAdmin(), htd, new byte[][] {Bytes.toBytes("s")});
}
+
+ protected static HTableDescriptor getHTableDescriptor() {
+ HTableDescriptor htd = new HTableDescriptor(TEST_TABLE);
+ HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY);
+ hcd.setMaxVersions(100);
+ htd.addFamily(hcd);
+ htd.setOwner(USER_OWNER);
+ return htd;
+ }
}
diff --git a/src/test/rego/hbase.rego b/src/test/rego/hbase.rego
new file mode 100644
index 0000000..14eba41
--- /dev/null
+++ b/src/test/rego/hbase.rego
@@ -0,0 +1,143 @@
+# Derived from hbase-operator/tests/templates/kuttl/opa/12-rego-rules.txt.j2
+# with $NAMESPACE replaced by "test-ns". Regenerate with:
+# tail -n +10 /12-rego-rules.txt.j2 | sed 's/^ //' | sed 's/\$NAMESPACE/test-ns/g'
+#
+package hbase
+
+default allow := false
+default matches_identity(identity) := false
+
+# table is null if the request is for namespace permissions, but as parameters cannot be
+# undefined, we have to set it to something specific:
+checked_table_name := input.table.qualifierAsString if {input.table.qualifierAsString}
+checked_table_name := "__undefined__" if {not input.table.qualifierAsString}
+
+allow if {
+ some acl in acls
+ matches_identity(acl.identity)
+ matches_resource(input.namespace, checked_table_name, acl.resource)
+ action_sufficient_for_operation(acl.action, input.action)
+ matches_operation(acl, input.operation)
+ matches_families(acl, input.families)
+}
+
+# Identity mentions the (long) userName explicitly
+matches_identity(identity) if {
+ identity in {
+ concat("", ["user:", input.callerUgi.userName])
+ }
+}
+
+# Identity regex matches the (long) userName
+matches_identity(identity) if {
+ match_entire(identity, concat("", ["userRegex:", input.callerUgi.userName]))
+}
+
+# Identity mentions group the user is part of (by looking up using the (long) userName)
+matches_identity(identity) if {
+ some group in groups_for_user[input.callerUgi.userName]
+ identity == concat("", ["group:", group])
+}
+
+# Allow all resources
+matches_resource(namespace, table, resource) if {
+ resource == "hbase:"
+}
+
+# Allow all namespaces
+matches_resource(namespace, table, resource) if {
+ resource == "hbase:namespace:"
+}
+
+# Resource mentions the namespace explicitly
+matches_resource(namespace, table, resource) if {
+ resource == concat(":", ["hbase:namespace", namespace])
+}
+
+# Resource mentions the namespaced table explicitly
+matches_resource(namespace, table, resource) if {
+ resource == concat("", ["hbase:table:", namespace, "/", table])
+}
+
+match_entire(pattern, value) if {
+ # Add the anchors ^ and $
+ pattern_with_anchors := concat("", ["^", pattern, "$"])
+
+ regex.match(pattern_with_anchors, value)
+}
+
+action_sufficient_for_operation(action, operation) if {
+ action_hierarchy[action][_] == action_for_operation[operation]
+}
+
+action_hierarchy := {
+ "full": ["full", "rw", "ro"],
+ "rw": ["rw", "ro"],
+ "ro": ["ro"],
+}
+
+action_for_operation := {
+ "ADMIN": "full",
+ "CREATE": "full",
+ "WRITE": "rw",
+ "READ": "ro",
+ "EXEC": "full",
+}
+
+# If the ACL does not restrict operations, all operation types are permitted.
+matches_operation(acl, _) if {
+ not acl.operations
+}
+
+# If the ACL restricts operations, the requested OpType must be in the permitted set.
+matches_operation(acl, operation) if {
+ acl.operations
+ operation in acl.operations
+}
+
+# If the ACL does not restrict families, all column families are permitted.
+matches_families(acl, _) if {
+ not acl.families
+}
+
+# If the ACL restricts families, every family present in the request must be allowed.
+# An empty families map (namespace-scoped or unfiltered scan) always passes.
+matches_families(acl, families) if {
+ acl.families
+ count({f | f := object.keys(families)[_]; not f in acl.families}) == 0
+}
+
+groups_for_user := {
+ "hbase/hbase.test-ns.svc.cluster.local@CLUSTER.LOCAL": ["admins"],
+ "admin/access-hbase.test-ns.svc.cluster.local@CLUSTER.LOCAL": ["admins"],
+ "developer/access-hbase.test-ns.svc.cluster.local@CLUSTER.LOCAL": ["developers"],
+ "public/access-hbase.test-ns.svc.cluster.local@CLUSTER.LOCAL": ["public"],
+ "readonlyuser/access-hbase.test-ns.svc.cluster.local@CLUSTER.LOCAL": [],
+}
+
+acls := [
+ {
+ "identity": "group:admins",
+ "action": "full",
+ "resource": "hbase:",
+ },
+ {
+ "identity": "group:developers",
+ "action": "full",
+ "resource": "hbase:namespace:developers",
+ },
+ {
+ "identity": "group:public",
+ "action": "full",
+ "resource": "hbase:namespace:public",
+ },
+ {
+ "identity": "user:readonlyuser/access-hbase.test-ns.svc.cluster.local@CLUSTER.LOCAL",
+ "action": "ro",
+ "resource": "hbase:namespace:",
+ # Restrict to read-only operation types; exercises matches_operation non-null branch.
+ "operations": ["EXISTS", "GET", "SCAN", "NONE"],
+ # Restrict to known column families; exercises matches_families non-null branch.
+ "families": ["cf1"],
+ },
+]
diff --git a/src/test/rego/hbase_test.rego b/src/test/rego/hbase_test.rego
new file mode 100644
index 0000000..ae4c40d
--- /dev/null
+++ b/src/test/rego/hbase_test.rego
@@ -0,0 +1,19 @@
+package hbase_test
+
+import data.hbase
+
+# Every fixture in allowed must produce allow=true under the real Rego policy.
+test_all_allowed_inputs if {
+ print("allowed fixtures:", count(data.fixtures.allowed))
+ every fixture in data.fixtures.allowed {
+ hbase.allow with input as fixture.input
+ }
+}
+
+# Every fixture in denied must produce allow=false under the real Rego policy.
+test_all_denied_inputs if {
+ print("denied fixtures:", count(data.fixtures.denied))
+ every fixture in data.fixtures.denied {
+ not hbase.allow with input as fixture.input
+ }
+}