> families) {
+ this.callerUgi = new OpaQueryUgi(ugi);
this.table = table;
this.action = action;
this.namespace = table.getNamespaceAsString();
+ this.operation = operation;
+ this.families = families;
}
public OpaAllowQueryInput(
UserGroupInformation ugi, String namespace, Permission.Action action) {
this.callerUgi = new OpaQueryUgi(ugi);
-
this.table = null;
this.action = action;
this.namespace = namespace;
+ this.operation = OpType.NONE;
+ this.families = Collections.emptyMap();
}
}
}
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..c00959f
--- /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; the implementation gaps are tracked separately in plan.md.
+ //
+ // 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..bfabe2c 100644
--- a/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessController.java
+++ b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessController.java
@@ -6,44 +6,81 @@
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 {
+ 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 +91,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 +105,574 @@ 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;
+ });
+ }
+
+ @Test
+ public void testPreMergeRegions() throws Exception {
+ RegionInfo ri = RegionInfoBuilder.newBuilder(TEST_TABLE).build();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preMergeRegions(ctx(), new RegionInfo[] {ri, ri});
+ return null;
+ });
+ }
+
+ @Test
+ public void testPreModifyTableStoreFileTracker() throws Exception {
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preModifyTableStoreFileTracker(ctx(), TEST_TABLE, "FILE");
+ return null;
+ });
}
- private static void logOk(AccessControlException e) {
- LOG.info("AccessControlException as expected: [{}]", e.getMessage());
+ @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 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 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;
+ });
+ }
- return htd;
+ @Test
+ public void testPreRestoreSnapshot() throws Exception {
+ SnapshotDescription snap = new SnapshotDescription("snap", TEST_TABLE);
+ TableDescriptor td = getHTableDescriptor();
+ assertAllowedThenDenied(
+ () -> {
+ getOpaController().preRestoreSnapshot(ctx(), snap, td);
+ return null;
+ });
}
- private OpenPolicyAgentAccessController getOpaController() {
- MasterCoprocessorHost masterCpHost =
- TEST_UTIL.getMiniHBaseCluster().getMaster().getMasterCoprocessorHost();
- return masterCpHost.findCoprocessor(OpenPolicyAgentAccessController.class);
+ @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 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..cccadbe
--- /dev/null
+++ b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessControllerRegion.java
@@ -0,0 +1,408 @@
+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 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 {
+ 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;
+ });
+ }
+
+ // --- 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..6d19bde
--- /dev/null
+++ b/src/test/java/tech/stackable/hbase/TestOpenPolicyAgentAccessControllerVariants.java
@@ -0,0 +1,91 @@
+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.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 public WireMockRule wireMockRule = new WireMockRule(8089);
+
+ @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..2cf25a1 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,9 @@ 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 USER_GROUP_ADMIN;
protected static User USER_GROUP_CREATE;
protected static User USER_GROUP_READ;
@@ -158,6 +166,9 @@ 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]);
}
protected static void setUpTables() throws Exception {
@@ -232,6 +243,24 @@ protected static void setUpTables() throws Exception {
assertEquals(5, size);
}
+ protected static void logOk(AccessControlException e) {
+ LOG.info("AccessControlException as expected: [{}]", e.getMessage());
+ }
+
+ 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 {
TEST_UTIL.shutdownMiniCluster();
}
@@ -537,4 +566,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;
+ }
}