diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java index 7ac60371db2e..2b9149be9e05 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java @@ -2669,5 +2669,23 @@ List getLogEntries(Set serverNames, String logType, Server * Refresh the system key cache on all specified region servers. * @param regionServers the list of region servers to refresh the system key cache on */ - void refreshSystemKeyCacheOnServers(Set regionServers) throws IOException; + void refreshSystemKeyCacheOnServers(List regionServers) throws IOException; + + /** + * Eject a specific managed key entry from the managed key data cache on all specified region + * servers. + * @param regionServers the list of region servers to eject the managed key entry from + * @param keyCustodian the key custodian + * @param keyNamespace the key namespace + * @param keyMetadata the key metadata + */ + void ejectManagedKeyDataCacheEntryOnServers(List regionServers, byte[] keyCustodian, + String keyNamespace, String keyMetadata) throws IOException; + + /** + * Clear all entries in the managed key data cache on all specified region servers without having + * to restart the process. + * @param regionServers the list of region servers to clear the managed key data cache on + */ + void clearManagedKeyDataCacheOnServers(List regionServers) throws IOException; } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AdminOverAsyncAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AdminOverAsyncAdmin.java index 89233cabedca..42c1edb4c52e 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AdminOverAsyncAdmin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AdminOverAsyncAdmin.java @@ -1148,7 +1148,19 @@ public void restoreBackupSystemTable(String snapshotName) throws IOException { } @Override - public void refreshSystemKeyCacheOnServers(Set regionServers) throws IOException { + public void refreshSystemKeyCacheOnServers(List regionServers) throws IOException { get(admin.refreshSystemKeyCacheOnServers(regionServers)); } + + @Override + public void ejectManagedKeyDataCacheEntryOnServers(List regionServers, + byte[] keyCustodian, String keyNamespace, String keyMetadata) throws IOException { + get(admin.ejectManagedKeyDataCacheEntryOnServers(regionServers, keyCustodian, keyNamespace, + keyMetadata)); + } + + @Override + public void clearManagedKeyDataCacheOnServers(List regionServers) throws IOException { + get(admin.clearManagedKeyDataCacheOnServers(regionServers)); + } } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java index dc2bb37153ef..3c1f90f5bc40 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java @@ -1879,5 +1879,22 @@ CompletableFuture> getLogEntries(Set serverNames, Str * Refresh the system key cache on all specified region servers. * @param regionServers the list of region servers to refresh the system key cache on */ - CompletableFuture refreshSystemKeyCacheOnServers(Set regionServers); + CompletableFuture refreshSystemKeyCacheOnServers(List regionServers); + + /** + * Eject a specific managed key entry from the managed key data cache on all specified region + * servers. + * @param regionServers the list of region servers to eject the managed key entry from + * @param keyCustodian the key custodian + * @param keyNamespace the key namespace + * @param keyMetadata the key metadata + */ + CompletableFuture ejectManagedKeyDataCacheEntryOnServers(List regionServers, + byte[] keyCustodian, String keyNamespace, String keyMetadata); + + /** + * Clear all entries in the managed key data cache on all specified region servers. + * @param regionServers the list of region servers to clear the managed key data cache on + */ + CompletableFuture clearManagedKeyDataCacheOnServers(List regionServers); } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncHBaseAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncHBaseAdmin.java index ac509c20be91..dc54b5880bea 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncHBaseAdmin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncHBaseAdmin.java @@ -687,10 +687,22 @@ public CompletableFuture updateConfiguration(String groupName) { } @Override - public CompletableFuture refreshSystemKeyCacheOnServers(Set regionServers) { + public CompletableFuture refreshSystemKeyCacheOnServers(List regionServers) { return wrap(rawAdmin.refreshSystemKeyCacheOnServers(regionServers)); } + @Override + public CompletableFuture ejectManagedKeyDataCacheEntryOnServers( + List regionServers, byte[] keyCustodian, String keyNamespace, String keyMetadata) { + return wrap(rawAdmin.ejectManagedKeyDataCacheEntryOnServers(regionServers, keyCustodian, + keyNamespace, keyMetadata)); + } + + @Override + public CompletableFuture clearManagedKeyDataCacheOnServers(List regionServers) { + return wrap(rawAdmin.clearManagedKeyDataCacheOnServers(regionServers)); + } + @Override public CompletableFuture rollWALWriter(ServerName serverName) { return wrap(rawAdmin.rollWALWriter(serverName)); diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java index 0af2625bb3b1..ce967b86bec4 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java @@ -76,6 +76,7 @@ import org.apache.hadoop.hbase.client.replication.TableCFs; import org.apache.hadoop.hbase.client.security.SecurityCapability; import org.apache.hadoop.hbase.exceptions.DeserializationException; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; import org.apache.hadoop.hbase.ipc.HBaseRpcController; import org.apache.hadoop.hbase.net.Address; import org.apache.hadoop.hbase.quotas.QuotaFilter; @@ -152,6 +153,8 @@ import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.LastHighestWalFilenum; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyRequest; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.NameStringPair; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ProcedureDescription; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.RegionSpecifier.RegionSpecifierType; @@ -4665,7 +4668,7 @@ MasterProtos.RestoreBackupSystemTableResponse> procedureCall(request, } @Override - public CompletableFuture refreshSystemKeyCacheOnServers(Set regionServers) { + public CompletableFuture refreshSystemKeyCacheOnServers(List regionServers) { CompletableFuture future = new CompletableFuture<>(); List> futures = regionServers.stream().map(this::refreshSystemKeyCache).collect(Collectors.toList()); @@ -4687,4 +4690,62 @@ private CompletableFuture refreshSystemKeyCache(ServerName serverName) { (s, c, req, done) -> s.refreshSystemKeyCache(controller, req, done), resp -> null)) .serverName(serverName).call(); } + + @Override + public CompletableFuture ejectManagedKeyDataCacheEntryOnServers( + List regionServers, byte[] keyCustodian, String keyNamespace, String keyMetadata) { + CompletableFuture future = new CompletableFuture<>(); + // Create the request once instead of repeatedly for each server + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash(keyMetadata); + ManagedKeyEntryRequest request = ManagedKeyEntryRequest.newBuilder() + .setKeyCustNs(ManagedKeyRequest.newBuilder().setKeyCust(ByteString.copyFrom(keyCustodian)) + .setKeyNamespace(keyNamespace).build()) + .setKeyMetadataHash(ByteString.copyFrom(keyMetadataHash)).build(); + List> futures = + regionServers.stream().map(serverName -> ejectManagedKeyDataCacheEntry(serverName, request)) + .collect(Collectors.toList()); + addListener(CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])), + (result, err) -> { + if (err != null) { + future.completeExceptionally(err); + } else { + future.complete(result); + } + }); + return future; + } + + private CompletableFuture ejectManagedKeyDataCacheEntry(ServerName serverName, + ManagedKeyEntryRequest request) { + return this. newAdminCaller() + .action((controller, stub) -> this. adminCall(controller, stub, request, + (s, c, req, done) -> s.ejectManagedKeyDataCacheEntry(controller, req, done), + resp -> null)) + .serverName(serverName).call(); + } + + @Override + public CompletableFuture clearManagedKeyDataCacheOnServers(List regionServers) { + CompletableFuture future = new CompletableFuture<>(); + List> futures = + regionServers.stream().map(this::clearManagedKeyDataCache).collect(Collectors.toList()); + addListener(CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])), + (result, err) -> { + if (err != null) { + future.completeExceptionally(err); + } else { + future.complete(result); + } + }); + return future; + } + + private CompletableFuture clearManagedKeyDataCache(ServerName serverName) { + return this. newAdminCaller() + .action((controller, stub) -> this. adminCall(controller, stub, + EmptyMsg.getDefaultInstance(), + (s, c, req, done) -> s.clearManagedKeyDataCache(controller, req, done), resp -> null)) + .serverName(serverName).call(); + } } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdminClient.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdminClient.java index 01a5574443d5..8c60e1a8b292 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdminClient.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdminClient.java @@ -21,19 +21,23 @@ import java.security.KeyException; import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.NotImplementedException; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; import org.apache.hadoop.hbase.io.crypto.ManagedKeyState; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeyRequest; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeyResponse; import org.apache.yetus.audience.InterfaceAudience; import org.apache.hbase.thirdparty.com.google.protobuf.ByteString; import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException; import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BooleanMsg; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.GetManagedKeysResponse; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyRequest; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyResponse; +import org.apache.hadoop.hbase.shaded.protobuf.generated.ManagedKeysProtos; @InterfaceAudience.Public public class KeymetaAdminClient implements KeymetaAdmin { @@ -48,9 +52,8 @@ public KeymetaAdminClient(Connection conn) throws IOException { public ManagedKeyData enableKeyManagement(byte[] keyCust, String keyNamespace) throws IOException { try { - ManagedKeysProtos.ManagedKeyResponse response = - stub.enableKeyManagement(null, ManagedKeyRequest.newBuilder() - .setKeyCust(ByteString.copyFrom(keyCust)).setKeyNamespace(keyNamespace).build()); + ManagedKeyResponse response = stub.enableKeyManagement(null, ManagedKeyRequest.newBuilder() + .setKeyCust(ByteString.copyFrom(keyCust)).setKeyNamespace(keyNamespace).build()); return generateKeyData(response); } catch (ServiceException e) { throw ProtobufUtil.handleRemoteException(e); @@ -61,7 +64,7 @@ public ManagedKeyData enableKeyManagement(byte[] keyCust, String keyNamespace) public List getManagedKeys(byte[] keyCust, String keyNamespace) throws IOException, KeyException { try { - ManagedKeysProtos.GetManagedKeysResponse statusResponse = + GetManagedKeysResponse statusResponse = stub.getManagedKeys(null, ManagedKeyRequest.newBuilder() .setKeyCust(ByteString.copyFrom(keyCust)).setKeyNamespace(keyNamespace).build()); return generateKeyDataList(statusResponse); @@ -73,16 +76,77 @@ public List getManagedKeys(byte[] keyCust, String keyNamespace) @Override public boolean rotateSTK() throws IOException { try { - ManagedKeysProtos.RotateSTKResponse response = - stub.rotateSTK(null, EmptyMsg.getDefaultInstance()); - return response.getRotated(); + BooleanMsg response = stub.rotateSTK(null, EmptyMsg.getDefaultInstance()); + return response.getBoolMsg(); + } catch (ServiceException e) { + throw ProtobufUtil.handleRemoteException(e); + } + } + + @Override + public void ejectManagedKeyDataCacheEntry(byte[] keyCustodian, String keyNamespace, + String keyMetadata) throws IOException { + throw new NotImplementedException( + "ejectManagedKeyDataCacheEntry not supported in KeymetaAdminClient"); + } + + @Override + public void clearManagedKeyDataCache() throws IOException { + throw new NotImplementedException( + "clearManagedKeyDataCache not supported in KeymetaAdminClient"); + } + + @Override + public ManagedKeyData disableKeyManagement(byte[] keyCust, String keyNamespace) + throws IOException, KeyException { + try { + ManagedKeyResponse response = stub.disableKeyManagement(null, ManagedKeyRequest.newBuilder() + .setKeyCust(ByteString.copyFrom(keyCust)).setKeyNamespace(keyNamespace).build()); + return generateKeyData(response); + } catch (ServiceException e) { + throw ProtobufUtil.handleRemoteException(e); + } + } + + @Override + public ManagedKeyData disableManagedKey(byte[] keyCust, String keyNamespace, + byte[] keyMetadataHash) throws IOException, KeyException { + try { + ManagedKeyResponse response = stub.disableManagedKey(null, + ManagedKeyEntryRequest.newBuilder() + .setKeyCustNs(ManagedKeyRequest.newBuilder().setKeyCust(ByteString.copyFrom(keyCust)) + .setKeyNamespace(keyNamespace).build()) + .setKeyMetadataHash(ByteString.copyFrom(keyMetadataHash)).build()); + return generateKeyData(response); } catch (ServiceException e) { throw ProtobufUtil.handleRemoteException(e); } } - private static List - generateKeyDataList(ManagedKeysProtos.GetManagedKeysResponse stateResponse) { + @Override + public ManagedKeyData rotateManagedKey(byte[] keyCust, String keyNamespace) + throws IOException, KeyException { + try { + ManagedKeyResponse response = stub.rotateManagedKey(null, ManagedKeyRequest.newBuilder() + .setKeyCust(ByteString.copyFrom(keyCust)).setKeyNamespace(keyNamespace).build()); + return generateKeyData(response); + } catch (ServiceException e) { + throw ProtobufUtil.handleRemoteException(e); + } + } + + @Override + public void refreshManagedKeys(byte[] keyCust, String keyNamespace) + throws IOException, KeyException { + try { + stub.refreshManagedKeys(null, ManagedKeyRequest.newBuilder() + .setKeyCust(ByteString.copyFrom(keyCust)).setKeyNamespace(keyNamespace).build()); + } catch (ServiceException e) { + throw ProtobufUtil.handleRemoteException(e); + } + } + + private static List generateKeyDataList(GetManagedKeysResponse stateResponse) { List keyStates = new ArrayList<>(); for (ManagedKeyResponse state : stateResponse.getStateList()) { keyStates.add(generateKeyData(state)); @@ -90,9 +154,17 @@ public boolean rotateSTK() throws IOException { return keyStates; } - private static ManagedKeyData generateKeyData(ManagedKeysProtos.ManagedKeyResponse response) { - return new ManagedKeyData(response.getKeyCust().toByteArray(), response.getKeyNamespace(), null, - ManagedKeyState.forValue((byte) response.getKeyState().getNumber()), - response.getKeyMetadata(), response.getRefreshTimestamp()); + private static ManagedKeyData generateKeyData(ManagedKeyResponse response) { + // Use hash-only constructor for client-side ManagedKeyData + byte[] keyMetadataHash = + response.hasKeyMetadataHash() ? response.getKeyMetadataHash().toByteArray() : null; + if (keyMetadataHash == null) { + return new ManagedKeyData(response.getKeyCust().toByteArray(), response.getKeyNamespace(), + ManagedKeyState.forValue((byte) response.getKeyState().getNumber())); + } else { + return new ManagedKeyData(response.getKeyCust().toByteArray(), response.getKeyNamespace(), + ManagedKeyState.forValue((byte) response.getKeyState().getNumber()), keyMetadataHash, + response.getRefreshTimestamp()); + } } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyData.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyData.java index ffd5dbb7b574..181f28aeab00 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyData.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyData.java @@ -21,9 +21,9 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import java.util.Base64; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.hadoop.hbase.HBaseInterfaceAudience; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.util.DataChecksum; import org.apache.yetus.audience.InterfaceAudience; @@ -77,10 +77,11 @@ public class ManagedKeyData { /** * Constructs a new instance with the given parameters. - * @param key_cust The key custodian. - * @param theKey The actual key, can be {@code null}. - * @param keyState The state of the key. - * @param keyMetadata The metadata associated with the key. + * @param key_cust The key custodian. + * @param key_namespace The key namespace. + * @param theKey The actual key, can be {@code null}. + * @param keyState The state of the key. + * @param keyMetadata The metadata associated with the key. * @throws NullPointerException if any of key_cust, keyState or keyMetadata is null. */ public ManagedKeyData(byte[] key_cust, String key_namespace, Key theKey, ManagedKeyState keyState, @@ -92,9 +93,9 @@ public ManagedKeyData(byte[] key_cust, String key_namespace, Key theKey, Managed /** * Constructs a new instance with the given parameters including refresh timestamp. * @param key_cust The key custodian. + * @param key_namespace The key namespace. * @param theKey The actual key, can be {@code null}. * @param keyState The state of the key. - * @param keyMetadata The metadata associated with the key. * @param refreshTimestamp The refresh timestamp for the key. * @throws NullPointerException if any of key_cust, keyState or keyMetadata is null. */ @@ -103,23 +104,82 @@ public ManagedKeyData(byte[] key_cust, String key_namespace, Key theKey, Managed Preconditions.checkNotNull(key_cust, "key_cust should not be null"); Preconditions.checkNotNull(key_namespace, "key_namespace should not be null"); Preconditions.checkNotNull(keyState, "keyState should not be null"); - // Only check for null metadata if state is not FAILED - if (keyState != ManagedKeyState.FAILED) { - Preconditions.checkNotNull(keyMetadata, "keyMetadata should not be null"); - } + Preconditions.checkNotNull(keyMetadata, "metadata should not be null"); this.keyCustodian = key_cust; this.keyNamespace = key_namespace; - this.theKey = theKey; this.keyState = keyState; + this.theKey = theKey; this.keyMetadata = keyMetadata; + this.keyMetadataHash = constructMetadataHash(keyMetadata); this.refreshTimestamp = refreshTimestamp; } - @InterfaceAudience.Private - public ManagedKeyData cloneWithoutKey() { - return new ManagedKeyData(keyCustodian, keyNamespace, null, keyState, keyMetadata, - refreshTimestamp); + /** + * Client-side constructor using only metadata hash. This constructor is intended for use by + * client code where the original metadata string is not available. + * @param key_cust The key custodian. + * @param key_namespace The key namespace. + * @param keyState The state of the key. + * @param keyMetadataHash The pre-computed metadata hash. + * @param refreshTimestamp The refresh timestamp for the key. + * @throws NullPointerException if any of key_cust, keyState or keyMetadataHash is null. + */ + public ManagedKeyData(byte[] key_cust, String key_namespace, ManagedKeyState keyState, + byte[] keyMetadataHash, long refreshTimestamp) { + Preconditions.checkNotNull(key_cust, "key_cust should not be null"); + Preconditions.checkNotNull(key_namespace, "key_namespace should not be null"); + Preconditions.checkNotNull(keyState, "keyState should not be null"); + Preconditions.checkNotNull(keyMetadataHash, "keyMetadataHash should not be null"); + this.keyCustodian = key_cust; + this.keyNamespace = key_namespace; + this.keyState = keyState; + this.keyMetadataHash = keyMetadataHash; + this.refreshTimestamp = refreshTimestamp; + this.theKey = null; + this.keyMetadata = null; + } + + /** + * Constructs a new instance for the given key management state with the current timestamp. + * @param key_cust The key custodian. + * @param key_namespace The key namespace. + * @param keyState The state of the key. + * @throws NullPointerException if any of key_cust or key_namespace is null. + * @throws IllegalArgumentException if keyState is not a key management state. + */ + public ManagedKeyData(byte[] key_cust, String key_namespace, ManagedKeyState keyState) { + this(key_cust, key_namespace, keyState, EnvironmentEdgeManager.currentTime()); + } + + /** + * Constructs a new instance for the given key management state. + * @param key_cust The key custodian. + * @param key_namespace The key namespace. + * @param keyState The state of the key. + * @throws NullPointerException if any of key_cust or key_namespace is null. + * @throws IllegalArgumentException if keyState is not a key management state. + */ + public ManagedKeyData(byte[] key_cust, String key_namespace, ManagedKeyState keyState, + long refreshTimestamp) { + Preconditions.checkNotNull(key_cust, "key_cust should not be null"); + Preconditions.checkNotNull(key_namespace, "key_namespace should not be null"); + Preconditions.checkNotNull(keyState, "keyState should not be null"); + Preconditions.checkArgument(ManagedKeyState.isKeyManagementState(keyState), + "keyState must be a key management state, got: " + keyState); + this.keyCustodian = key_cust; + this.keyNamespace = key_namespace; + this.keyState = keyState; + this.refreshTimestamp = refreshTimestamp; + this.theKey = null; + this.keyMetadata = null; + this.keyMetadataHash = null; + } + + @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.UNITTEST) + public ManagedKeyData createClientFacingInstance() { + return new ManagedKeyData(keyCustodian, keyNamespace, keyState.getExternalState(), + keyMetadataHash, refreshTimestamp); } /** @@ -135,7 +195,7 @@ public byte[] getKeyCustodian() { * @return the encoded key custodian */ public String getKeyCustodianEncoded() { - return Base64.getEncoder().encodeToString(keyCustodian); + return ManagedKeyProvider.encodeToStr(keyCustodian); } /** @@ -212,20 +272,17 @@ public static long constructKeyChecksum(byte[] data) { * @return The hash of the key metadata as a byte array. */ public byte[] getKeyMetadataHash() { - if (keyMetadataHash == null && keyMetadata != null) { - keyMetadataHash = constructMetadataHash(keyMetadata); - } return keyMetadataHash; } /** * Return the hash of key metadata in Base64 encoded form. - * @return the encoded hash or {@code null} if no meatadata is available. + * @return the encoded hash or {@code null} if no metadata is available. */ public String getKeyMetadataHashEncoded() { byte[] hash = getKeyMetadataHash(); if (hash != null) { - return Base64.getEncoder().encodeToString(hash); + return ManagedKeyProvider.encodeToStr(hash); } return null; } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyProvider.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyProvider.java index c3adc5867bd1..308625fbfb17 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyProvider.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyProvider.java @@ -45,10 +45,13 @@ public interface ManagedKeyProvider { ManagedKeyData getSystemKey(byte[] systemId) throws IOException; /** - * Retrieve a managed key for the specified prefix. + * Retrieve a managed key for the specified prefix. The returned key is expected to be in the + * ACTIVE state, but it may be in any other state, such as FAILED and DISABLED. A key provider is + * typically expected to return a key with no associated metadata, but it is not required, instead + * it is permitted to throw IOException which will be treated as a retrieval FAILURE. * @param key_cust The key custodian. * @param key_namespace Key namespace - * @return ManagedKeyData for the system key and is expected to be not {@code null} + * @return ManagedKeyData for the managed key and is expected to be not {@code null} * @throws IOException if an error occurs while retrieving the key */ ManagedKeyData getManagedKey(byte[] key_cust, String key_namespace) throws IOException; diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyState.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyState.java index 2947addf5f8a..55257b9df799 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyState.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/ManagedKeyState.java @@ -26,14 +26,26 @@ */ @InterfaceAudience.Public public enum ManagedKeyState { + + // Key management states that are also applicable to individual keys. + /** Represents the active status of a managed key. */ ACTIVE((byte) 1), - /** Represents the inactive status of a managed key. */ - INACTIVE((byte) 2), /** Represents the retrieval failure status of a managed key. */ - FAILED((byte) 3), + FAILED((byte) 2), /** Represents the disabled status of a managed key. */ - DISABLED((byte) 4),; + DISABLED((byte) 3), + + // Additional states that are applicable only to individual keys. + + /** Represents the inactive status of a managed key. */ + INACTIVE((byte) 4), + + /** Represents the disable state of an active key. */ + ACTIVE_DISABLED((byte) 5), + + /** Represents the disable state of an inactive key. */ + INACTIVE_DISABLED((byte) 6),; private static Map lookupByVal; @@ -51,6 +63,15 @@ public byte getVal() { return val; } + /** + * Returns the external state of a key. + * @param state The key state to convert + * @return The external state of the key + */ + public ManagedKeyState getExternalState() { + return this == ACTIVE_DISABLED || this == INACTIVE_DISABLED ? DISABLED : this; + } + /** * Returns the ManagedKeyState for the given numeric value. * @param val The numeric value of the desired ManagedKeyState @@ -75,4 +96,8 @@ public static ManagedKeyState forValue(byte val) { public static boolean isUsable(ManagedKeyState state) { return state == ACTIVE || state == INACTIVE; } + + public static boolean isKeyManagementState(ManagedKeyState state) { + return state.getVal() < 4; + } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java index a703f5ff630e..8e66bcf535bc 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/HBaseHostnameVerifier.java @@ -81,11 +81,11 @@ private static final class SubjectName { this.type = type; } - public int getType() { + private int getType() { return type; } - public String getValue() { + private String getValue() { return value; } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdmin.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdmin.java index 4bf79090c3be..5778c3d19366 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdmin.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdmin.java @@ -58,4 +58,67 @@ List getManagedKeys(byte[] keyCust, String keyNamespace) * @throws IOException if an error occurs while rotating the STK */ boolean rotateSTK() throws IOException; + + /** + * Eject a specific managed key entry from the managed key data cache on all live region servers. + * @param keyCustodian the key custodian + * @param keyNamespace the key namespace + * @param keyMetadata the key metadata + * @throws IOException if an error occurs while ejecting the key + */ + void ejectManagedKeyDataCacheEntry(byte[] keyCustodian, String keyNamespace, String keyMetadata) + throws IOException; + + /** + * Clear all entries in the managed key data cache on all live region servers. + * @throws IOException if an error occurs while clearing the cache + */ + void clearManagedKeyDataCache() throws IOException; + + /** + * Disables key management for the specified custodian and namespace. This marks any ACTIVE keys + * as INACTIVE and adds a DISABLED state marker such that no new ACTIVE key is retrieved, so the + * new data written will not be encrypted. + * @param keyCust The key custodian identifier. + * @param keyNamespace The namespace for the key management. + * @return The {@link ManagedKeyData} object identifying the previously active key and its current + * state. + * @throws IOException if an error occurs while disabling key management. + * @throws KeyException if an error occurs while disabling key management. + */ + ManagedKeyData disableKeyManagement(byte[] keyCust, String keyNamespace) + throws IOException, KeyException; + + /** + * Disables the specific managed key identified by the specified custodian, namespace, and + * metadata hash. + * @param keyCust The key custodian identifier. + * @param keyNamespace The namespace for the key management. + * @param keyMetadataHash The key metadata hash. + * @return A {@link ManagedKeyData} object identifying the key and its current status. + * @throws IOException if an error occurs while disabling the managed key. + * @throws KeyException if an error occurs while disabling the managed key. + */ + ManagedKeyData disableManagedKey(byte[] keyCust, String keyNamespace, byte[] keyMetadataHash) + throws IOException, KeyException; + + /** + * Attempt a key rotation for the active key of the specified custodian and namespace. + * @param keyCust The key custodian identifier. + * @param keyNamespace The namespace for the key management. + * @return A {@link ManagedKeyData} object identifying the key and its current status. + * @throws IOException if an error occurs while rotating the managed key. + * @throws KeyException if an error occurs while rotating the managed key. + */ + ManagedKeyData rotateManagedKey(byte[] keyCust, String keyNamespace) + throws IOException, KeyException; + + /** + * Refresh all the keymeta entries for the specified custodian and namespace. + * @param keyCust The key custodian identifier. + * @param keyNamespace The namespace for the key management. + * @throws IOException if an error occurs while refreshing managed keys. + * @throws KeyException if an error occurs while refreshing managed keys. + */ + void refreshManagedKeys(byte[] keyCust, String keyNamespace) throws IOException, KeyException; } diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/KeymetaTestUtils.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/KeymetaTestUtils.java index 78a3f143825e..3c6fd5c1d481 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/KeymetaTestUtils.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/KeymetaTestUtils.java @@ -45,6 +45,10 @@ public final class KeymetaTestUtils { + private KeymetaTestUtils() { + // Utility class + } + /** * A ByteArrayInputStream that implements Seekable and PositionedReadable to work with * FSDataInputStream. @@ -106,10 +110,6 @@ public void readFully(long position, byte[] buffer) throws IOException { } } - private KeymetaTestUtils() { - // Utility class - } - public static final String ALIAS = "test"; public static final String PASSWORD = "password"; diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/MockManagedKeyProvider.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/MockManagedKeyProvider.java index a4c0d6833477..9e24e93c9edb 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/MockManagedKeyProvider.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/MockManagedKeyProvider.java @@ -28,6 +28,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; + /** * A simple implementation of ManagedKeyProvider for testing. It generates a key on demand given a * prefix. One can control the state of a key by calling setKeyState and can rotate a key by calling @@ -72,6 +74,7 @@ public ManagedKeyData getManagedKey(byte[] key_cust, String key_namespace) throw @Override public ManagedKeyData unwrapKey(String keyMetadata, byte[] wrappedKey) throws IOException { String[] meta_toks = keyMetadata.split(":"); + Preconditions.checkArgument(meta_toks.length >= 3, "Invalid key metadata: %s", keyMetadata); if (allGeneratedKeys.containsKey(keyMetadata)) { ManagedKeyState keyState = this.keyState.get(meta_toks[1]); ManagedKeyData managedKeyData = diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/TestManagedKeyData.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/TestManagedKeyData.java index 36932371c110..66a2bfd9344b 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/TestManagedKeyData.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/TestManagedKeyData.java @@ -32,6 +32,7 @@ import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.testclassification.MiscTests; import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.hbase.util.Bytes; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -85,12 +86,12 @@ public void testConstructorNullChecks() { } @Test - public void testConstructorWithFailedStateAndNullMetadata() { - ManagedKeyData keyData = - new ManagedKeyData(keyCust, keyNamespace, null, ManagedKeyState.FAILED, null); + public void testConstructorWithFailedEncryptionStateAndNullMetadata() { + ManagedKeyData keyData = new ManagedKeyData(keyCust, keyNamespace, ManagedKeyState.FAILED); assertNotNull(keyData); assertEquals(ManagedKeyState.FAILED, keyData.getKeyState()); assertNull(keyData.getKeyMetadata()); + assertNull(keyData.getKeyMetadataHash()); assertNull(keyData.getTheKey()); } @@ -104,12 +105,13 @@ public void testConstructorWithRefreshTimestamp() { @Test public void testCloneWithoutKey() { - ManagedKeyData cloned = managedKeyData.cloneWithoutKey(); + ManagedKeyData cloned = managedKeyData.createClientFacingInstance(); assertNull(cloned.getTheKey()); + assertNull(cloned.getKeyMetadata()); assertEquals(managedKeyData.getKeyCustodian(), cloned.getKeyCustodian()); assertEquals(managedKeyData.getKeyNamespace(), cloned.getKeyNamespace()); assertEquals(managedKeyData.getKeyState(), cloned.getKeyState()); - assertEquals(managedKeyData.getKeyMetadata(), cloned.getKeyMetadata()); + assertTrue(Bytes.equals(managedKeyData.getKeyMetadataHash(), cloned.getKeyMetadataHash())); } @Test @@ -151,17 +153,6 @@ public void testGetKeyMetadataHashEncoded() { assertEquals(24, encodedHash.length()); // Base64 encoded MD5 hash is 24 characters long } - @Test - public void testGetKeyMetadataHashEncodedWithNullHash() { - // Create ManagedKeyData with FAILED state and null metadata - // Passing null for metadata should result in null hash. - ManagedKeyData keyData = - new ManagedKeyData("custodian".getBytes(), "namespace", null, ManagedKeyState.FAILED, null); - - String encoded = keyData.getKeyMetadataHashEncoded(); - assertNull(encoded); - } - @Test public void testConstructMetadataHash() { byte[] hash = ManagedKeyData.constructMetadataHash(keyMetadata); @@ -190,6 +181,16 @@ public void testEquals() { assertNotEquals(managedKeyData, different); } + @Test + public void testEqualsWithNull() { + assertNotEquals(managedKeyData, null); + } + + @Test + public void testEqualsWithDifferentClass() { + assertNotEquals(managedKeyData, new Object()); + } + @Test public void testHashCode() { ManagedKeyData same = new ManagedKeyData(keyCust, keyNamespace, theKey, keyState, keyMetadata); diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/TestManagedKeyState.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/TestManagedKeyState.java new file mode 100644 index 000000000000..5b60385a3d90 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/io/crypto/TestManagedKeyState.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.io.crypto; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +/** + * Tests for ManagedKeyState enum and its utility methods. + */ +@Category({ MiscTests.class, SmallTests.class }) +public class TestManagedKeyState { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestManagedKeyState.class); + + @Test + public void testGetVal() { + assertEquals((byte) 1, ManagedKeyState.ACTIVE.getVal()); + assertEquals((byte) 2, ManagedKeyState.FAILED.getVal()); + assertEquals((byte) 3, ManagedKeyState.DISABLED.getVal()); + assertEquals((byte) 4, ManagedKeyState.INACTIVE.getVal()); + assertEquals((byte) 5, ManagedKeyState.ACTIVE_DISABLED.getVal()); + assertEquals((byte) 6, ManagedKeyState.INACTIVE_DISABLED.getVal()); + } + + @Test + public void testForValue() { + assertEquals(ManagedKeyState.ACTIVE, ManagedKeyState.forValue((byte) 1)); + assertEquals(ManagedKeyState.FAILED, ManagedKeyState.forValue((byte) 2)); + assertEquals(ManagedKeyState.DISABLED, ManagedKeyState.forValue((byte) 3)); + assertEquals(ManagedKeyState.INACTIVE, ManagedKeyState.forValue((byte) 4)); + assertEquals(ManagedKeyState.ACTIVE_DISABLED, ManagedKeyState.forValue((byte) 5)); + assertEquals(ManagedKeyState.INACTIVE_DISABLED, ManagedKeyState.forValue((byte) 6)); + } + + @Test + public void testIsUsable() { + // ACTIVE and INACTIVE are usable for encryption/decryption + assertTrue(ManagedKeyState.isUsable(ManagedKeyState.ACTIVE)); + assertTrue(ManagedKeyState.isUsable(ManagedKeyState.INACTIVE)); + + // Other states are not usable + assertFalse(ManagedKeyState.isUsable(ManagedKeyState.FAILED)); + assertFalse(ManagedKeyState.isUsable(ManagedKeyState.DISABLED)); + assertFalse(ManagedKeyState.isUsable(ManagedKeyState.ACTIVE_DISABLED)); + assertFalse(ManagedKeyState.isUsable(ManagedKeyState.INACTIVE_DISABLED)); + } + + @Test + public void testIsKeyManagementState() { + // States with val < 4 are key management states (apply to namespaces) + assertTrue(ManagedKeyState.isKeyManagementState(ManagedKeyState.ACTIVE)); + assertTrue(ManagedKeyState.isKeyManagementState(ManagedKeyState.FAILED)); + assertTrue(ManagedKeyState.isKeyManagementState(ManagedKeyState.DISABLED)); + + // States with val >= 4 are key-specific states + assertFalse(ManagedKeyState.isKeyManagementState(ManagedKeyState.INACTIVE)); + assertFalse(ManagedKeyState.isKeyManagementState(ManagedKeyState.ACTIVE_DISABLED)); + assertFalse(ManagedKeyState.isKeyManagementState(ManagedKeyState.INACTIVE_DISABLED)); + } + + @Test + public void testGetExternalState() { + // ACTIVE_DISABLED and INACTIVE_DISABLED should map to DISABLED + assertEquals(ManagedKeyState.DISABLED, ManagedKeyState.ACTIVE_DISABLED.getExternalState()); + assertEquals(ManagedKeyState.DISABLED, ManagedKeyState.INACTIVE_DISABLED.getExternalState()); + + // Other states should return themselves + assertEquals(ManagedKeyState.ACTIVE, ManagedKeyState.ACTIVE.getExternalState()); + assertEquals(ManagedKeyState.INACTIVE, ManagedKeyState.INACTIVE.getExternalState()); + assertEquals(ManagedKeyState.FAILED, ManagedKeyState.FAILED.getExternalState()); + assertEquals(ManagedKeyState.DISABLED, ManagedKeyState.DISABLED.getExternalState()); + } + + @Test + public void testStateValuesUnique() { + // Ensure all state values are unique + ManagedKeyState[] states = ManagedKeyState.values(); + for (int i = 0; i < states.length; i++) { + for (int j = i + 1; j < states.length; j++) { + assertNotNull(states[i]); + assertNotNull(states[j]); + assertFalse("State values must be unique: " + states[i] + " vs " + states[j], + states[i].getVal() == states[j].getVal()); + } + } + } +} diff --git a/hbase-protocol-shaded/src/main/protobuf/HBase.proto b/hbase-protocol-shaded/src/main/protobuf/HBase.proto index c66ee7eb9791..674619d9b5ae 100644 --- a/hbase-protocol-shaded/src/main/protobuf/HBase.proto +++ b/hbase-protocol-shaded/src/main/protobuf/HBase.proto @@ -212,6 +212,10 @@ message BigDecimalMsg { required bytes bigdecimal_msg = 1; } +message BooleanMsg { + required bool bool_msg = 1; +} + message UUID { required uint64 least_sig_bits = 1; required uint64 most_sig_bits = 2; @@ -293,3 +297,32 @@ message RotateFileData { message LastHighestWalFilenum { map file_num = 1; } + +message ManagedKeyRequest { + required bytes key_cust = 1; + required string key_namespace = 2; +} + +message ManagedKeyEntryRequest { + required ManagedKeyRequest key_cust_ns = 1; + required bytes key_metadata_hash = 2; +} + +enum ManagedKeyState { + KEY_ACTIVE = 1; + KEY_DISABLED = 2; + KEY_INACTIVE = 3; + KEY_FAILED = 4; +} + +message ManagedKeyResponse { + required bytes key_cust = 1; + required string key_namespace = 2; + required ManagedKeyState key_state = 3; + optional bytes key_metadata_hash = 4; + optional int64 refresh_timestamp = 5; +} + +message GetManagedKeysResponse { + repeated ManagedKeyResponse state = 1; +} diff --git a/hbase-protocol-shaded/src/main/protobuf/server/ManagedKeys.proto b/hbase-protocol-shaded/src/main/protobuf/server/ManagedKeys.proto index 8e633fc25bab..f79badb544fc 100644 --- a/hbase-protocol-shaded/src/main/protobuf/server/ManagedKeys.proto +++ b/hbase-protocol-shaded/src/main/protobuf/server/ManagedKeys.proto @@ -18,7 +18,7 @@ syntax = "proto2"; package hbase.pb; -option java_package = "org.apache.hadoop.hbase.protobuf.generated"; +option java_package = "org.apache.hadoop.hbase.shaded.protobuf.generated"; option java_outer_classname = "ManagedKeysProtos"; option java_generic_services = true; option java_generate_equals_and_hash = true; @@ -26,39 +26,19 @@ option optimize_for = SPEED; import "HBase.proto"; -message ManagedKeyRequest { - required bytes key_cust = 1; - required string key_namespace = 2; -} - -enum ManagedKeyState { - KEY_ACTIVE = 1; - KEY_INACTIVE = 2; - KEY_FAILED = 3; - KEY_DISABLED = 4; -} - -message ManagedKeyResponse { - required bytes key_cust = 1; - required string key_namespace = 2; - required ManagedKeyState key_state = 3; - optional string key_metadata = 4; - optional int64 refresh_timestamp = 5; -} - -message GetManagedKeysResponse { - repeated ManagedKeyResponse state = 1; -} - -message RotateSTKResponse { - required bool rotated = 1; -} - service ManagedKeysService { rpc EnableKeyManagement(ManagedKeyRequest) returns (ManagedKeyResponse); rpc GetManagedKeys(ManagedKeyRequest) returns (GetManagedKeysResponse); rpc RotateSTK(EmptyMsg) - returns (RotateSTKResponse); + returns (BooleanMsg); + rpc DisableKeyManagement(ManagedKeyRequest) + returns (ManagedKeyResponse); + rpc DisableManagedKey(ManagedKeyEntryRequest) + returns (ManagedKeyResponse); + rpc RotateManagedKey(ManagedKeyRequest) + returns (ManagedKeyResponse); + rpc RefreshManagedKeys(ManagedKeyRequest) + returns (EmptyMsg); } diff --git a/hbase-protocol-shaded/src/main/protobuf/server/region/Admin.proto b/hbase-protocol-shaded/src/main/protobuf/server/region/Admin.proto index 31abe9ad4176..591d78aaeb1b 100644 --- a/hbase-protocol-shaded/src/main/protobuf/server/region/Admin.proto +++ b/hbase-protocol-shaded/src/main/protobuf/server/region/Admin.proto @@ -423,4 +423,10 @@ service AdminService { rpc RefreshSystemKeyCache(EmptyMsg) returns(EmptyMsg); + + rpc EjectManagedKeyDataCacheEntry(ManagedKeyEntryRequest) + returns(BooleanMsg); + + rpc ClearManagedKeyDataCache(EmptyMsg) + returns(EmptyMsg); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeyManagementBase.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeyManagementBase.java index e263ccb4fbef..1885790c3ca9 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeyManagementBase.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeyManagementBase.java @@ -18,11 +18,9 @@ package org.apache.hadoop.hbase.keymeta; import java.io.IOException; -import java.security.KeyException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.io.crypto.Encryption; -import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; import org.apache.hadoop.hbase.io.crypto.ManagedKeyProvider; import org.apache.hadoop.hbase.security.SecurityUtil; import org.apache.yetus.audience.InterfaceAudience; @@ -43,8 +41,9 @@ public abstract class KeyManagementBase { private Boolean isKeyManagementEnabled; /** - * Construct with a server instance. Configuration is derived from the server. - * @param server the server instance + * Construct with a key management service instance. Configuration is derived from the key + * management service. + * @param keyManagementService the key management service instance */ public KeyManagementBase(KeyManagementService keyManagementService) { this(keyManagementService.getConfiguration()); @@ -75,7 +74,7 @@ protected Configuration getConfiguration() { * @return the managed key provider * @throws RuntimeException if no provider is configured */ - protected ManagedKeyProvider getKeyProvider() { + public ManagedKeyProvider getKeyProvider() { return Encryption.getManagedKeyProvider(getConfiguration()); } @@ -108,43 +107,4 @@ protected boolean isKeyManagementEnabled() { } return isKeyManagementEnabled; } - - /** - * Utility function to retrieves a managed key from the key provider. If an existing key is - * provided and the retrieved key is the same as the existing key, it will be ignored. - * @param encKeyCust the encoded key custodian - * @param key_cust the key custodian - * @param keyNamespace the key namespace - * @param accessor the accessor to use to persist the key. If null, the key will not be - * persisted. - * @param existingActiveKey the existing key, typically the active key already retrieved from the - * key provider, can be null. - * @return the retrieved key, or null if no key could be retrieved - * @throws IOException if an error occurs - * @throws KeyException if an error occurs - */ - protected ManagedKeyData retrieveActiveKey(String encKeyCust, byte[] key_cust, - String keyNamespace, KeymetaTableAccessor accessor, ManagedKeyData existingActiveKey) - throws IOException, KeyException { - ManagedKeyProvider provider = getKeyProvider(); - ManagedKeyData pbeKey = provider.getManagedKey(key_cust, keyNamespace); - if (pbeKey == null) { - throw new IOException("Invalid null managed key received from key provider"); - } - /* - * Will be useful when refresh API is implemented. if (existingActiveKey != null && - * existingActiveKey.equals(pbeKey)) { - * LOG.info("retrieveActiveKey: no change in key for (custodian: {}, namespace: {}", encKeyCust, - * keyNamespace); return null; } // TODO: If existingActiveKey is not null, we should update the - * key state to INACTIVE. - */ - LOG.info( - "retrieveActiveKey: got active key with status: {} and metadata: {} for " - + "(custodian: {}, namespace: {})", - pbeKey.getKeyState(), pbeKey.getKeyMetadata(), encKeyCust, pbeKey.getKeyNamespace()); - if (accessor != null) { - accessor.addKey(pbeKey); - } - return pbeKey; - } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeyManagementUtils.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeyManagementUtils.java new file mode 100644 index 000000000000..816d5371475a --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeyManagementUtils.java @@ -0,0 +1,285 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.keymeta; + +import java.io.IOException; +import java.security.KeyException; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyProvider; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyState; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; + +@InterfaceAudience.Private +public final class KeyManagementUtils { + private static final Logger LOG = LoggerFactory.getLogger(KeyManagementUtils.class); + + private KeyManagementUtils() { + // Utility class, should not be instantiated + } + + /** + * Utility function to retrieves a managed key from the key provider. If an existing key is + * provided and the retrieved key is the same as the existing key, it will be ignored. + * @param provider the managed key provider + * @param accessor the accessor to use to persist the key. If null, the key will not be + * persisted. + * @param encKeyCust the encoded key custodian + * @param key_cust the key custodian + * @param keyNamespace the key namespace + * @param existingActiveKey the existing key, typically the active key already retrieved from the + * key provider, can be null. + * @return the retrieved key, or null if no key could be retrieved + * @throws IOException if an error occurs + * @throws KeyException if an error occurs + */ + public static ManagedKeyData retrieveActiveKey(ManagedKeyProvider provider, + KeymetaTableAccessor accessor, String encKeyCust, byte[] key_cust, String keyNamespace, + ManagedKeyData existingActiveKey) throws IOException, KeyException { + Preconditions.checkArgument( + existingActiveKey == null || existingActiveKey.getKeyState() == ManagedKeyState.ACTIVE, + "Expected existing active key to be null or having ACTIVE state" + + (existingActiveKey == null ? "" : ", but got: " + existingActiveKey.getKeyState())); + ManagedKeyData keyData; + try { + keyData = provider.getManagedKey(key_cust, keyNamespace); + } catch (IOException e) { + keyData = new ManagedKeyData(key_cust, keyNamespace, ManagedKeyState.FAILED); + } + if (keyData == null) { + throw new IOException("Invalid null managed key received from key provider"); + } + if (keyData.getKeyMetadata() != null && keyData.getKeyState() == ManagedKeyState.INACTIVE) { + throw new IOException( + "Expected key to be ACTIVE, but got an INACTIVE key with metadata hash: " + + keyData.getKeyMetadataHashEncoded() + " for (custodian: " + encKeyCust + ", namespace: " + + keyNamespace + ")"); + } + + if (existingActiveKey != null && existingActiveKey.equals(keyData)) { + LOG.info("retrieveActiveKey: no change in active key for (custodian: {}, namespace: {}", + encKeyCust, keyNamespace); + return existingActiveKey; + } + + LOG.info( + "retrieveActiveKey: got key with state: {} and metadata: {} for custodian: {} namespace: {}", + keyData.getKeyState(), keyData.getKeyMetadataHashEncoded(), encKeyCust, + keyData.getKeyNamespace()); + if (accessor != null) { + if (keyData.getKeyMetadata() != null) { + accessor.addKey(keyData); + } else { + accessor.addKeyManagementStateMarker(keyData.getKeyCustodian(), keyData.getKeyNamespace(), + keyData.getKeyState()); + } + } + return keyData; + } + + /** + * Retrieves a key from the key provider for the specified metadata. + * @param provider the managed key provider + * @param accessor the accessor to use to persist the key. If null, the key will not be + * persisted. + * @param encKeyCust the encoded key custodian + * @param keyCust the key custodian + * @param keyNamespace the key namespace + * @param keyMetadata the key metadata + * @param wrappedKey the wrapped key, if available, can be null. + * @return the retrieved key that is guaranteed to be not null and have non-null metadata. + * @throws IOException if an error occurs while retrieving or persisting the key + * @throws KeyException if an error occurs while retrieving or validating the key + */ + public static ManagedKeyData retrieveKey(ManagedKeyProvider provider, + KeymetaTableAccessor accessor, String encKeyCust, byte[] keyCust, String keyNamespace, + String keyMetadata, byte[] wrappedKey) throws IOException, KeyException { + ManagedKeyData keyData = provider.unwrapKey(keyMetadata, wrappedKey); + // Do some validation of the resposne, as we can't trust that all providers honour the contract. + // If the key is disabled, we expect a more specific key state to be used, not the generic + // DISABLED state. + if ( + keyData == null || keyData.getKeyMetadata() == null + || !keyData.getKeyMetadata().equals(keyMetadata) + || keyData.getKeyState() == ManagedKeyState.DISABLED + ) { + throw new KeyException( + "Invalid key that is null or having invalid metadata or state received from key provider " + + "for (custodian: " + encKeyCust + ", namespace: " + keyNamespace + + ") and metadata hash: " + + ManagedKeyProvider.encodeToStr(ManagedKeyData.constructMetadataHash(keyMetadata))); + } + if (LOG.isInfoEnabled()) { + LOG.info( + "retrieveKey: got key with state: {} and metadata: {} for (custodian: {}, " + + "namespace: {}) and metadata hash: {}", + keyData.getKeyState(), keyData.getKeyMetadata(), encKeyCust, keyNamespace, + ManagedKeyProvider.encodeToStr(ManagedKeyData.constructMetadataHash(keyMetadata))); + } + if (accessor != null) { + try { + accessor.addKey(keyData); + } catch (IOException e) { + LOG.warn( + "retrieveKey: Failed to add key to L2 for metadata hash: {}, for custodian: {}, " + + "namespace: {}", + ManagedKeyProvider.encodeToStr(ManagedKeyData.constructMetadataHash(keyMetadata)), + encKeyCust, keyNamespace, e); + } + } + return keyData; + } + + /** + * Refreshes the specified key from the configured managed key provider to confirm it is still + * valid. + * @param provider the managed key provider + * @param accessor the accessor to use to persist changes + * @param keyData the key data to refresh + * @return the refreshed key data, or the original if unchanged + * @throws IOException if an error occurs + * @throws KeyException if an error occurs + */ + public static ManagedKeyData refreshKey(ManagedKeyProvider provider, + KeymetaTableAccessor accessor, ManagedKeyData keyData) throws IOException, KeyException { + if (LOG.isDebugEnabled()) { + LOG.debug( + "refreshKey: entry with keyData state: {}, metadata hash: {} for (custodian: {}, " + + "namespace: {})", + keyData.getKeyState(), keyData.getKeyMetadataHashEncoded(), + ManagedKeyProvider.encodeToStr(keyData.getKeyCustodian()), keyData.getKeyNamespace()); + } + + Preconditions.checkArgument(keyData.getKeyMetadata() != null, + "Key metadata should be non-null for key to be refreshed"); + + ManagedKeyData result; + // NOTE: Even FAILED keys can have metadata that is good enough for refreshing from provider. + // Refresh key using unwrapKey + ManagedKeyData newKeyData; + try { + newKeyData = provider.unwrapKey(keyData.getKeyMetadata(), null); + if (LOG.isDebugEnabled()) { + LOG.debug( + "refreshKey: unwrapped key with state: {}, metadata hash: {} for (custodian: " + + "{}, namespace: {})", + newKeyData.getKeyState(), newKeyData.getKeyMetadataHashEncoded(), + ManagedKeyProvider.encodeToStr(newKeyData.getKeyCustodian()), + newKeyData.getKeyNamespace()); + } + } catch (IOException e) { + LOG.warn("refreshKey: Failed to unwrap key for (custodian: {}, namespace: {})", + ManagedKeyProvider.encodeToStr(keyData.getKeyCustodian()), keyData.getKeyNamespace(), e); + newKeyData = new ManagedKeyData(keyData.getKeyCustodian(), keyData.getKeyNamespace(), null, + ManagedKeyState.FAILED, keyData.getKeyMetadata()); + } + + // Validate metadata hasn't changed + if (!keyData.getKeyMetadata().equals(newKeyData.getKeyMetadata())) { + throw new KeyException("Key metadata changed during refresh: current metadata hash: " + + keyData.getKeyMetadataHashEncoded() + ", got metadata hash: " + + newKeyData.getKeyMetadataHashEncoded() + " for (custodian: " + + ManagedKeyProvider.encodeToStr(keyData.getKeyCustodian()) + ", namespace: " + + keyData.getKeyNamespace() + ")"); + } + + // Check if state changed + if (keyData.getKeyState() == newKeyData.getKeyState()) { + // No change, return original + result = keyData; + } else if (newKeyData.getKeyState() == ManagedKeyState.FAILED) { + // Ignore if new state is FAILED, let us just keep the existing key data as is as this is + // most likely a transitional issue with KMS. + result = keyData; + } else { + if (newKeyData.getKeyState().getExternalState() == ManagedKeyState.DISABLED) { + // Handle DISABLED state change specially. + accessor.disableKey(keyData); + } else { + // Rest of the state changes are only ACTIVE and INACTIVE.. + accessor.updateActiveState(keyData, newKeyData.getKeyState()); + } + result = newKeyData; + } + + if (LOG.isDebugEnabled()) { + LOG.debug( + "refreshKey: completed with result state: {}, metadata hash: {} for (custodian: " + + "{}, namespace: {})", + result.getKeyState(), result.getKeyMetadataHashEncoded(), + ManagedKeyProvider.encodeToStr(result.getKeyCustodian()), result.getKeyNamespace()); + } + + return result; + } + + /** + * Rotates the ACTIVE key for the specified (custodian, namespace) combination. + * @param provider the managed key provider + * @param accessor the accessor to use to persist changes + * @param keyCust the key custodian + * @param keyNamespace the key namespace + * @return the new active key, or null if no rotation happened + * @throws IOException if an error occurs + * @throws KeyException if an error occurs + */ + public static ManagedKeyData rotateActiveKey(ManagedKeyProvider provider, + KeymetaTableAccessor accessor, String encKeyCust, byte[] keyCust, String keyNamespace) + throws IOException, KeyException { + // Get current active key + ManagedKeyData currentActiveKey = accessor.getKeyManagementStateMarker(keyCust, keyNamespace); + if (currentActiveKey == null || currentActiveKey.getKeyState() != ManagedKeyState.ACTIVE) { + throw new IOException("No active key found, key management not yet enabled for (custodian: " + + encKeyCust + ", namespace: " + keyNamespace + ") ?"); + } + + // Retrieve new key from provider, We pass null accessor to skip default persistence logic, + // because a failure to rotate shouldn't make the current active key invalid. + ManagedKeyData newKey = retrieveActiveKey(provider, null, + ManagedKeyProvider.encodeToStr(keyCust), keyCust, keyNamespace, currentActiveKey); + if (newKey == null || newKey.equals(currentActiveKey)) { + LOG.warn( + "rotateActiveKey: failed to retrieve new active key for (custodian: {}, namespace: {})", + encKeyCust, keyNamespace); + return null; + } + + // If rotation succeeds in generating a new active key, persist the new key and mark the current + // active key as inactive. + if (newKey.getKeyState() == ManagedKeyState.ACTIVE) { + try { + accessor.addKey(newKey); + accessor.updateActiveState(currentActiveKey, ManagedKeyState.INACTIVE); + return newKey; + } catch (IOException e) { + LOG.warn("rotateActiveKey: failed to persist new active key to L2 for (custodian: {}, " + + "namespace: {})", encKeyCust, keyNamespace, e); + return null; + } + } else { + LOG.warn( + "rotateActiveKey: ignoring new key with state {} without metadata hash: {} for " + + "(custodian: {}, namespace: {})", + newKey.getKeyState(), newKey.getKeyMetadataHashEncoded(), encKeyCust, keyNamespace); + return null; + } + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdminImpl.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdminImpl.java index 894606e9a2b2..33b7423fcba5 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdminImpl.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaAdminImpl.java @@ -20,12 +20,12 @@ import java.io.IOException; import java.security.KeyException; import java.util.List; -import java.util.Set; import org.apache.hadoop.hbase.Server; import org.apache.hadoop.hbase.ServerName; import org.apache.hadoop.hbase.client.AsyncAdmin; import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; import org.apache.hadoop.hbase.io.crypto.ManagedKeyProvider; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyState; import org.apache.hadoop.hbase.master.MasterServices; import org.apache.hadoop.hbase.util.FutureUtils; import org.apache.yetus.audience.InterfaceAudience; @@ -49,18 +49,21 @@ public ManagedKeyData enableKeyManagement(byte[] keyCust, String keyNamespace) keyNamespace); // Check if (cust, namespace) pair is already enabled and has an active key. - ManagedKeyData activeKey = getActiveKey(keyCust, keyNamespace); - if (activeKey != null) { + ManagedKeyData markerKey = getKeyManagementStateMarker(keyCust, keyNamespace); + if (markerKey != null && markerKey.getKeyState() == ManagedKeyState.ACTIVE) { LOG.info( "enableManagedKeys: specified (custodian: {}, namespace: {}) already has " + "an active managed key with metadata: {}", - encodedCust, keyNamespace, activeKey.getKeyMetadata()); - return activeKey; + encodedCust, keyNamespace, markerKey.getKeyMetadata()); + // ACTIVE marker contains the full key data, so we can return it directly. + return markerKey; } - // Retrieve a single key from provider - ManagedKeyData retrievedKey = retrieveActiveKey(encodedCust, keyCust, keyNamespace, this, null); - return retrievedKey; + // Retrieve an active key from provider if this is the first time enabling key management or + // the previous attempt left it in a non-ACTIVE state. This may or may not succeed. When fails, + // it can leave the key management state in FAILED or DISABLED. + return KeyManagementUtils.retrieveActiveKey(getKeyProvider(), this, encodedCust, keyCust, + keyNamespace, null); } @Override @@ -71,7 +74,7 @@ public List getManagedKeys(byte[] keyCust, String keyNamespace) LOG.info("Getting key statuses for custodian: {} under namespace: {}", ManagedKeyProvider.encodeToStr(keyCust), keyNamespace); } - return getAllKeys(keyCust, keyNamespace); + return getAllKeys(keyCust, keyNamespace, false); } @Override @@ -90,7 +93,7 @@ public boolean rotateSTK() throws IOException { return false; } - Set regionServers = master.getServerManager().getOnlineServers().keySet(); + List regionServers = master.getServerManager().getOnlineServersList(); LOG.info("System Key is rotated, initiating cache refresh on all region servers"); try { @@ -104,6 +107,190 @@ public boolean rotateSTK() throws IOException { return true; } + @Override + public void ejectManagedKeyDataCacheEntry(byte[] keyCustodian, String keyNamespace, + String keyMetadata) throws IOException { + assertKeyManagementEnabled(); + if (!(getServer() instanceof MasterServices)) { + throw new IOException("ejectManagedKeyDataCacheEntry can only be called on master"); + } + MasterServices master = (MasterServices) getServer(); + + List regionServers = master.getServerManager().getOnlineServersList(); + + LOG.info("Ejecting managed key data cache entry on all region servers"); + try { + FutureUtils.get(getAsyncAdmin(master).ejectManagedKeyDataCacheEntryOnServers(regionServers, + keyCustodian, keyNamespace, keyMetadata)); + } catch (Exception e) { + throw new IOException(e); + } + + LOG.info("Successfully ejected managed key data cache entry on all region servers"); + } + + @Override + public void clearManagedKeyDataCache() throws IOException { + assertKeyManagementEnabled(); + if (!(getServer() instanceof MasterServices)) { + throw new IOException("clearManagedKeyDataCache can only be called on master"); + } + MasterServices master = (MasterServices) getServer(); + + List regionServers = master.getServerManager().getOnlineServersList(); + + LOG.info("Clearing managed key data cache on all region servers"); + try { + FutureUtils.get(getAsyncAdmin(master).clearManagedKeyDataCacheOnServers(regionServers)); + } catch (Exception e) { + throw new IOException(e); + } + + LOG.info("Successfully cleared managed key data cache on all region servers"); + } + + @Override + public ManagedKeyData disableKeyManagement(byte[] keyCust, String keyNamespace) + throws IOException, KeyException { + assertKeyManagementEnabled(); + String encodedCust = LOG.isInfoEnabled() ? ManagedKeyProvider.encodeToStr(keyCust) : null; + LOG.info("disableKeyManagement started for custodian: {} under namespace: {}", encodedCust, + keyNamespace); + + ManagedKeyData markerKey = getKeyManagementStateMarker(keyCust, keyNamespace); + if (markerKey != null && markerKey.getKeyState() == ManagedKeyState.ACTIVE) { + updateActiveState(markerKey, ManagedKeyState.INACTIVE); + LOG.info("disableKeyManagement completed for custodian: {} under namespace: {}", encodedCust, + keyNamespace); + } + + // Add key management state marker for the specified (keyCust, keyNamespace) combination + addKeyManagementStateMarker(keyCust, keyNamespace, ManagedKeyState.DISABLED); + LOG.info("disableKeyManagement completed for custodian: {} under namespace: {}", encodedCust, + keyNamespace); + + return getKeyManagementStateMarker(keyCust, keyNamespace); + } + + @Override + public ManagedKeyData disableManagedKey(byte[] keyCust, String keyNamespace, + byte[] keyMetadataHash) throws IOException, KeyException { + assertKeyManagementEnabled(); + String encodedCust = LOG.isInfoEnabled() ? ManagedKeyProvider.encodeToStr(keyCust) : null; + String encodedHash = + LOG.isInfoEnabled() ? ManagedKeyProvider.encodeToStr(keyMetadataHash) : null; + LOG.info("Disabling managed key with metadata hash: {} for custodian: {} under namespace: {}", + encodedHash, encodedCust, keyNamespace); + + // First retrieve the key to verify it exists and get the full metadata for cache ejection + ManagedKeyData existingKey = getKey(keyCust, keyNamespace, keyMetadataHash); + if (existingKey == null) { + throw new IOException("Key not found for (custodian: " + encodedCust + ", namespace: " + + keyNamespace + ") with metadata hash: " + encodedHash); + } + if (existingKey.getKeyState().getExternalState() == ManagedKeyState.DISABLED) { + throw new IOException("Key is already disabled for (custodian: " + encodedCust + + ", namespace: " + keyNamespace + ") with metadata hash: " + encodedHash); + } + + disableKey(existingKey); + // Eject from cache on all region servers (requires full metadata) + ejectManagedKeyDataCacheEntry(keyCust, keyNamespace, existingKey.getKeyMetadata()); + + LOG.info("Successfully disabled managed key with metadata hash: {} for custodian: {} under " + + "namespace: {}", encodedHash, encodedCust, keyNamespace); + // Retrieve and return the disabled key + ManagedKeyData disabledKey = getKey(keyCust, keyNamespace, keyMetadataHash); + return disabledKey; + } + + @Override + public ManagedKeyData rotateManagedKey(byte[] keyCust, String keyNamespace) + throws IOException, KeyException { + assertKeyManagementEnabled(); + String encodedCust = ManagedKeyProvider.encodeToStr(keyCust); + LOG.info("Rotating managed key for custodian: {} under namespace: {}", encodedCust, + keyNamespace); + + // Attempt rotation + return KeyManagementUtils.rotateActiveKey(getKeyProvider(), this, encodedCust, keyCust, + keyNamespace); + } + + @Override + public void refreshManagedKeys(byte[] keyCust, String keyNamespace) + throws IOException, KeyException { + assertKeyManagementEnabled(); + String encodedCust = ManagedKeyProvider.encodeToStr(keyCust); + LOG.info("refreshManagedKeys started for custodian: {} under namespace: {}", encodedCust, + keyNamespace); + + ManagedKeyData markerKey = getKeyManagementStateMarker(keyCust, keyNamespace); + if (markerKey != null && markerKey.getKeyState() == ManagedKeyState.DISABLED) { + LOG.info( + "refreshManagedKeys skipping since key management is disabled for custodian: {} under " + + "namespace: {}", + encodedCust, keyNamespace); + return; + } + // First, get all keys for the specified custodian and namespace and refresh those that have a + // non-null metadata. + List allKeys = getAllKeys(keyCust, keyNamespace, false); + IOException refreshException = null; + for (ManagedKeyData keyData : allKeys) { + if (keyData.getKeyMetadata() == null) { + continue; + } + LOG.debug( + "refreshManagedKeys: Refreshing key with metadata hash: {} for custodian: {} under " + + "namespace: {} with state: {}", + keyData.getKeyMetadataHashEncoded(), encodedCust, keyNamespace, keyData.getKeyState()); + try { + ManagedKeyData refreshedKey = + KeyManagementUtils.refreshKey(getKeyProvider(), this, keyData); + if (refreshedKey == keyData) { + LOG.debug( + "refreshManagedKeys: Key with metadata hash: {} for custodian: {} under " + + "namespace: {} is unchanged", + keyData.getKeyMetadataHashEncoded(), encodedCust, keyNamespace); + } else { + if (refreshedKey.getKeyState().getExternalState() == ManagedKeyState.DISABLED) { + LOG.info("refreshManagedKeys: Refreshed key is DISABLED, ejecting from cache"); + ejectManagedKeyDataCacheEntry(keyCust, keyNamespace, refreshedKey.getKeyMetadata()); + } else { + LOG.info( + "refreshManagedKeys: Successfully refreshed key with metadata hash: {} for " + + "custodian: {} under namespace: {}", + refreshedKey.getKeyMetadataHashEncoded(), encodedCust, keyNamespace); + } + } + } catch (IOException | KeyException e) { + LOG.error( + "refreshManagedKeys: Failed to refresh key with metadata hash: {} for custodian: {} " + + "under namespace: {}", + keyData.getKeyMetadataHashEncoded(), encodedCust, keyNamespace, e); + if (refreshException == null) { + refreshException = new IOException("Key refresh failed for (custodian: " + encodedCust + + ", namespace: " + keyNamespace + ")", e); + } + // Continue refreshing other keys + } + } + if (refreshException != null) { + throw refreshException; + } + + if (markerKey != null && markerKey.getKeyState() == ManagedKeyState.FAILED) { + LOG.info("refreshManagedKeys: Found FAILED marker for (custodian: " + encodedCust + + ", namespace: " + keyNamespace + + ") indicating previous attempt to enable, reattempting to enable key management"); + enableKeyManagement(keyCust, keyNamespace); + } + + LOG.info("refreshManagedKeys: Completed for custodian: {} under namespace: {}", encodedCust, + keyNamespace); + } + protected AsyncAdmin getAsyncAdmin(MasterServices master) { return master.getAsyncClusterConnection().getAdmin(); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaServiceEndpoint.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaServiceEndpoint.java index 85ff9ba3feb1..94539100aab1 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaServiceEndpoint.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaServiceEndpoint.java @@ -29,21 +29,24 @@ import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; import org.apache.hadoop.hbase.ipc.CoprocessorRpcUtils; import org.apache.hadoop.hbase.master.MasterServices; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.GetManagedKeysResponse; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeyRequest; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeyResponse; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeysService; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.RotateSTKResponse; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.hbase.thirdparty.com.google.protobuf.ByteString; import org.apache.hbase.thirdparty.com.google.protobuf.RpcCallback; import org.apache.hbase.thirdparty.com.google.protobuf.RpcController; import org.apache.hbase.thirdparty.com.google.protobuf.Service; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BooleanMsg; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.GetManagedKeysResponse; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyRequest; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyResponse; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyState; +import org.apache.hadoop.hbase.shaded.protobuf.generated.ManagedKeysProtos; +import org.apache.hadoop.hbase.shaded.protobuf.generated.ManagedKeysProtos.ManagedKeysService; /** * This class implements a coprocessor service endpoint for the key management metadata operations. @@ -113,7 +116,7 @@ public void enableKeyManagement(RpcController controller, ManagedKeyRequest requ response = generateKeyStateResponse(managedKeyState, builder); } catch (IOException | KeyException e) { CoprocessorRpcUtils.setControllerException(controller, new DoNotRetryIOException(e)); - builder.setKeyState(ManagedKeysProtos.ManagedKeyState.KEY_FAILED); + builder.setKeyState(ManagedKeyState.KEY_FAILED); } if (response == null) { response = builder.build(); @@ -149,7 +152,7 @@ public void getManagedKeys(RpcController controller, ManagedKeyRequest request, */ @Override public void rotateSTK(RpcController controller, EmptyMsg request, - RpcCallback done) { + RpcCallback done) { boolean rotated; try { rotated = master.getKeymetaAdmin().rotateSTK(); @@ -157,18 +160,126 @@ public void rotateSTK(RpcController controller, EmptyMsg request, CoprocessorRpcUtils.setControllerException(controller, new DoNotRetryIOException(e)); rotated = false; } - done.run(RotateSTKResponse.newBuilder().setRotated(rotated).build()); + done.run(BooleanMsg.newBuilder().setBoolMsg(rotated).build()); + } + + /** + * Disables all managed keys for a given custodian and namespace. + * @param controller The RPC controller. + * @param request The request containing the custodian and namespace specifications. + * @param done The callback to be invoked with the response. + */ + @Override + public void disableKeyManagement(RpcController controller, ManagedKeyRequest request, + RpcCallback done) { + ManagedKeyResponse response = null; + ManagedKeyResponse.Builder builder = ManagedKeyResponse.newBuilder(); + try { + initManagedKeyResponseBuilder(controller, request, builder); + ManagedKeyData managedKeyState = master.getKeymetaAdmin() + .disableKeyManagement(request.getKeyCust().toByteArray(), request.getKeyNamespace()); + response = generateKeyStateResponse(managedKeyState, builder); + } catch (IOException | KeyException e) { + CoprocessorRpcUtils.setControllerException(controller, new DoNotRetryIOException(e)); + builder.setKeyState(ManagedKeyState.KEY_FAILED); + } + if (response == null) { + response = builder.build(); + } + done.run(response); + } + + /** + * Disables a specific managed key for a given custodian, namespace, and metadata. + * @param controller The RPC controller. + * @param request The request containing the custodian, namespace, and metadata + * specifications. + * @param done The callback to be invoked with the response. + */ + @Override + public void disableManagedKey(RpcController controller, ManagedKeyEntryRequest request, + RpcCallback done) { + ManagedKeyResponse response = null; + ManagedKeyResponse.Builder builder = ManagedKeyResponse.newBuilder(); + try { + initManagedKeyResponseBuilder(controller, request.getKeyCustNs(), builder); + // Convert hash to metadata by looking up the key first + byte[] keyMetadataHash = request.getKeyMetadataHash().toByteArray(); + byte[] keyCust = request.getKeyCustNs().getKeyCust().toByteArray(); + String keyNamespace = request.getKeyCustNs().getKeyNamespace(); + + ManagedKeyData managedKeyState = + master.getKeymetaAdmin().disableManagedKey(keyCust, keyNamespace, keyMetadataHash); + response = generateKeyStateResponse(managedKeyState, builder); + } catch (IOException | KeyException e) { + CoprocessorRpcUtils.setControllerException(controller, new DoNotRetryIOException(e)); + builder.setKeyState(ManagedKeyState.KEY_FAILED); + } + if (response == null) { + response = builder.build(); + } + done.run(response); + } + + /** + * Rotates the managed key for a given custodian and namespace. + * @param controller The RPC controller. + * @param request The request containing the custodian and namespace specifications. + * @param done The callback to be invoked with the response. + */ + @Override + public void rotateManagedKey(RpcController controller, ManagedKeyRequest request, + RpcCallback done) { + ManagedKeyResponse response = null; + ManagedKeyResponse.Builder builder = ManagedKeyResponse.newBuilder(); + try { + initManagedKeyResponseBuilder(controller, request, builder); + ManagedKeyData managedKeyState = master.getKeymetaAdmin() + .rotateManagedKey(request.getKeyCust().toByteArray(), request.getKeyNamespace()); + response = generateKeyStateResponse(managedKeyState, builder); + } catch (IOException | KeyException e) { + CoprocessorRpcUtils.setControllerException(controller, new DoNotRetryIOException(e)); + builder.setKeyState(ManagedKeyState.KEY_FAILED); + } + if (response == null) { + response = builder.build(); + } + done.run(response); + } + + /** + * Refreshes all managed keys for a given custodian and namespace. + * @param controller The RPC controller. + * @param request The request containing the custodian and namespace specifications. + * @param done The callback to be invoked with the response. + */ + @Override + public void refreshManagedKeys(RpcController controller, ManagedKeyRequest request, + RpcCallback done) { + try { + // Do this just for validation. + initManagedKeyResponseBuilder(controller, request, ManagedKeyResponse.newBuilder()); + master.getKeymetaAdmin().refreshManagedKeys(request.getKeyCust().toByteArray(), + request.getKeyNamespace()); + } catch (IOException | KeyException e) { + CoprocessorRpcUtils.setControllerException(controller, new DoNotRetryIOException(e)); + } + done.run(EmptyMsg.getDefaultInstance()); } } @InterfaceAudience.Private public static ManagedKeyResponse.Builder initManagedKeyResponseBuilder(RpcController controller, ManagedKeyRequest request, ManagedKeyResponse.Builder builder) throws IOException { + // We need to set this in advance to make sure builder has non-null values set. builder.setKeyCust(request.getKeyCust()); builder.setKeyNamespace(request.getKeyNamespace()); if (request.getKeyCust().isEmpty()) { throw new IOException("key_cust must not be empty"); } + if (request.getKeyNamespace().isEmpty()) { + throw new IOException("key_namespace must not be empty"); + } return builder; } @@ -185,9 +296,17 @@ public static GetManagedKeysResponse generateKeyStateResponse( private static ManagedKeyResponse generateKeyStateResponse(ManagedKeyData keyData, ManagedKeyResponse.Builder builder) { - builder.setKeyState(ManagedKeysProtos.ManagedKeyState.forNumber(keyData.getKeyState().getVal())) - .setKeyMetadata(keyData.getKeyMetadata()).setRefreshTimestamp(keyData.getRefreshTimestamp()) + builder + .setKeyState(ManagedKeyState.forNumber(keyData.getKeyState().getExternalState().getVal())) + .setRefreshTimestamp(keyData.getRefreshTimestamp()) .setKeyNamespace(keyData.getKeyNamespace()); + + // Set metadata hash if available + byte[] metadataHash = keyData.getKeyMetadataHash(); + if (metadataHash != null) { + builder.setKeyMetadataHash(ByteString.copyFrom(metadataHash)); + } + return builder.build(); } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaTableAccessor.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaTableAccessor.java index e6a7d288fddb..4a291ff39d89 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaTableAccessor.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/KeymetaTableAccessor.java @@ -24,18 +24,20 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import org.apache.hadoop.hbase.HBaseInterfaceAudience; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.NamespaceDescriptor; import org.apache.hadoop.hbase.Server; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; import org.apache.hadoop.hbase.client.Connection; +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.Mutation; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.ResultScanner; +import org.apache.hadoop.hbase.client.Row; import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.client.Table; import org.apache.hadoop.hbase.client.TableDescriptorBuilder; @@ -44,8 +46,11 @@ import org.apache.hadoop.hbase.io.crypto.ManagedKeyState; import org.apache.hadoop.hbase.security.EncryptionUtil; import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.yetus.audience.InterfaceAudience; +import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; + /** * Accessor for keymeta table as part of key management. */ @@ -117,19 +122,19 @@ public void addKey(ManagedKeyData keyData) throws IOException { } /** - * Get all the keys for the specified key_cust and key_namespace. - * @param key_cust The key custodian. - * @param keyNamespace The namespace + * Get all the keys for the specified keyCust and key_namespace. + * @param keyCust The key custodian. + * @param keyNamespace The namespace + * @param includeMarkers Whether to include key management state markers in the result. * @return a list of key data, one for each key, can be empty when none were found. * @throws IOException when there is an underlying IOException. * @throws KeyException when there is an underlying KeyException. */ - @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.UNITTEST) - public List getAllKeys(byte[] key_cust, String keyNamespace) - throws IOException, KeyException { + public List getAllKeys(byte[] keyCust, String keyNamespace, + boolean includeMarkers) throws IOException, KeyException { assertKeyManagementEnabled(); Connection connection = getServer().getConnection(); - byte[] prefixForScan = constructRowKeyForCustNamespace(key_cust, keyNamespace); + byte[] prefixForScan = constructRowKeyForCustNamespace(keyCust, keyNamespace); PrefixFilter prefixFilter = new PrefixFilter(prefixForScan); Scan scan = new Scan(); scan.setFilter(prefixFilter); @@ -140,8 +145,8 @@ public List getAllKeys(byte[] key_cust, String keyNamespace) Set allKeys = new LinkedHashSet<>(); for (Result result : scanner) { ManagedKeyData keyData = - parseFromResult(getKeyManagementService(), key_cust, keyNamespace, result); - if (keyData != null) { + parseFromResult(getKeyManagementService(), keyCust, keyNamespace, result); + if (keyData != null && (includeMarkers || keyData.getKeyMetadata() != null)) { allKeys.add(keyData); } } @@ -150,75 +155,194 @@ public List getAllKeys(byte[] key_cust, String keyNamespace) } /** - * Get the active key for the specified key_cust and key_namespace. - * @param key_cust The prefix + * Get the key management state marker for the specified keyCust and key_namespace. + * @param keyCust The prefix * @param keyNamespace The namespace - * @return the active key data, or null if no active key found + * @return the key management state marker data, or null if no key management state marker found * @throws IOException when there is an underlying IOException. * @throws KeyException when there is an underlying KeyException. */ - public ManagedKeyData getActiveKey(byte[] key_cust, String keyNamespace) + public ManagedKeyData getKeyManagementStateMarker(byte[] keyCust, String keyNamespace) throws IOException, KeyException { - assertKeyManagementEnabled(); - Connection connection = getServer().getConnection(); - byte[] rowkeyForGet = constructRowKeyForCustNamespace(key_cust, keyNamespace); - Get get = new Get(rowkeyForGet); - - try (Table table = connection.getTable(KEY_META_TABLE_NAME)) { - Result result = table.get(get); - return parseFromResult(getKeyManagementService(), key_cust, keyNamespace, result); - } + return getKey(keyCust, keyNamespace, null); } /** - * Get the specific key identified by key_cust, keyNamespace and keyState. - * @param key_cust The prefix. - * @param keyNamespace The namespace. - * @param keyState The state of the key. + * Get the specific key identified by keyCust, keyNamespace and keyMetadataHash. + * @param keyCust The prefix. + * @param keyNamespace The namespace. + * @param keyMetadataHash The metadata hash. * @return the key or {@code null} * @throws IOException when there is an underlying IOException. * @throws KeyException when there is an underlying KeyException. */ - public ManagedKeyData getKey(byte[] key_cust, String keyNamespace, ManagedKeyState keyState) + public ManagedKeyData getKey(byte[] keyCust, String keyNamespace, byte[] keyMetadataHash) throws IOException, KeyException { - return getKeyInternal(key_cust, keyNamespace, new byte[] { keyState.getVal() }); + assertKeyManagementEnabled(); + Connection connection = getServer().getConnection(); + try (Table table = connection.getTable(KEY_META_TABLE_NAME)) { + byte[] rowKey = keyMetadataHash != null + ? constructRowKeyForMetadata(keyCust, keyNamespace, keyMetadataHash) + : constructRowKeyForCustNamespace(keyCust, keyNamespace); + Result result = table.get(new Get(rowKey)); + return parseFromResult(getKeyManagementService(), keyCust, keyNamespace, result); + } } /** - * Get the specific key identified by key_cust, keyNamespace and keyMetadata. - * @param key_cust The prefix. + * Disables a key by removing the wrapped key and updating its state to DISABLED. + * @param keyData The key data to disable. + * @throws IOException when there is an underlying IOException. + */ + public void disableKey(ManagedKeyData keyData) throws IOException { + assertKeyManagementEnabled(); + Preconditions.checkNotNull(keyData.getKeyMetadata(), "Key metadata cannot be null"); + byte[] keyCust = keyData.getKeyCustodian(); + String keyNamespace = keyData.getKeyNamespace(); + byte[] keyMetadataHash = keyData.getKeyMetadataHash(); + + List mutations = new ArrayList<>(3); // Max possible mutations. + + if (keyData.getKeyState() == ManagedKeyState.ACTIVE) { + // Delete the CustNamespace row + byte[] rowKeyForCustNamespace = constructRowKeyForCustNamespace(keyCust, keyNamespace); + mutations.add(new Delete(rowKeyForCustNamespace).setDurability(Durability.SKIP_WAL) + .setPriority(HConstants.SYSTEMTABLE_QOS)); + } + + // Update state to DISABLED and timestamp on Metadata row + byte[] rowKeyForMetadata = constructRowKeyForMetadata(keyCust, keyNamespace, keyMetadataHash); + addMutationsForKeyDisabled(mutations, rowKeyForMetadata, keyData.getKeyMetadata(), + keyData.getKeyState() == ManagedKeyState.ACTIVE + ? ManagedKeyState.ACTIVE_DISABLED + : ManagedKeyState.INACTIVE_DISABLED, + keyData.getKeyState()); + + Connection connection = getServer().getConnection(); + try (Table table = connection.getTable(KEY_META_TABLE_NAME)) { + table.batch(mutations, null); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while disabling key", e); + } + } + + private void addMutationsForKeyDisabled(List mutations, byte[] rowKey, String metadata, + ManagedKeyState targetState, ManagedKeyState currentState) { + Put put = new Put(rowKey); + if (metadata != null) { + put.addColumn(KEY_META_INFO_FAMILY, DEK_METADATA_QUAL_BYTES, metadata.getBytes()); + } + Put putForState = addMutationColumnsForState(put, targetState); + mutations.add(putForState); + + // Delete wrapped key columns from Metadata row + if (currentState == null || ManagedKeyState.isUsable(currentState)) { + Delete deleteWrappedKey = new Delete(rowKey).setDurability(Durability.SKIP_WAL) + .setPriority(HConstants.SYSTEMTABLE_QOS) + .addColumns(KEY_META_INFO_FAMILY, DEK_CHECKSUM_QUAL_BYTES) + .addColumns(KEY_META_INFO_FAMILY, DEK_WRAPPED_BY_STK_QUAL_BYTES) + .addColumns(KEY_META_INFO_FAMILY, STK_CHECKSUM_QUAL_BYTES); + mutations.add(deleteWrappedKey); + } + } + + /** + * Adds a key management state marker to the specified (keyCust, keyNamespace) combination. It + * also adds delete markers for the columns unrelates to marker, in case the state is + * transitioning from ACTIVE to DISABLED or FAILED. This method is only used for setting the state + * to DISABLED or FAILED. For ACTIVE state, the addKey() method implicitly adds the marker. + * @param keyCust The key custodian. * @param keyNamespace The namespace. - * @param keyMetadata The metadata. - * @return the key or {@code null} - * @throws IOException when there is an underlying IOException. - * @throws KeyException when there is an underlying KeyException. + * @param state The key management state to add. + * @throws IOException when there is an underlying IOException. */ - public ManagedKeyData getKey(byte[] key_cust, String keyNamespace, String keyMetadata) - throws IOException, KeyException { - return getKeyInternal(key_cust, keyNamespace, - ManagedKeyData.constructMetadataHash(keyMetadata)); + public void addKeyManagementStateMarker(byte[] keyCust, String keyNamespace, + ManagedKeyState state) throws IOException { + assertKeyManagementEnabled(); + Preconditions.checkArgument(ManagedKeyState.isKeyManagementState(state), + "State must be a key management state, got: " + state); + List mutations = new ArrayList<>(2); + byte[] rowKey = constructRowKeyForCustNamespace(keyCust, keyNamespace); + addMutationsForKeyDisabled(mutations, rowKey, null, state, null); + Connection connection = getServer().getConnection(); + try (Table table = connection.getTable(KEY_META_TABLE_NAME)) { + table.batch(mutations, null); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while adding key management state marker", e); + } } /** - * Internal helper method to get a key using the provided metadata hash. - * @param key_cust The prefix. - * @param keyNamespace The namespace. - * @param keyMetadataHash The metadata hash or state value. - * @return the key or {@code null} - * @throws IOException when there is an underlying IOException. - * @throws KeyException when there is an underlying KeyException. + * Updates the state of a key to one of the ACTIVE or INACTIVE states. The current state can be + * any state, but if it the same, it becomes a no-op. + * @param keyData The key data. + * @param newState The new state (must be ACTIVE or INACTIVE). + * @throws IOException when there is an underlying IOException. */ - private ManagedKeyData getKeyInternal(byte[] key_cust, String keyNamespace, - byte[] keyMetadataHash) throws IOException, KeyException { + public void updateActiveState(ManagedKeyData keyData, ManagedKeyState newState) + throws IOException { assertKeyManagementEnabled(); + ManagedKeyState currentState = keyData.getKeyState(); + + // Validate states + Preconditions.checkArgument(ManagedKeyState.isUsable(newState), + "New state must be ACTIVE or INACTIVE, got: " + newState); + // Even for FAILED keys, we expect the metadata to be non-null. + Preconditions.checkNotNull(keyData.getKeyMetadata(), "Key metadata cannot be null"); + + // No-op if states are the same + if (currentState == newState) { + return; + } + + List mutations = new ArrayList<>(2); + byte[] rowKeyForCustNamespace = constructRowKeyForCustNamespace(keyData); + byte[] rowKeyForMetadata = constructRowKeyForMetadata(keyData); + + // First take care of the active key specific row. + if (newState == ManagedKeyState.ACTIVE) { + // INACTIVE -> ACTIVE: Add CustNamespace row and update Metadata row + mutations.add(addMutationColumns(new Put(rowKeyForCustNamespace), keyData)); + } + if (currentState == ManagedKeyState.ACTIVE) { + mutations.add(new Delete(rowKeyForCustNamespace).setDurability(Durability.SKIP_WAL) + .setPriority(HConstants.SYSTEMTABLE_QOS)); + } + + // Now take care of the key specific row (for point gets by metadata). + if (!ManagedKeyState.isUsable(currentState)) { + // For DISABLED and FAILED keys, we don't expect cached key material, so add all columns + // similar to what addKey() does. + mutations.add(addMutationColumns(new Put(rowKeyForMetadata), keyData)); + } else { + // We expect cached key material, so only update the state and timestamp columns. + mutations.add(addMutationColumnsForState(new Put(rowKeyForMetadata), newState)); + } + Connection connection = getServer().getConnection(); try (Table table = connection.getTable(KEY_META_TABLE_NAME)) { - byte[] rowKey = constructRowKeyForMetadata(key_cust, keyNamespace, keyMetadataHash); - Result result = table.get(new Get(rowKey)); - return parseFromResult(getKeyManagementService(), key_cust, keyNamespace, result); + table.batch(mutations, null); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while updating active state", e); } } + private Put addMutationColumnsForState(Put put, ManagedKeyState newState) { + return addMutationColumnsForState(put, newState, EnvironmentEdgeManager.currentTime()); + } + + /** + * Add only state and timestamp columns to the given Put. + */ + private Put addMutationColumnsForState(Put put, ManagedKeyState newState, long timestamp) { + return put.setDurability(Durability.SKIP_WAL).setPriority(HConstants.SYSTEMTABLE_QOS) + .addColumn(KEY_META_INFO_FAMILY, KEY_STATE_QUAL_BYTES, new byte[] { newState.getVal() }) + .addColumn(KEY_META_INFO_FAMILY, REFRESHED_TIMESTAMP_QUAL_BYTES, Bytes.toBytes(timestamp)); + } + /** * Add the mutation columns to the given Put that are derived from the keyData. */ @@ -235,11 +359,8 @@ private Put addMutationColumns(Put put, ManagedKeyData keyData) throws IOExcepti .addColumn(KEY_META_INFO_FAMILY, STK_CHECKSUM_QUAL_BYTES, Bytes.toBytes(latestSystemKey.getKeyChecksum())); } - Put result = put.setDurability(Durability.SKIP_WAL).setPriority(HConstants.SYSTEMTABLE_QOS) - .addColumn(KEY_META_INFO_FAMILY, REFRESHED_TIMESTAMP_QUAL_BYTES, - Bytes.toBytes(keyData.getRefreshTimestamp())) - .addColumn(KEY_META_INFO_FAMILY, KEY_STATE_QUAL_BYTES, - new byte[] { keyData.getKeyState().getVal() }); + Put result = + addMutationColumnsForState(put, keyData.getKeyState(), keyData.getRefreshTimestamp()); // Only add metadata column if metadata is not null String metadata = keyData.getKeyMetadata(); @@ -252,21 +373,15 @@ private Put addMutationColumns(Put put, ManagedKeyData keyData) throws IOExcepti @InterfaceAudience.Private public static byte[] constructRowKeyForMetadata(ManagedKeyData keyData) { - byte[] keyMetadataHash; - if (keyData.getKeyState() == ManagedKeyState.FAILED && keyData.getKeyMetadata() == null) { - // For FAILED state with null metadata, use state as metadata - keyMetadataHash = new byte[] { keyData.getKeyState().getVal() }; - } else { - keyMetadataHash = keyData.getKeyMetadataHash(); - } + Preconditions.checkNotNull(keyData.getKeyMetadata(), "Key metadata cannot be null"); return constructRowKeyForMetadata(keyData.getKeyCustodian(), keyData.getKeyNamespace(), - keyMetadataHash); + keyData.getKeyMetadataHash()); } @InterfaceAudience.Private - public static byte[] constructRowKeyForMetadata(byte[] key_cust, String keyNamespace, + public static byte[] constructRowKeyForMetadata(byte[] keyCust, String keyNamespace, byte[] keyMetadataHash) { - return Bytes.add(constructRowKeyForCustNamespace(key_cust, keyNamespace), keyMetadataHash); + return Bytes.add(constructRowKeyForCustNamespace(keyCust, keyNamespace), keyMetadataHash); } @InterfaceAudience.Private @@ -275,14 +390,14 @@ public static byte[] constructRowKeyForCustNamespace(ManagedKeyData keyData) { } @InterfaceAudience.Private - public static byte[] constructRowKeyForCustNamespace(byte[] key_cust, String keyNamespace) { - int custLength = key_cust.length; - return Bytes.add(Bytes.toBytes(custLength), key_cust, Bytes.toBytes(keyNamespace)); + public static byte[] constructRowKeyForCustNamespace(byte[] keyCust, String keyNamespace) { + int custLength = keyCust.length; + return Bytes.add(Bytes.toBytes(custLength), keyCust, Bytes.toBytes(keyNamespace)); } @InterfaceAudience.Private public static ManagedKeyData parseFromResult(KeyManagementService keyManagementService, - byte[] key_cust, String keyNamespace, Result result) throws IOException, KeyException { + byte[] keyCust, String keyNamespace, Result result) throws IOException, KeyException { if (result == null || result.isEmpty()) { return null; } @@ -313,16 +428,24 @@ public static ManagedKeyData parseFromResult(KeyManagementService keyManagementS } long refreshedTimestamp = Bytes.toLong(result.getValue(KEY_META_INFO_FAMILY, REFRESHED_TIMESTAMP_QUAL_BYTES)); - ManagedKeyData dekKeyData = - new ManagedKeyData(key_cust, keyNamespace, dek, keyState, dekMetadata, refreshedTimestamp); - if (dek != null) { - long dekChecksum = - Bytes.toLong(result.getValue(KEY_META_INFO_FAMILY, DEK_CHECKSUM_QUAL_BYTES)); - if (dekKeyData.getKeyChecksum() != dekChecksum) { - LOG.error("Dropping key, current key checksum: {} didn't match the expected checksum: {}" - + " for key with metadata: {}", dekKeyData.getKeyChecksum(), dekChecksum, dekMetadata); - return null; + ManagedKeyData dekKeyData; + if (dekMetadata != null) { + dekKeyData = + new ManagedKeyData(keyCust, keyNamespace, dek, keyState, dekMetadata, refreshedTimestamp); + if (dek != null) { + long dekChecksum = + Bytes.toLong(result.getValue(KEY_META_INFO_FAMILY, DEK_CHECKSUM_QUAL_BYTES)); + if (dekKeyData.getKeyChecksum() != dekChecksum) { + LOG.error( + "Dropping key, current key checksum: {} didn't match the expected checksum: {}" + + " for key with metadata: {}", + dekKeyData.getKeyChecksum(), dekChecksum, dekMetadata); + dekKeyData = null; + } } + } else { + // Key management marker. + dekKeyData = new ManagedKeyData(keyCust, keyNamespace, keyState, refreshedTimestamp); } return dekKeyData; } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/ManagedKeyDataCache.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/ManagedKeyDataCache.java index 76a6bdb8f915..f93706690ded 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/ManagedKeyDataCache.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/ManagedKeyDataCache.java @@ -22,6 +22,9 @@ import java.io.IOException; import java.security.KeyException; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseInterfaceAudience; import org.apache.hadoop.hbase.HConstants; @@ -34,7 +37,7 @@ import org.slf4j.LoggerFactory; /** - * In-memory cache for ManagedKeyData entries, using key metadata as the cache key. Uses two + * In-memory cache for ManagedKeyData entries, using key metadata hash as the cache key. Uses two * independent Caffeine caches: one for general key data and one for active keys only with * hierarchical structure for efficient single key retrieval. */ @@ -42,7 +45,7 @@ public class ManagedKeyDataCache extends KeyManagementBase { private static final Logger LOG = LoggerFactory.getLogger(ManagedKeyDataCache.class); - private Cache cacheByMetadata; + private Cache cacheByMetadataHash; // Key is Bytes wrapper around hash private Cache activeKeysCache; private final KeymetaTableAccessor keymetaAccessor; @@ -97,14 +100,30 @@ public ManagedKeyDataCache(Configuration conf, KeymetaTableAccessor keymetaAcces int activeKeysMaxEntries = conf.getInt(HConstants.CRYPTO_MANAGED_KEYS_L1_ACTIVE_CACHE_MAX_NS_ENTRIES_CONF_KEY, HConstants.CRYPTO_MANAGED_KEYS_L1_ACTIVE_CACHE_MAX_NS_ENTRIES_DEFAULT); - this.cacheByMetadata = Caffeine.newBuilder().maximumSize(maxEntries).build(); + this.cacheByMetadataHash = Caffeine.newBuilder().maximumSize(maxEntries).build(); this.activeKeysCache = Caffeine.newBuilder().maximumSize(activeKeysMaxEntries).build(); } + /** + * Retrieves an entry from the cache, if it already exists, otherwise a null is returned. No + * attempt will be made to load from L2 or provider. + * @return the corresponding ManagedKeyData entry, or null if not found + */ + public ManagedKeyData getEntry(byte[] keyCust, String keyNamespace, byte[] keyMetadataHash) + throws IOException, KeyException { + Bytes metadataHashKey = new Bytes(keyMetadataHash); + // Return the entry if it exists in the generic cache or active keys cache, otherwise return + // null. + ManagedKeyData entry = cacheByMetadataHash.get(metadataHashKey, hashKey -> { + return getFromActiveKeysCache(keyCust, keyNamespace, keyMetadataHash); + }); + return entry; + } + /** * Retrieves an entry from the cache, loading it from L2 if KeymetaTableAccessor is available. * When L2 is not available, it will try to load from provider, unless dynamic lookup is disabled. - * @param key_cust the key custodian + * @param keyCust the key custodian * @param keyNamespace the key namespace * @param keyMetadata the key metadata of the entry to be retrieved * @param wrappedKey The DEK key material encrypted with the corresponding KEK, if available. @@ -112,89 +131,154 @@ public ManagedKeyDataCache(Configuration conf, KeymetaTableAccessor keymetaAcces * @throws IOException if an error occurs while loading from KeymetaTableAccessor * @throws KeyException if an error occurs while loading from KeymetaTableAccessor */ - public ManagedKeyData getEntry(byte[] key_cust, String keyNamespace, String keyMetadata, + public ManagedKeyData getEntry(byte[] keyCust, String keyNamespace, String keyMetadata, byte[] wrappedKey) throws IOException, KeyException { - ManagedKeyData entry = cacheByMetadata.get(keyMetadata, metadata -> { + // Compute hash and use it as cache key + byte[] metadataHashBytes = ManagedKeyData.constructMetadataHash(keyMetadata); + Bytes metadataHashKey = new Bytes(metadataHashBytes); + + ManagedKeyData entry = cacheByMetadataHash.get(metadataHashKey, hashKey -> { // First check if it's in the active keys cache - ManagedKeyData keyData = getFromActiveKeysCache(key_cust, keyNamespace, keyMetadata); + ManagedKeyData keyData = getFromActiveKeysCache(keyCust, keyNamespace, metadataHashBytes); // Try to load from L2 if (keyData == null && keymetaAccessor != null) { try { - keyData = keymetaAccessor.getKey(key_cust, keyNamespace, metadata); - } catch (IOException | KeyException | RuntimeException e) { - LOG.warn("Failed to load key from KeymetaTableAccessor for metadata: {}", metadata, e); + keyData = keymetaAccessor.getKey(keyCust, keyNamespace, metadataHashBytes); + } catch (IOException | KeyException e) { + LOG.warn( + "Failed to load key from L2 for (custodian: {}, namespace: {}) with metadata hash: {}", + ManagedKeyProvider.encodeToStr(keyCust), keyNamespace, + ManagedKeyProvider.encodeToStr(metadataHashBytes), e); } } // If not found in L2 and dynamic lookup is enabled, try with Key Provider if (keyData == null && isDynamicLookupEnabled()) { + String encKeyCust = ManagedKeyProvider.encodeToStr(keyCust); try { - ManagedKeyProvider provider = getKeyProvider(); - keyData = provider.unwrapKey(metadata, wrappedKey); - LOG.info("Got key data with status: {} and metadata: {} for prefix: {}", - keyData.getKeyState(), keyData.getKeyMetadata(), - ManagedKeyProvider.encodeToStr(key_cust)); - // Add to KeymetaTableAccessor for future L2 lookups - if (keymetaAccessor != null) { - try { - keymetaAccessor.addKey(keyData); - } catch (IOException | RuntimeException e) { - LOG.warn("Failed to add key to KeymetaTableAccessor for metadata: {}", metadata, e); - } - } - } catch (IOException | RuntimeException e) { - LOG.warn("Failed to load key from provider for metadata: {}", metadata, e); + keyData = KeyManagementUtils.retrieveKey(getKeyProvider(), keymetaAccessor, encKeyCust, + keyCust, keyNamespace, keyMetadata, wrappedKey); + } catch (IOException | KeyException | RuntimeException e) { + LOG.warn( + "Failed to retrieve key from provider for (custodian: {}, namespace: {}) with " + + "metadata hash: {}", ManagedKeyProvider.encodeToStr(keyCust), keyNamespace, + ManagedKeyProvider.encodeToStr(metadataHashBytes), e); } } if (keyData == null) { keyData = - new ManagedKeyData(key_cust, keyNamespace, null, ManagedKeyState.FAILED, keyMetadata); + new ManagedKeyData(keyCust, keyNamespace, null, ManagedKeyState.FAILED, keyMetadata); } // Also update activeKeysCache if relevant and is missing. if (keyData.getKeyState() == ManagedKeyState.ACTIVE) { - activeKeysCache.asMap().putIfAbsent(new ActiveKeysCacheKey(key_cust, keyNamespace), - keyData); + activeKeysCache.asMap().putIfAbsent(new ActiveKeysCacheKey(keyCust, keyNamespace), keyData); } - if (!ManagedKeyState.isUsable(keyData.getKeyState())) { - LOG.info("Failed to get usable key data with metadata: {} for prefix: {}", metadata, - ManagedKeyProvider.encodeToStr(key_cust)); - } return keyData; }); - // This should never be null, but adding a check just to satisfy spotbugs. + + // Verify custodian/namespace match to guard against hash collisions if (entry != null && ManagedKeyState.isUsable(entry.getKeyState())) { - return entry; + if ( + Bytes.equals(entry.getKeyCustodian(), keyCust) + && entry.getKeyNamespace().equals(keyNamespace) + ) { + return entry; + } + LOG.warn( + "Hash collision or incorrect/mismatched custodian/namespace detected for metadata hash: " + + "{} - custodian/namespace mismatch expected: ({}, {}), actual: ({}, {})", + ManagedKeyProvider.encodeToStr(metadataHashBytes), ManagedKeyProvider.encodeToStr(keyCust), + keyNamespace, ManagedKeyProvider.encodeToStr(entry.getKeyCustodian()), + entry.getKeyNamespace()); } return null; } /** * Retrieves an existing key from the active keys cache. - * @param key_cust the key custodian - * @param keyNamespace the key namespace - * @param keyMetadata the key metadata + * @param keyCust the key custodian + * @param keyNamespace the key namespace + * @param keyMetadataHash the key metadata hash * @return the ManagedKeyData if found, null otherwise */ - private ManagedKeyData getFromActiveKeysCache(byte[] key_cust, String keyNamespace, - String keyMetadata) { - ActiveKeysCacheKey cacheKey = new ActiveKeysCacheKey(key_cust, keyNamespace); + private ManagedKeyData getFromActiveKeysCache(byte[] keyCust, String keyNamespace, + byte[] keyMetadataHash) { + ActiveKeysCacheKey cacheKey = new ActiveKeysCacheKey(keyCust, keyNamespace); ManagedKeyData keyData = activeKeysCache.getIfPresent(cacheKey); - if (keyData != null && keyData.getKeyMetadata().equals(keyMetadata)) { + if (keyData != null && Bytes.equals(keyData.getKeyMetadataHash(), keyMetadataHash)) { return keyData; } return null; } + /** + * Eject the key identified by the given custodian, namespace and metadata from both the active + * keys cache and the generic cache. + * @param keyCust the key custodian + * @param keyNamespace the key namespace + * @param keyMetadataHash the key metadata hash + * @return true if the key was ejected from either cache, false otherwise + */ + public boolean ejectKey(byte[] keyCust, String keyNamespace, byte[] keyMetadataHash) { + Bytes keyMetadataHashKey = new Bytes(keyMetadataHash); + ActiveKeysCacheKey cacheKey = new ActiveKeysCacheKey(keyCust, keyNamespace); + AtomicBoolean ejected = new AtomicBoolean(false); + AtomicReference rejectedValue = new AtomicReference<>(null); + + Function conditionalCompute = (value) -> { + if (rejectedValue.get() != null) { + return value; + } + if ( + Bytes.equals(value.getKeyMetadataHash(), keyMetadataHash) + && Bytes.equals(value.getKeyCustodian(), keyCust) + && value.getKeyNamespace().equals(keyNamespace) + ) { + ejected.set(true); + return null; + } + rejectedValue.set(value); + return value; + }; + + // Try to eject from active keys cache by matching hash with collision check + activeKeysCache.asMap().computeIfPresent(cacheKey, + (key, value) -> conditionalCompute.apply(value)); + + // Also remove from generic cache by hash, with collision check + cacheByMetadataHash.asMap().computeIfPresent(keyMetadataHashKey, + (hash, value) -> conditionalCompute.apply(value)); + + if (rejectedValue.get() != null) { + LOG.warn( + "Hash collision or incorrect/mismatched custodian/namespace detected for metadata " + + "hash: {} - custodian/namespace mismatch expected: ({}, {}), actual: ({}, {})", + ManagedKeyProvider.encodeToStr(keyMetadataHash), ManagedKeyProvider.encodeToStr(keyCust), + keyNamespace, ManagedKeyProvider.encodeToStr(rejectedValue.get().getKeyCustodian()), + rejectedValue.get().getKeyNamespace()); + } + + return ejected.get(); + } + + /** + * Clear all the cached entries. + */ + public void clearCache() { + cacheByMetadataHash.invalidateAll(); + activeKeysCache.invalidateAll(); + } + /** * @return the approximate number of entries in the main cache which is meant for general lookup - * by key metadata. + * by key metadata hash. */ public int getGenericCacheEntryCount() { - return (int) cacheByMetadata.estimatedSize(); + return (int) cacheByMetadataHash.estimatedSize(); } /** Returns the approximate number of entries in the active keys cache */ @@ -205,13 +289,13 @@ public int getActiveCacheEntryCount() { /** * Retrieves the active entry from the cache based on its key custodian and key namespace. This * method also loads active keys from provider if not found in cache. - * @param key_cust The key custodian. + * @param keyCust The key custodian. * @param keyNamespace the key namespace to search for * @return the ManagedKeyData entry with the given custodian and ACTIVE status, or null if not * found */ - public ManagedKeyData getActiveEntry(byte[] key_cust, String keyNamespace) { - ActiveKeysCacheKey cacheKey = new ActiveKeysCacheKey(key_cust, keyNamespace); + public ManagedKeyData getActiveEntry(byte[] keyCust, String keyNamespace) { + ActiveKeysCacheKey cacheKey = new ActiveKeysCacheKey(keyCust, keyNamespace); ManagedKeyData keyData = activeKeysCache.get(cacheKey, key -> { ManagedKeyData retrievedKey = null; @@ -219,10 +303,10 @@ public ManagedKeyData getActiveEntry(byte[] key_cust, String keyNamespace) { // Try to load from KeymetaTableAccessor if not found in cache if (keymetaAccessor != null) { try { - retrievedKey = keymetaAccessor.getActiveKey(key_cust, keyNamespace); + retrievedKey = keymetaAccessor.getKeyManagementStateMarker(keyCust, keyNamespace); } catch (IOException | KeyException | RuntimeException e) { LOG.warn("Failed to load active key from KeymetaTableAccessor for custodian: {} " - + "namespace: {}", ManagedKeyProvider.encodeToStr(key_cust), keyNamespace, e); + + "namespace: {}", ManagedKeyProvider.encodeToStr(keyCust), keyNamespace, e); } } @@ -230,17 +314,17 @@ public ManagedKeyData getActiveEntry(byte[] key_cust, String keyNamespace) { // standalone tools. if (retrievedKey == null && isDynamicLookupEnabled()) { try { - String keyCust = ManagedKeyProvider.encodeToStr(key_cust); - retrievedKey = retrieveActiveKey(keyCust, key_cust, keyNamespace, keymetaAccessor, null); + String keyCustEnc = ManagedKeyProvider.encodeToStr(keyCust); + retrievedKey = KeyManagementUtils.retrieveActiveKey(getKeyProvider(), keymetaAccessor, + keyCustEnc, keyCust, keyNamespace, null); } catch (IOException | KeyException | RuntimeException e) { LOG.warn("Failed to load active key from provider for custodian: {} namespace: {}", - ManagedKeyProvider.encodeToStr(key_cust), keyNamespace, e); + ManagedKeyProvider.encodeToStr(keyCust), keyNamespace, e); } } if (retrievedKey == null) { - retrievedKey = - new ManagedKeyData(key_cust, keyNamespace, null, ManagedKeyState.FAILED, null); + retrievedKey = new ManagedKeyData(keyCust, keyNamespace, ManagedKeyState.FAILED); } return retrievedKey; @@ -252,12 +336,4 @@ public ManagedKeyData getActiveEntry(byte[] key_cust, String keyNamespace) { } return null; } - - /** - * Invalidates all entries in the cache. - */ - public void invalidateAll() { - cacheByMetadata.invalidateAll(); - activeKeysCache.invalidateAll(); - } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/SystemKeyCache.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/SystemKeyCache.java index bcdf2ae11cf0..b01af650d764 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/SystemKeyCache.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/keymeta/SystemKeyCache.java @@ -65,8 +65,12 @@ public static SystemKeyCache createCache(SystemKeyAccessor accessor) throws IOEx ManagedKeyData latestSystemKey = null; Map systemKeys = new TreeMap<>(); for (Path keyPath : allSystemKeyFiles) { - LOG.info("Loading system key from: {}", keyPath); ManagedKeyData keyData = accessor.loadSystemKey(keyPath); + LOG.info( + "Loaded system key with (custodian: {}, namespace: {}), checksum: {} and metadata hash: {} " + + " from file: {}", + keyData.getKeyCustodianEncoded(), keyData.getKeyNamespace(), keyData.getKeyChecksum(), + keyData.getKeyMetadataHashEncoded(), keyPath); if (latestSystemKey == null) { latestSystemKey = keyData; } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java index c63a1e7e8ecf..dbb56899b91f 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java @@ -193,7 +193,9 @@ import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos.RegionStoreSequenceIds; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BooleanMsg; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.NameStringPair; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ProcedureDescription; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.RegionSpecifier; @@ -3628,6 +3630,18 @@ public EmptyMsg refreshSystemKeyCache(RpcController controller, EmptyMsg request throw new ServiceException(new DoNotRetryIOException("Unsupported method on master")); } + @Override + public BooleanMsg ejectManagedKeyDataCacheEntry(RpcController controller, + ManagedKeyEntryRequest request) throws ServiceException { + throw new ServiceException(new DoNotRetryIOException("Unsupported method on master")); + } + + @Override + public EmptyMsg clearManagedKeyDataCache(RpcController controller, EmptyMsg request) + throws ServiceException { + throw new ServiceException(new DoNotRetryIOException("Unsupported method on master")); + } + @Override public GetLiveRegionServersResponse getLiveRegionServers(RpcController controller, GetLiveRegionServersRequest request) throws ServiceException { diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java index ed2f81a947d8..61bd92821de7 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java @@ -90,6 +90,7 @@ import org.apache.hadoop.hbase.exceptions.TimeoutIOException; import org.apache.hadoop.hbase.exceptions.UnknownProtocolException; import org.apache.hadoop.hbase.io.ByteBuffAllocator; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyProvider; import org.apache.hadoop.hbase.io.hfile.BlockCache; import org.apache.hadoop.hbase.ipc.HBaseRpcController; import org.apache.hadoop.hbase.ipc.PriorityFunction; @@ -234,7 +235,9 @@ import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.ScanResponse; import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.ClusterStatusProtos.RegionLoad; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BooleanMsg; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.NameBytesPair; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.NameInt64Pair; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.RegionSpecifier; @@ -4082,6 +4085,63 @@ public EmptyMsg refreshSystemKeyCache(final RpcController controller, final Empt } } + /** + * Ejects a specific managed key entry from the managed key data cache on the region server. + * @param controller the RPC controller + * @param request the request containing key custodian, namespace, and metadata hash + * @return BooleanMsg indicating whether the key was ejected + */ + @Override + @QosPriority(priority = HConstants.ADMIN_QOS) + public BooleanMsg ejectManagedKeyDataCacheEntry(final RpcController controller, + final ManagedKeyEntryRequest request) throws ServiceException { + try { + checkOpen(); + } catch (IOException e) { + LOG.error("Failed to eject managed key data cache entry", e); + throw new ServiceException(e); + } + requestCount.increment(); + byte[] keyCustodian = request.getKeyCustNs().getKeyCust().toByteArray(); + String keyNamespace = request.getKeyCustNs().getKeyNamespace(); + byte[] keyMetadataHash = request.getKeyMetadataHash().toByteArray(); + + if (LOG.isInfoEnabled()) { + String keyCustodianEncoded = ManagedKeyProvider.encodeToStr(keyCustodian); + String keyMetadataHashEncoded = ManagedKeyProvider.encodeToStr(keyMetadataHash); + LOG.info( + "Received EjectManagedKeyDataCacheEntry request for key custodian: {}, namespace: {}, " + + "metadata hash: {}", + keyCustodianEncoded, keyNamespace, keyMetadataHashEncoded); + } + + boolean ejected = server.getKeyManagementService().getManagedKeyDataCache() + .ejectKey(keyCustodian, keyNamespace, keyMetadataHash); + return BooleanMsg.newBuilder().setBoolMsg(ejected).build(); + } + + /** + * Clears all entries in the managed key data cache on the region server. + * @param controller the RPC controller + * @param request the request (empty) + * @return empty response + */ + @Override + @QosPriority(priority = HConstants.ADMIN_QOS) + public EmptyMsg clearManagedKeyDataCache(final RpcController controller, final EmptyMsg request) + throws ServiceException { + try { + checkOpen(); + } catch (IOException ie) { + LOG.error("Failed to clear managed key data cache", ie); + throw new ServiceException(ie); + } + requestCount.increment(); + LOG.info("Received ClearManagedKeyDataCache request, clearing managed key data cache"); + server.getKeyManagementService().getManagedKeyDataCache().clearCache(); + return EmptyMsg.getDefaultInstance(); + } + RegionScannerContext checkQuotaAndGetRegionScannerContext(ScanRequest request, ScanResponse.Builder builder) throws IOException { if (request.hasScannerId()) { diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/SecurityUtil.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/SecurityUtil.java index b104f4687608..5fff2a417ebc 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/SecurityUtil.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/SecurityUtil.java @@ -42,9 +42,13 @@ */ @InterfaceAudience.Private @InterfaceStability.Evolving -public class SecurityUtil { +public final class SecurityUtil { private static final Logger LOG = LoggerFactory.getLogger(SecurityUtil.class); + private SecurityUtil() { + // Utility class + } + /** * Get the user name from a principal */ @@ -134,8 +138,11 @@ public static Encryption.Context createEncryptionContext(Configuration conf, if (candidate != null) { // Log information on the table and column family we are looking for the active key in if (LOG.isDebugEnabled()) { - LOG.debug("Looking for active key for table: {} and column family: {}", - tableDescriptor.getTableName().getNameAsString(), family.getNameAsString()); + LOG.debug( + "Looking for active key for table: {} and column family: {} with " + + "(custodian: {}, namespace: {})", + tableDescriptor.getTableName().getNameAsString(), family.getNameAsString(), + ManagedKeyData.KEY_GLOBAL_CUSTODIAN, candidate); } activeKeyData = managedKeyDataCache .getActiveEntry(ManagedKeyData.KEY_GLOBAL_CUSTODIAN_BYTES, candidate); @@ -161,11 +168,11 @@ public static Encryption.Context createEncryptionContext(Configuration conf, } if (LOG.isDebugEnabled()) { LOG.debug( - "Scenario 2: Use active key for namespace {} cipher: {} " + "Scenario 2: Use active key with (custodian: {}, namespace: {}) for cipher: {} " + "localKeyGenEnabled: {} for table: {} and column family: {}", - keyNamespace, cipherName, localKeyGenEnabled, - tableDescriptor.getTableName().getNameAsString(), family.getNameAsString(), - activeKeyData.getKeyNamespace()); + activeKeyData.getKeyCustodianEncoded(), activeKeyData.getKeyNamespace(), cipherName, + localKeyGenEnabled, tableDescriptor.getTableName().getNameAsString(), + family.getNameAsString()); } } else { if (LOG.isDebugEnabled()) { @@ -185,6 +192,12 @@ public static Encryption.Context createEncryptionContext(Configuration conf, } } } + if (LOG.isDebugEnabled() && kekKeyData != null) { + LOG.debug( + "Usigng KEK with (custodian: {}, namespace: {}), checksum: {} and metadata " + "hash: {}", + kekKeyData.getKeyCustodianEncoded(), kekKeyData.getKeyNamespace(), + kekKeyData.getKeyChecksum(), kekKeyData.getKeyMetadataHashEncoded()); + } if (cipher == null) { cipher = @@ -234,13 +247,21 @@ public static Encryption.Context createEncryptionContext(Configuration conf, Pat "Seeing newer trailer with KEK checksum, but key management is disabled"); } - // Try STK lookup first if checksum is available and system key cache is not null. - if (trailer.getKEKChecksum() != 0L && trailer.getKeyNamespace() == null) { + // Try STK lookup first if checksum is available. + if (trailer.getKEKChecksum() != 0L) { + LOG.debug("Looking for System Key with checksum: {}", trailer.getKEKChecksum()); ManagedKeyData systemKeyData = systemKeyCache.getSystemKeyByChecksum(trailer.getKEKChecksum()); if (systemKeyData != null) { kek = systemKeyData.getTheKey(); kekKeyData = systemKeyData; + if (LOG.isDebugEnabled()) { + LOG.debug( + "Found System Key with (custodian: {}, namespace: {}), checksum: {} and " + + "metadata hash: {}", + systemKeyData.getKeyCustodianEncoded(), systemKeyData.getKeyNamespace(), + systemKeyData.getKeyChecksum(), systemKeyData.getKeyMetadataHashEncoded()); + } } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ModifyRegionUtils.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ModifyRegionUtils.java index db7a9422b75e..d91cd9b78615 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ModifyRegionUtils.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ModifyRegionUtils.java @@ -85,7 +85,7 @@ public static RegionInfo[] createRegionInfos(TableDescriptor tableDescriptor, /** * Create new set of regions on the specified file-system. NOTE: that you should add the regions * to hbase:meta after this operation. - * @param conf {@link Configuration} + * @param env {@link MasterProcedureEnv} * @param rootDir Root directory for HBase instance * @param tableDescriptor description of the table * @param newRegions {@link RegionInfo} that describes the regions to create diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeyManagementUtils.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeyManagementUtils.java new file mode 100644 index 000000000000..36df6a32ccd8 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeyManagementUtils.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.keymeta; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.Key; +import java.security.KeyException; +import javax.crypto.KeyGenerator; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyProvider; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyState; +import org.apache.hadoop.hbase.testclassification.MasterTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +/** + * Tests KeyManagementUtils for the difficult to cover error paths. + */ +@Category({ MasterTests.class, SmallTests.class }) +public class TestKeyManagementUtils { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestKeyManagementUtils.class); + + private ManagedKeyProvider mockProvider; + private KeymetaTableAccessor mockAccessor; + private byte[] keyCust; + private String keyNamespace; + private String keyMetadata; + private byte[] wrappedKey; + private Key testKey; + + @Before + public void setUp() throws Exception { + mockProvider = mock(ManagedKeyProvider.class); + mockAccessor = mock(KeymetaTableAccessor.class); + keyCust = "testCustodian".getBytes(); + keyNamespace = "testNamespace"; + keyMetadata = "testMetadata"; + wrappedKey = new byte[] { 1, 2, 3, 4 }; + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + testKey = keyGen.generateKey(); + } + + @Test + public void testRetrieveKeyWithNullResponse() throws Exception { + String encKeyCust = ManagedKeyProvider.encodeToStr(keyCust); + when(mockProvider.unwrapKey(any(), any())).thenReturn(null); + + KeyException exception = assertThrows(KeyException.class, () -> { + KeyManagementUtils.retrieveKey(mockProvider, mockAccessor, encKeyCust, keyCust, keyNamespace, + keyMetadata, wrappedKey); + }); + + assertNotNull(exception.getMessage()); + assertEquals(true, exception.getMessage().contains("Invalid key that is null")); + } + + @Test + public void testRetrieveKeyWithNullMetadata() throws Exception { + String encKeyCust = ManagedKeyProvider.encodeToStr(keyCust); + // Create a mock that returns null for getKeyMetadata() + ManagedKeyData mockKeyData = mock(ManagedKeyData.class); + when(mockKeyData.getKeyMetadata()).thenReturn(null); + when(mockProvider.unwrapKey(any(), any())).thenReturn(mockKeyData); + + KeyException exception = assertThrows(KeyException.class, () -> { + KeyManagementUtils.retrieveKey(mockProvider, mockAccessor, encKeyCust, keyCust, keyNamespace, + keyMetadata, wrappedKey); + }); + + assertNotNull(exception.getMessage()); + assertEquals(true, exception.getMessage().contains("Invalid key that is null")); + } + + @Test + public void testRetrieveKeyWithMismatchedMetadata() throws Exception { + String encKeyCust = ManagedKeyProvider.encodeToStr(keyCust); + String differentMetadata = "differentMetadata"; + ManagedKeyData keyDataWithDifferentMetadata = + new ManagedKeyData(keyCust, keyNamespace, testKey, ManagedKeyState.ACTIVE, differentMetadata); + when(mockProvider.unwrapKey(any(), any())).thenReturn(keyDataWithDifferentMetadata); + + KeyException exception = assertThrows(KeyException.class, () -> { + KeyManagementUtils.retrieveKey(mockProvider, mockAccessor, encKeyCust, keyCust, keyNamespace, + keyMetadata, wrappedKey); + }); + + assertNotNull(exception.getMessage()); + assertEquals(true, exception.getMessage().contains("invalid metadata")); + } + + @Test + public void testRetrieveKeyWithDisabledState() throws Exception { + String encKeyCust = ManagedKeyProvider.encodeToStr(keyCust); + ManagedKeyData keyDataWithDisabledState = + new ManagedKeyData(keyCust, keyNamespace, testKey, ManagedKeyState.DISABLED, keyMetadata); + when(mockProvider.unwrapKey(any(), any())).thenReturn(keyDataWithDisabledState); + + KeyException exception = assertThrows(KeyException.class, () -> { + KeyManagementUtils.retrieveKey(mockProvider, mockAccessor, encKeyCust, keyCust, keyNamespace, + keyMetadata, wrappedKey); + }); + + assertNotNull(exception.getMessage()); + assertEquals(true, + exception.getMessage().contains("Invalid key that is null or having invalid metadata")); + } + + @Test + public void testRetrieveKeySuccess() throws Exception { + String encKeyCust = ManagedKeyProvider.encodeToStr(keyCust); + ManagedKeyData validKeyData = + new ManagedKeyData(keyCust, keyNamespace, testKey, ManagedKeyState.ACTIVE, keyMetadata); + when(mockProvider.unwrapKey(any(), any())).thenReturn(validKeyData); + + ManagedKeyData result = KeyManagementUtils.retrieveKey(mockProvider, mockAccessor, encKeyCust, + keyCust, keyNamespace, keyMetadata, wrappedKey); + + assertNotNull(result); + assertEquals(keyMetadata, result.getKeyMetadata()); + assertEquals(ManagedKeyState.ACTIVE, result.getKeyState()); + } + + @Test + public void testRetrieveKeyWithFailedState() throws Exception { + // FAILED state is allowed (unlike DISABLED), so this should succeed + String encKeyCust = ManagedKeyProvider.encodeToStr(keyCust); + ManagedKeyData keyDataWithFailedState = + new ManagedKeyData(keyCust, keyNamespace, null, ManagedKeyState.FAILED, keyMetadata); + when(mockProvider.unwrapKey(any(), any())).thenReturn(keyDataWithFailedState); + + ManagedKeyData result = KeyManagementUtils.retrieveKey(mockProvider, mockAccessor, encKeyCust, + keyCust, keyNamespace, keyMetadata, wrappedKey); + + assertNotNull(result); + assertEquals(ManagedKeyState.FAILED, result.getKeyState()); + } + + @Test + public void testRetrieveKeyWithInactiveState() throws Exception { + // INACTIVE state is allowed, so this should succeed + String encKeyCust = ManagedKeyProvider.encodeToStr(keyCust); + ManagedKeyData keyDataWithInactiveState = + new ManagedKeyData(keyCust, keyNamespace, testKey, ManagedKeyState.INACTIVE, keyMetadata); + when(mockProvider.unwrapKey(any(), any())).thenReturn(keyDataWithInactiveState); + + ManagedKeyData result = KeyManagementUtils.retrieveKey(mockProvider, mockAccessor, encKeyCust, + keyCust, keyNamespace, keyMetadata, wrappedKey); + + assertNotNull(result); + assertEquals(ManagedKeyState.INACTIVE, result.getKeyState()); + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeymetaEndpoint.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeymetaEndpoint.java index 56c92c873c03..0e9c0eae2393 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeymetaEndpoint.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeymetaEndpoint.java @@ -18,6 +18,7 @@ package org.apache.hadoop.hbase.keymeta; import static org.apache.hadoop.hbase.io.crypto.ManagedKeyState.ACTIVE; +import static org.apache.hadoop.hbase.io.crypto.ManagedKeyState.DISABLED; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -44,10 +45,6 @@ import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; import org.apache.hadoop.hbase.keymeta.KeymetaServiceEndpoint.KeymetaAdminServiceImpl; import org.apache.hadoop.hbase.master.MasterServices; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.GetManagedKeysResponse; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeyRequest; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeyResponse; import org.apache.hadoop.hbase.testclassification.MasterTests; import org.apache.hadoop.hbase.testclassification.SmallTests; import org.apache.hadoop.hbase.util.Bytes; @@ -56,12 +53,20 @@ import org.junit.Test; import org.junit.experimental.categories.Category; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.apache.hbase.thirdparty.com.google.protobuf.ByteString; import org.apache.hbase.thirdparty.com.google.protobuf.RpcCallback; import org.apache.hbase.thirdparty.com.google.protobuf.RpcController; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.GetManagedKeysResponse; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyRequest; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyResponse; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyState; + @Category({ MasterTests.class, SmallTests.class }) public class TestKeymetaEndpoint { @@ -82,6 +87,14 @@ public class TestKeymetaEndpoint { private RpcCallback enableKeyManagementDone; @Mock private RpcCallback getManagedKeysDone; + @Mock + private RpcCallback disableKeyManagementDone; + @Mock + private RpcCallback disableManagedKeyDone; + @Mock + private RpcCallback rotateManagedKeyDone; + @Mock + private RpcCallback refreshManagedKeysDone; KeymetaServiceEndpoint keymetaServiceEndpoint; private ManagedKeyResponse.Builder responseBuilder; @@ -103,8 +116,7 @@ public void setUp() throws Exception { keymetaServiceEndpoint.start(env); keyMetaAdminService = (KeymetaAdminServiceImpl) keymetaServiceEndpoint.getServices().iterator().next(); - responseBuilder = - ManagedKeyResponse.newBuilder().setKeyState(ManagedKeysProtos.ManagedKeyState.KEY_ACTIVE); + responseBuilder = ManagedKeyResponse.newBuilder().setKeyState(ManagedKeyState.KEY_ACTIVE); requestBuilder = ManagedKeyRequest.newBuilder().setKeyNamespace(ManagedKeyData.KEY_SPACE_GLOBAL); keyData1 = new ManagedKeyData(KEY_CUST.getBytes(), KEY_NAMESPACE, @@ -153,8 +165,7 @@ public void testGenerateKeyStateResponse() throws Exception { assertNotNull(response); assertNotNull(result.getStateList()); assertEquals(2, result.getStateList().size()); - assertEquals(ManagedKeysProtos.ManagedKeyState.KEY_ACTIVE, - result.getStateList().get(0).getKeyState()); + assertEquals(ManagedKeyState.KEY_ACTIVE, result.getStateList().get(0).getKeyState()); assertEquals(0, Bytes.compareTo(keyData1.getKeyCustodian(), result.getStateList().get(0).getKeyCust().toByteArray())); assertEquals(keyData1.getKeyNamespace(), result.getStateList().get(0).getKeyNamespace()); @@ -216,7 +227,8 @@ void call(RpcController controller, ManagedKeyRequest request, RpcCallback do @Test public void testGenerateKeyStateResponse_InvalidCust() throws Exception { // Arrange - ManagedKeyRequest request = requestBuilder.setKeyCust(ByteString.EMPTY).build(); + ManagedKeyRequest request = + requestBuilder.setKeyCust(ByteString.EMPTY).setKeyNamespace(KEY_NAMESPACE).build(); // Act keyMetaAdminService.enableKeyManagement(controller, request, enableKeyManagementDone); @@ -224,8 +236,8 @@ public void testGenerateKeyStateResponse_InvalidCust() throws Exception { // Assert verify(controller).setFailed(contains("key_cust must not be empty")); verify(keymetaAdmin, never()).enableKeyManagement(any(), any()); - verify(enableKeyManagementDone).run( - argThat(response -> response.getKeyState() == ManagedKeysProtos.ManagedKeyState.KEY_FAILED)); + verify(enableKeyManagementDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); } @Test @@ -241,8 +253,8 @@ public void testGenerateKeyStateResponse_IOException() throws Exception { // Assert verify(controller).setFailed(contains("IOException")); verify(keymetaAdmin).enableKeyManagement(any(), any()); - verify(enableKeyManagementDone).run( - argThat(response -> response.getKeyState() == ManagedKeysProtos.ManagedKeyState.KEY_FAILED)); + verify(enableKeyManagementDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); } @Test @@ -281,4 +293,269 @@ public void testGetManagedKeys_InvalidCust() throws Exception { verify(keymetaAdmin, never()).getManagedKeys(any(), any()); verify(getManagedKeysDone).run(argThat(response -> response.getStateList().isEmpty())); } + + @Test + public void testDisableKeyManagement_Success() throws Exception { + // Arrange + ManagedKeyRequest request = + requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())).build(); + ManagedKeyData disabledKey = new ManagedKeyData(KEY_CUST.getBytes(), KEY_NAMESPACE, DISABLED); + when(keymetaAdmin.disableKeyManagement(any(), any())).thenReturn(disabledKey); + // Act + keyMetaAdminService.disableKeyManagement(controller, request, disableKeyManagementDone); + + // Assert + verify(controller, never()).setFailed(anyString()); + verify(disableKeyManagementDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_INACTIVE)); + } + + @Test + public void testDisableKeyManagement_IOException() throws Exception { + doTestDisableKeyManagementError(IOException.class); + } + + @Test + public void testDisableKeyManagement_KeyException() throws Exception { + doTestDisableKeyManagementError(KeyException.class); + } + + private void doTestDisableKeyManagementError(Class exType) throws Exception { + // Arrange + when(keymetaAdmin.disableKeyManagement(any(), any())).thenThrow(exType); + ManagedKeyRequest request = + requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())).build(); + + // Act + keyMetaAdminService.disableKeyManagement(controller, request, disableKeyManagementDone); + + // Assert + verify(controller).setFailed(contains(exType.getSimpleName())); + verify(keymetaAdmin).disableKeyManagement(any(), any()); + verify(disableKeyManagementDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); + } + + @Test + public void testDisableKeyManagement_InvalidCust() throws Exception { + // Arrange + ManagedKeyRequest request = requestBuilder.setKeyCust(ByteString.EMPTY).build(); + + keyMetaAdminService.disableKeyManagement(controller, request, disableKeyManagementDone); + + verify(controller).setFailed(contains("key_cust must not be empty")); + verify(keymetaAdmin, never()).disableKeyManagement(any(), any()); + verify(disableKeyManagementDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); + } + + @Test + public void testDisableKeyManagement_InvalidNamespace() throws Exception { + // Arrange + ManagedKeyRequest request = requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())) + .setKeyNamespace("").build(); + + keyMetaAdminService.disableKeyManagement(controller, request, disableKeyManagementDone); + + verify(controller).setFailed(contains("key_namespace must not be empty")); + verify(keymetaAdmin, never()).disableKeyManagement(any(), any()); + verify(disableKeyManagementDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); + } + + @Test + public void testDisableManagedKey_Success() throws Exception { + // Arrange + ManagedKeyEntryRequest request = ManagedKeyEntryRequest.newBuilder() + .setKeyCustNs(requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())).build()) + .setKeyMetadataHash(ByteString.copyFrom(keyData1.getKeyMetadataHash())).build(); + when(keymetaAdmin.disableManagedKey(any(), any(), any())).thenReturn(keyData1); + + // Act + keyMetaAdminService.disableManagedKey(controller, request, disableManagedKeyDone); + + // Assert + verify(disableManagedKeyDone).run(any()); + verify(controller, never()).setFailed(anyString()); + } + + @Test + public void testDisableManagedKey_IOException() throws Exception { + doTestDisableManagedKeyError(IOException.class); + } + + @Test + public void testDisableManagedKey_KeyException() throws Exception { + doTestDisableManagedKeyError(KeyException.class); + } + + private void doTestDisableManagedKeyError(Class exType) throws Exception { + // Arrange + when(keymetaAdmin.disableManagedKey(any(), any(), any())).thenThrow(exType); + ManagedKeyEntryRequest request = ManagedKeyEntryRequest.newBuilder() + .setKeyCustNs(requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())).build()) + .setKeyMetadataHash(ByteString.copyFrom(keyData1.getKeyMetadataHash())).build(); + + // Act + keyMetaAdminService.disableManagedKey(controller, request, disableManagedKeyDone); + + // Assert + verify(controller).setFailed(contains(exType.getSimpleName())); + verify(keymetaAdmin).disableManagedKey(any(), any(), any()); + verify(disableManagedKeyDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); + } + + @Test + public void testDisableManagedKey_InvalidCust() throws Exception { + // Arrange + ManagedKeyEntryRequest request = ManagedKeyEntryRequest.newBuilder() + .setKeyCustNs( + requestBuilder.setKeyCust(ByteString.EMPTY).setKeyNamespace(KEY_NAMESPACE).build()) + .setKeyMetadataHash(ByteString.copyFrom(keyData1.getKeyMetadataHash())).build(); + + keyMetaAdminService.disableManagedKey(controller, request, disableManagedKeyDone); + + verify(controller).setFailed(contains("key_cust must not be empty")); + verify(keymetaAdmin, never()).disableManagedKey(any(), any(), any()); + verify(disableManagedKeyDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); + } + + @Test + public void testDisableManagedKey_InvalidNamespace() throws Exception { + // Arrange + ManagedKeyEntryRequest request = ManagedKeyEntryRequest.newBuilder() + .setKeyCustNs(requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())) + .setKeyNamespace("").build()) + .setKeyMetadataHash(ByteString.copyFrom(keyData1.getKeyMetadataHash())).build(); + + keyMetaAdminService.disableManagedKey(controller, request, disableManagedKeyDone); + + verify(controller).setFailed(contains("key_namespace must not be empty")); + verify(keymetaAdmin, never()).disableManagedKey(any(), any(), any()); + verify(disableManagedKeyDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); + } + + @Test + public void testRotateManagedKey_Success() throws Exception { + // Arrange + ManagedKeyRequest request = + requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())).build(); + when(keymetaAdmin.rotateManagedKey(any(), any())).thenReturn(keyData1); + + // Act + keyMetaAdminService.rotateManagedKey(controller, request, rotateManagedKeyDone); + + // Assert + verify(rotateManagedKeyDone).run(any()); + verify(controller, never()).setFailed(anyString()); + } + + @Test + public void testRotateManagedKey_IOException() throws Exception { + doTestRotateManagedKeyError(IOException.class); + } + + @Test + public void testRotateManagedKey_KeyException() throws Exception { + doTestRotateManagedKeyError(KeyException.class); + } + + private void doTestRotateManagedKeyError(Class exType) throws Exception { + // Arrange + when(keymetaAdmin.rotateManagedKey(any(), any())).thenThrow(exType); + ManagedKeyRequest request = + requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())).build(); + + // Act + keyMetaAdminService.rotateManagedKey(controller, request, rotateManagedKeyDone); + + // Assert + verify(controller).setFailed(contains(exType.getSimpleName())); + verify(keymetaAdmin).rotateManagedKey(any(), any()); + verify(rotateManagedKeyDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); + } + + @Test + public void testRotateManagedKey_InvalidCust() throws Exception { + // Arrange + ManagedKeyRequest request = + requestBuilder.setKeyCust(ByteString.EMPTY).setKeyNamespace(KEY_NAMESPACE).build(); + + keyMetaAdminService.rotateManagedKey(controller, request, rotateManagedKeyDone); + + verify(controller).setFailed(contains("key_cust must not be empty")); + verify(keymetaAdmin, never()).rotateManagedKey(any(), any()); + verify(rotateManagedKeyDone) + .run(argThat(response -> response.getKeyState() == ManagedKeyState.KEY_FAILED)); + } + + @Test + public void testRefreshManagedKeys_Success() throws Exception { + // Arrange + ManagedKeyRequest request = + requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())).build(); + + // Act + keyMetaAdminService.refreshManagedKeys(controller, request, refreshManagedKeysDone); + + // Assert + verify(refreshManagedKeysDone).run(any()); + verify(controller, never()).setFailed(anyString()); + } + + @Test + public void testRefreshManagedKeys_IOException() throws Exception { + doTestRefreshManagedKeysError(IOException.class); + } + + @Test + public void testRefreshManagedKeys_KeyException() throws Exception { + doTestRefreshManagedKeysError(KeyException.class); + } + + private void doTestRefreshManagedKeysError(Class exType) throws Exception { + // Arrange + Mockito.doThrow(exType).when(keymetaAdmin).refreshManagedKeys(any(), any()); + ManagedKeyRequest request = + requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())).build(); + + // Act + keyMetaAdminService.refreshManagedKeys(controller, request, refreshManagedKeysDone); + + // Assert + verify(controller).setFailed(contains(exType.getSimpleName())); + verify(keymetaAdmin).refreshManagedKeys(any(), any()); + verify(refreshManagedKeysDone).run(EmptyMsg.getDefaultInstance()); + } + + @Test + public void testRefreshManagedKeys_InvalidCust() throws Exception { + // Arrange + ManagedKeyRequest request = requestBuilder.setKeyCust(ByteString.EMPTY).build(); + + keyMetaAdminService.refreshManagedKeys(controller, request, refreshManagedKeysDone); + + verify(controller).setFailed(contains("key_cust must not be empty")); + verify(keymetaAdmin, never()).refreshManagedKeys(any(), any()); + verify(refreshManagedKeysDone).run(EmptyMsg.getDefaultInstance()); + } + + @Test + public void testRefreshManagedKeys_InvalidNamespace() throws Exception { + // Arrange + ManagedKeyRequest request = requestBuilder.setKeyCust(ByteString.copyFrom(KEY_CUST.getBytes())) + .setKeyNamespace("").build(); + + // Act + keyMetaAdminService.refreshManagedKeys(controller, request, refreshManagedKeysDone); + + // Assert + verify(controller).setFailed(contains("key_namespace must not be empty")); + verify(keymetaAdmin, never()).refreshManagedKeys(any(), any()); + verify(refreshManagedKeysDone).run(EmptyMsg.getDefaultInstance()); + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeymetaTableAccessor.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeymetaTableAccessor.java index 2afa235007c7..fde1d81481c1 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeymetaTableAccessor.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestKeymetaTableAccessor.java @@ -19,9 +19,11 @@ import static org.apache.hadoop.hbase.io.crypto.ManagedKeyData.KEY_SPACE_GLOBAL; import static org.apache.hadoop.hbase.io.crypto.ManagedKeyState.ACTIVE; +import static org.apache.hadoop.hbase.io.crypto.ManagedKeyState.ACTIVE_DISABLED; import static org.apache.hadoop.hbase.io.crypto.ManagedKeyState.DISABLED; import static org.apache.hadoop.hbase.io.crypto.ManagedKeyState.FAILED; import static org.apache.hadoop.hbase.io.crypto.ManagedKeyState.INACTIVE; +import static org.apache.hadoop.hbase.io.crypto.ManagedKeyState.INACTIVE_DISABLED; import static org.apache.hadoop.hbase.keymeta.KeymetaTableAccessor.DEK_CHECKSUM_QUAL_BYTES; import static org.apache.hadoop.hbase.keymeta.KeymetaTableAccessor.DEK_METADATA_QUAL_BYTES; import static org.apache.hadoop.hbase.keymeta.KeymetaTableAccessor.DEK_WRAPPED_BY_STK_QUAL_BYTES; @@ -45,21 +47,25 @@ import static org.mockito.Mockito.when; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NavigableMap; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.Cell; +import org.apache.hadoop.hbase.CellUtil; import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.client.Connection; +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.Mutation; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.ResultScanner; @@ -84,13 +90,16 @@ import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Suite; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @RunWith(Suite.class) @Suite.SuiteClasses({ TestKeymetaTableAccessor.TestAdd.class, - TestKeymetaTableAccessor.TestAddWithNullableFields.class, - TestKeymetaTableAccessor.TestGet.class, }) + TestKeymetaTableAccessor.TestAddWithNullableFields.class, TestKeymetaTableAccessor.TestGet.class, + TestKeymetaTableAccessor.TestDisableKey.class, + TestKeymetaTableAccessor.TestUpdateActiveState.class, }) @Category({ MasterTests.class, SmallTests.class }) public class TestKeymetaTableAccessor { protected static final String ALIAS = "custId1"; @@ -108,6 +117,8 @@ public class TestKeymetaTableAccessor { protected ResultScanner scanner; @Mock protected SystemKeyCache systemKeyCache; + @Mock + protected KeyManagementService keyManagementService; protected KeymetaTableAccessor accessor; protected Configuration conf = HBaseConfiguration.create(); @@ -128,7 +139,9 @@ public void setUp() throws Exception { when(connection.getTable(KeymetaTableAccessor.KEY_META_TABLE_NAME)).thenReturn(table); when(server.getSystemKeyCache()).thenReturn(systemKeyCache); when(server.getConfiguration()).thenReturn(conf); - when(server.getKeyManagementService()).thenReturn(server); + when(server.getKeyManagementService()).thenReturn(keyManagementService); + when(keyManagementService.getConfiguration()).thenReturn(conf); + when(keyManagementService.getSystemKeyCache()).thenReturn(systemKeyCache); accessor = new KeymetaTableAccessor(server); managedKeyProvider = new MockManagedKeyProvider(); @@ -158,6 +171,9 @@ public static Collection data() { return Arrays.asList(new Object[][] { { ACTIVE }, { FAILED }, { INACTIVE }, { DISABLED }, }); } + @Captor + private ArgumentCaptor> putCaptor; + @Test public void testAddKey() throws Exception { managedKeyProvider.setMockedKeyState(ALIAS, keyState); @@ -165,15 +181,14 @@ public void testAddKey() throws Exception { accessor.addKey(keyData); - ArgumentCaptor> putCaptor = ArgumentCaptor.forClass(ArrayList.class); verify(table).put(putCaptor.capture()); List puts = putCaptor.getValue(); assertEquals(keyState == ACTIVE ? 2 : 1, puts.size()); if (keyState == ACTIVE) { - assertPut(keyData, puts.get(0), constructRowKeyForCustNamespace(keyData)); - assertPut(keyData, puts.get(1), constructRowKeyForMetadata(keyData)); + assertPut(keyData, puts.get(0), constructRowKeyForCustNamespace(keyData), ACTIVE); + assertPut(keyData, puts.get(1), constructRowKeyForMetadata(keyData), ACTIVE); } else { - assertPut(keyData, puts.get(0), constructRowKeyForMetadata(keyData)); + assertPut(keyData, puts.get(0), constructRowKeyForMetadata(keyData), keyState); } } } @@ -185,22 +200,29 @@ public static class TestAddWithNullableFields extends TestKeymetaTableAccessor { public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(TestAddWithNullableFields.class); + @Captor + private ArgumentCaptor> batchCaptor; + @Test - public void testAddKeyWithFailedStateAndNullMetadata() throws Exception { + public void testAddKeyManagementStateMarker() throws Exception { managedKeyProvider.setMockedKeyState(ALIAS, FAILED); - ManagedKeyData keyData = new ManagedKeyData(CUST_ID, KEY_SPACE_GLOBAL, null, FAILED, null); + ManagedKeyData keyData = new ManagedKeyData(CUST_ID, KEY_SPACE_GLOBAL, FAILED); - accessor.addKey(keyData); + accessor.addKeyManagementStateMarker(keyData.getKeyCustodian(), keyData.getKeyNamespace(), + keyData.getKeyState()); - ArgumentCaptor> putCaptor = ArgumentCaptor.forClass(ArrayList.class); - verify(table).put(putCaptor.capture()); - List puts = putCaptor.getValue(); - assertEquals(1, puts.size()); - Put put = puts.get(0); + verify(table).batch(batchCaptor.capture(), any()); + List mutations = batchCaptor.getValue(); + assertEquals(2, mutations.size()); + Mutation mutation1 = mutations.get(0); + Mutation mutation2 = mutations.get(1); + assertTrue(mutation1 instanceof Put); + assertTrue(mutation2 instanceof Delete); + Put put = (Put) mutation1; + Delete delete = (Delete) mutation2; // Verify the row key uses state value for metadata hash - byte[] expectedRowKey = - constructRowKeyForMetadata(CUST_ID, KEY_SPACE_GLOBAL, new byte[] { FAILED.getVal() }); + byte[] expectedRowKey = constructRowKeyForCustNamespace(CUST_ID, KEY_SPACE_GLOBAL); assertEquals(0, Bytes.compareTo(expectedRowKey, put.getRow())); Map valueMap = getValueMap(put); @@ -210,9 +232,21 @@ public void testAddKeyWithFailedStateAndNullMetadata() throws Exception { assertNull(valueMap.get(new Bytes(DEK_WRAPPED_BY_STK_QUAL_BYTES))); assertNull(valueMap.get(new Bytes(STK_CHECKSUM_QUAL_BYTES))); + assertEquals(Durability.SKIP_WAL, put.getDurability()); + assertEquals(HConstants.SYSTEMTABLE_QOS, put.getPriority()); + // Verify state is set correctly assertEquals(new Bytes(new byte[] { FAILED.getVal() }), valueMap.get(new Bytes(KEY_STATE_QUAL_BYTES))); + + // Verify the delete operation properties + assertEquals(Durability.SKIP_WAL, delete.getDurability()); + assertEquals(HConstants.SYSTEMTABLE_QOS, delete.getPriority()); + + // Verify the row key is correct for a failure marker + assertEquals(0, Bytes.compareTo(expectedRowKey, delete.getRow())); + // Verify the key checksum, wrapped key, and STK checksum columns are deleted + assertDeleteColumns(delete); } } @@ -233,6 +267,8 @@ public static class TestGet extends TestKeymetaTableAccessor { public void setUp() throws Exception { super.setUp(); + when(result1.isEmpty()).thenReturn(false); + when(result2.isEmpty()).thenReturn(false); when(result1.getValue(eq(KEY_META_INFO_FAMILY), eq(KEY_STATE_QUAL_BYTES))) .thenReturn(new byte[] { ACTIVE.getVal() }); when(result2.getValue(eq(KEY_META_INFO_FAMILY), eq(KEY_STATE_QUAL_BYTES))) @@ -265,12 +301,13 @@ public void testGetActiveKeyMissingWrappedKey() throws Exception { when(result.getValue(eq(KEY_META_INFO_FAMILY), eq(KEY_STATE_QUAL_BYTES))) .thenReturn(new byte[] { ACTIVE.getVal() }, new byte[] { INACTIVE.getVal() }); + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash(KEY_METADATA); IOException ex; ex = assertThrows(IOException.class, - () -> accessor.getKey(CUST_ID, KEY_SPACE_GLOBAL, KEY_METADATA)); + () -> accessor.getKey(CUST_ID, KEY_SPACE_GLOBAL, keyMetadataHash)); assertEquals("ACTIVE key must have a wrapped key", ex.getMessage()); ex = assertThrows(IOException.class, - () -> accessor.getKey(CUST_ID, KEY_SPACE_GLOBAL, KEY_METADATA)); + () -> accessor.getKey(CUST_ID, KEY_SPACE_GLOBAL, keyMetadataHash)); assertEquals("INACTIVE key must have a wrapped key", ex.getMessage()); } @@ -281,7 +318,8 @@ public void testGetKeyMissingSTK() throws Exception { when(systemKeyCache.getSystemKeyByChecksum(anyLong())).thenReturn(null); when(table.get(any(Get.class))).thenReturn(result1); - ManagedKeyData result = accessor.getKey(CUST_ID, KEY_NAMESPACE, KEY_METADATA); + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash(KEY_METADATA); + ManagedKeyData result = accessor.getKey(CUST_ID, KEY_NAMESPACE, keyMetadataHash); assertNull(result); } @@ -290,7 +328,8 @@ public void testGetKeyMissingSTK() throws Exception { public void testGetKeyWithWrappedKey() throws Exception { ManagedKeyData keyData = setupActiveKey(CUST_ID, result1); - ManagedKeyData result = accessor.getKey(CUST_ID, KEY_NAMESPACE, KEY_METADATA); + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash(keyData.getKeyMetadata()); + ManagedKeyData result = accessor.getKey(CUST_ID, KEY_NAMESPACE, keyMetadataHash); verify(table).get(any(Get.class)); assertNotNull(result); @@ -302,41 +341,16 @@ public void testGetKeyWithWrappedKey() throws Exception { assertEquals(ACTIVE, result.getKeyState()); // When DEK checksum doesn't match, we expect a null value. - result = accessor.getKey(CUST_ID, KEY_NAMESPACE, KEY_METADATA); + result = accessor.getKey(CUST_ID, KEY_NAMESPACE, keyMetadataHash); assertNull(result); } - @Test - public void testGetKeyWithFailedState() throws Exception { - // Test with FAILED state and null metadata - Result failedResult = mock(Result.class); - when(failedResult.getValue(eq(KEY_META_INFO_FAMILY), eq(KEY_STATE_QUAL_BYTES))) - .thenReturn(new byte[] { FAILED.getVal() }); - when(failedResult.getValue(eq(KEY_META_INFO_FAMILY), eq(REFRESHED_TIMESTAMP_QUAL_BYTES))) - .thenReturn(Bytes.toBytes(0L)); - when(failedResult.getValue(eq(KEY_META_INFO_FAMILY), eq(STK_CHECKSUM_QUAL_BYTES))) - .thenReturn(Bytes.toBytes(0L)); - // Explicitly return null for metadata to simulate FAILED state with null metadata - when(failedResult.getValue(eq(KEY_META_INFO_FAMILY), eq(DEK_METADATA_QUAL_BYTES))) - .thenReturn(null); - - when(table.get(any(Get.class))).thenReturn(failedResult); - ManagedKeyData result = accessor.getKey(CUST_ID, KEY_NAMESPACE, FAILED); - - verify(table).get(any(Get.class)); - assertNotNull(result); - assertEquals(0, Bytes.compareTo(CUST_ID, result.getKeyCustodian())); - assertEquals(KEY_NAMESPACE, result.getKeyNamespace()); - assertNull(result.getKeyMetadata()); - assertNull(result.getTheKey()); - assertEquals(FAILED, result.getKeyState()); - } - @Test public void testGetKeyWithoutWrappedKey() throws Exception { when(table.get(any(Get.class))).thenReturn(result2); - ManagedKeyData result = accessor.getKey(CUST_ID, KEY_NAMESPACE, KEY_METADATA); + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash(keyMetadata2); + ManagedKeyData result = accessor.getKey(CUST_ID, KEY_NAMESPACE, keyMetadataHash); verify(table).get(any(Get.class)); assertNotNull(result); @@ -354,7 +368,7 @@ public void testGetAllKeys() throws Exception { when(scanner.iterator()).thenReturn(List.of(result1, result2).iterator()); when(table.getScanner(any(Scan.class))).thenReturn(scanner); - List allKeys = accessor.getAllKeys(CUST_ID, KEY_NAMESPACE); + List allKeys = accessor.getAllKeys(CUST_ID, KEY_NAMESPACE, true); assertEquals(2, allKeys.size()); assertEquals(keyData.getKeyMetadata(), allKeys.get(0).getKeyMetadata()); @@ -369,7 +383,7 @@ public void testGetActiveKey() throws Exception { when(scanner.iterator()).thenReturn(List.of(result1).iterator()); when(table.get(any(Get.class))).thenReturn(result1); - ManagedKeyData activeKey = accessor.getActiveKey(CUST_ID, KEY_NAMESPACE); + ManagedKeyData activeKey = accessor.getKeyManagementStateMarker(CUST_ID, KEY_NAMESPACE); assertNotNull(activeKey); assertEquals(keyData, activeKey); @@ -392,7 +406,8 @@ private ManagedKeyData setupActiveKey(byte[] custId, Result result) throws Excep } } - protected void assertPut(ManagedKeyData keyData, Put put, byte[] rowKey) { + protected void assertPut(ManagedKeyData keyData, Put put, byte[] rowKey, + ManagedKeyState targetState) { assertEquals(Durability.SKIP_WAL, put.getDurability()); assertEquals(HConstants.SYSTEMTABLE_QOS, put.getPriority()); assertTrue(Bytes.compareTo(rowKey, put.getRow()) == 0); @@ -412,12 +427,29 @@ protected void assertPut(ManagedKeyData keyData, Put put, byte[] rowKey) { assertEquals(new Bytes(keyData.getKeyMetadata().getBytes()), valueMap.get(new Bytes(DEK_METADATA_QUAL_BYTES))); assertNotNull(valueMap.get(new Bytes(REFRESHED_TIMESTAMP_QUAL_BYTES))); - assertEquals(new Bytes(new byte[] { keyData.getKeyState().getVal() }), + assertEquals(new Bytes(new byte[] { targetState.getVal() }), valueMap.get(new Bytes(KEY_STATE_QUAL_BYTES))); } - private static Map getValueMap(Put put) { - NavigableMap> familyCellMap = put.getFamilyCellMap(); + // Verify the key checksum, wrapped key, and STK checksum columns are deleted + private static void assertDeleteColumns(Delete delete) { + Map> familyCellMap = delete.getFamilyCellMap(); + assertTrue(familyCellMap.containsKey(KEY_META_INFO_FAMILY)); + + List cells = familyCellMap.get(KEY_META_INFO_FAMILY); + assertEquals(3, cells.size()); + + // Verify each column is present in the delete + Set qualifiers = + cells.stream().map(CellUtil::cloneQualifier).collect(Collectors.toSet()); + + assertTrue(qualifiers.stream().anyMatch(q -> Bytes.equals(q, DEK_CHECKSUM_QUAL_BYTES))); + assertTrue(qualifiers.stream().anyMatch(q -> Bytes.equals(q, DEK_WRAPPED_BY_STK_QUAL_BYTES))); + assertTrue(qualifiers.stream().anyMatch(q -> Bytes.equals(q, STK_CHECKSUM_QUAL_BYTES))); + } + + private static Map getValueMap(Mutation mutation) { + NavigableMap> familyCellMap = mutation.getFamilyCellMap(); List cells = familyCellMap.get(KEY_META_INFO_FAMILY); Map valueMap = new HashMap<>(); for (Cell cell : cells) { @@ -427,4 +459,133 @@ private static Map getValueMap(Put put) { } return valueMap; } + + /** + * Tests for disableKey() method. + */ + @RunWith(Parameterized.class) + @Category({ MasterTests.class, SmallTests.class }) + public static class TestDisableKey extends TestKeymetaTableAccessor { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestDisableKey.class); + + // Parameterize the key state + @Parameter(0) + public ManagedKeyState keyState; + + @Captor + private ArgumentCaptor> mutationsCaptor; + + @Parameterized.Parameters(name = "{index},keyState={0}") + public static Collection data() { + return Arrays.asList(new Object[][] { { ACTIVE }, { INACTIVE }, { ACTIVE_DISABLED }, + { INACTIVE_DISABLED }, { FAILED }, }); + } + + @Test + public void testDisableKey() throws Exception { + ManagedKeyData keyData = + new ManagedKeyData(CUST_ID, KEY_NAMESPACE, null, keyState, "testMetadata"); + + accessor.disableKey(keyData); + + verify(table).batch(mutationsCaptor.capture(), any()); + List mutations = mutationsCaptor.getValue(); + assertEquals(keyState == ACTIVE ? 3 : keyState == INACTIVE ? 2 : 1, mutations.size()); + int putIndex = 0; + ManagedKeyState targetState = keyState == ACTIVE ? ACTIVE_DISABLED : INACTIVE_DISABLED; + if (keyState == ACTIVE) { + assertTrue( + Bytes.compareTo(constructRowKeyForCustNamespace(keyData), mutations.get(0).getRow()) + == 0); + ++putIndex; + } + assertPut(keyData, (Put) mutations.get(putIndex), constructRowKeyForMetadata(keyData), + targetState); + if (keyState == INACTIVE) { + assertTrue( + Bytes.compareTo(constructRowKeyForMetadata(keyData), mutations.get(putIndex + 1).getRow()) + == 0); + // Verify the key checksum, wrapped key, and STK checksum columns are deleted + assertDeleteColumns((Delete) mutations.get(putIndex + 1)); + } + } + } + + /** + * Tests for updateActiveState() method. + */ + @RunWith(BlockJUnit4ClassRunner.class) + @Category({ MasterTests.class, SmallTests.class }) + public static class TestUpdateActiveState extends TestKeymetaTableAccessor { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestUpdateActiveState.class); + + @Captor + private ArgumentCaptor> mutationsCaptor; + + @Test + public void testUpdateActiveStateFromInactiveToActive() throws Exception { + ManagedKeyData keyData = + new ManagedKeyData(CUST_ID, KEY_NAMESPACE, null, INACTIVE, "metadata", 123L); + ManagedKeyData systemKey = + new ManagedKeyData(new byte[] { 1 }, KEY_SPACE_GLOBAL, null, ACTIVE, "syskey", 100L); + when(systemKeyCache.getLatestSystemKey()).thenReturn(systemKey); + + accessor.updateActiveState(keyData, ACTIVE); + + verify(table).batch(mutationsCaptor.capture(), any()); + List mutations = mutationsCaptor.getValue(); + assertEquals(2, mutations.size()); + } + + @Test + public void testUpdateActiveStateFromActiveToInactive() throws Exception { + ManagedKeyData keyData = + new ManagedKeyData(CUST_ID, KEY_NAMESPACE, null, ACTIVE, "metadata", 123L); + + accessor.updateActiveState(keyData, INACTIVE); + + verify(table).batch(mutationsCaptor.capture(), any()); + List mutations = mutationsCaptor.getValue(); + assertEquals(2, mutations.size()); + } + + @Test + public void testUpdateActiveStateNoOp() throws Exception { + ManagedKeyData keyData = + new ManagedKeyData(CUST_ID, KEY_NAMESPACE, null, ACTIVE, "metadata", 123L); + + accessor.updateActiveState(keyData, ACTIVE); + + verify(table, Mockito.never()).batch(any(), any()); + } + + @Test + public void testUpdateActiveStateFromDisabledToActive() throws Exception { + ManagedKeyData keyData = + new ManagedKeyData(CUST_ID, KEY_NAMESPACE, null, DISABLED, "metadata", 123L); + ManagedKeyData systemKey = + new ManagedKeyData(new byte[] { 1 }, KEY_SPACE_GLOBAL, null, ACTIVE, "syskey", 100L); + when(systemKeyCache.getLatestSystemKey()).thenReturn(systemKey); + + accessor.updateActiveState(keyData, ACTIVE); + + verify(table).batch(mutationsCaptor.capture(), any()); + List mutations = mutationsCaptor.getValue(); + // Should have 2 mutations: add CustNamespace row and add all columns to Metadata row + assertEquals(2, mutations.size()); + } + + @Test + public void testUpdateActiveStateInvalidNewState() { + ManagedKeyData keyData = + new ManagedKeyData(CUST_ID, KEY_NAMESPACE, null, ACTIVE, "metadata", 123L); + + assertThrows(IllegalArgumentException.class, + () -> accessor.updateActiveState(keyData, DISABLED)); + } + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestManagedKeyDataCache.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestManagedKeyDataCache.java index 6e2eef1f67a7..0b00df9e57b6 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestManagedKeyDataCache.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestManagedKeyDataCache.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; @@ -211,7 +212,7 @@ public void setUp() { } @Test - public void testGenericCacheForNonExistentKey() throws Exception { + public void testGenericCacheForInvalidMetadata() throws Exception { assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, "test-metadata", null)); verify(testProvider).unwrapKey(any(String.class), any()); } @@ -221,19 +222,24 @@ public void testWithInvalidProvider() throws Exception { ManagedKeyData globalKey1 = testProvider.getManagedKey(CUST_ID, KEY_SPACE_GLOBAL); doThrow(new IOException("Test exception")).when(testProvider).unwrapKey(any(String.class), any()); + // With no L2 and invalid provider, there will be no entry. assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, globalKey1.getKeyMetadata(), null)); verify(testProvider).unwrapKey(any(String.class), any()); - // A second call to getEntry should not result in a call to the provider due to -ve entry. clearInvocations(testProvider); - verify(testProvider, never()).unwrapKey(any(String.class), any()); + + // A second call to getEntry should not result in a call to the provider due to -ve entry. assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, globalKey1.getKeyMetadata(), null)); + verify(testProvider, never()).unwrapKey(any(String.class), any()); + + // doThrow(new IOException("Test exception")).when(testProvider).getManagedKey(any(), any(String.class)); assertNull(cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); verify(testProvider).getManagedKey(any(), any(String.class)); - // A second call to getRandomEntry should not result in a call to the provider due to -ve - // entry. clearInvocations(testProvider); + + // A second call to getActiveEntry should not result in a call to the provider due to -ve + // entry. assertNull(cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); verify(testProvider, never()).getManagedKey(any(), any(String.class)); } @@ -294,7 +300,7 @@ public void testActiveKeysCacheOperations() throws Exception { assertNotNull(cache.getActiveEntry(CUST_ID, "namespace1")); assertEquals(2, cache.getActiveCacheEntryCount()); - cache.invalidateAll(); + cache.clearCache(); assertEquals(0, cache.getActiveCacheEntryCount()); assertNotNull(cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); assertEquals(1, cache.getActiveCacheEntryCount()); @@ -322,7 +328,7 @@ public void testThatActiveKeysCache_SkipsProvider_WhenLoadedViaGenericCache() th // ACTIVE keys are automatically added to activeKeysCache when loaded // via getEntry, so getActiveEntry will find them there and won't call the provider verify(testProvider, never()).getManagedKey(any(), any(String.class)); - cache.invalidateAll(); + cache.clearCache(); assertEquals(0, cache.getActiveCacheEntryCount()); } @@ -351,10 +357,10 @@ public void testActiveKeysCacheWithMultipleCustodiansInGenericCache() throws Exc String alias2 = "cust2"; byte[] cust_id2 = alias2.getBytes(); ManagedKeyData key2 = testProvider.getManagedKey(cust_id2, KEY_SPACE_GLOBAL); - assertNotNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, key2.getKeyMetadata(), null)); + assertNotNull(cache.getEntry(cust_id2, KEY_SPACE_GLOBAL, key2.getKeyMetadata(), null)); assertNotNull(cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); // ACTIVE keys are automatically added to activeKeysCache when loaded. - assertEquals(1, cache.getActiveCacheEntryCount()); + assertEquals(2, cache.getActiveCacheEntryCount()); } @Test @@ -371,6 +377,278 @@ public void testActiveKeysCacheWithMultipleNamespaces() throws Exception { verify(testProvider, times(3)).getManagedKey(any(), any(String.class)); assertEquals(3, cache.getActiveCacheEntryCount()); } + + @Test + public void testEjectKey_ActiveKeysCacheOnly() throws Exception { + // Load a key into the active keys cache + ManagedKeyData key = cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL); + assertNotNull(key); + assertEquals(1, cache.getActiveCacheEntryCount()); + + // Eject the key - should remove from active keys cache + boolean ejected = cache.ejectKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertTrue("Key should be ejected when metadata matches", ejected); + assertEquals(0, cache.getActiveCacheEntryCount()); + + // Try to eject again - should return false since it's already gone from active keys cache + boolean ejectedAgain = cache.ejectKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertFalse("Should return false when key is already ejected", ejectedAgain); + assertEquals(0, cache.getActiveCacheEntryCount()); + } + + @Test + public void testEjectKey_GenericCacheOnly() throws Exception { + // Load a key into the generic cache + ManagedKeyData key = cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, + testProvider.getManagedKey(CUST_ID, KEY_SPACE_GLOBAL).getKeyMetadata(), null); + assertNotNull(key); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Eject the key - should remove from generic cache + boolean ejected = cache.ejectKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertTrue("Key should be ejected when metadata matches", ejected); + assertEquals(0, cache.getGenericCacheEntryCount()); + + // Try to eject again - should return false since it's already gone from generic cache + boolean ejectedAgain = cache.ejectKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertFalse("Should return false when key is already ejected", ejectedAgain); + assertEquals(0, cache.getGenericCacheEntryCount()); + } + + @Test + public void testEjectKey_Success() throws Exception { + // Load a key into the active keys cache + ManagedKeyData key = cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL); + assertNotNull(key); + String metadata = key.getKeyMetadata(); + assertEquals(1, cache.getActiveCacheEntryCount()); + + // Also load into the generic cache + ManagedKeyData keyFromGeneric = cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, metadata, null); + assertNotNull(keyFromGeneric); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Eject the key with matching metadata - should remove from both caches + boolean ejected = cache.ejectKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertTrue("Key should be ejected when metadata matches", ejected); + assertEquals(0, cache.getActiveCacheEntryCount()); + assertEquals(0, cache.getGenericCacheEntryCount()); + + // Try to eject again - should return false since it's already gone from active keys cache + boolean ejectedAgain = cache.ejectKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertFalse("Should return false when key is already ejected", ejectedAgain); + assertEquals(0, cache.getActiveCacheEntryCount()); + assertEquals(0, cache.getGenericCacheEntryCount()); + } + + @Test + public void testEjectKey_MetadataMismatch() throws Exception { + // Load a key into both caches + ManagedKeyData key = cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL); + assertNotNull(key); + assertEquals(1, cache.getActiveCacheEntryCount()); + + // Also load into the generic cache + cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Try to eject with wrong metadata - should not eject from either cache + String wrongMetadata = "wrong-metadata"; + boolean ejected = cache.ejectKey(CUST_ID, KEY_SPACE_GLOBAL, + ManagedKeyData.constructMetadataHash(wrongMetadata)); + assertFalse("Key should not be ejected when metadata doesn't match", ejected); + assertEquals(1, cache.getActiveCacheEntryCount()); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Verify the key is still in both caches + assertEquals(key, cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); + assertEquals(key.getKeyMetadata(), + cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()).getKeyMetadata()); + } + + @Test + public void testEjectKey_KeyNotPresent() throws Exception { + // Try to eject a key that doesn't exist in the cache + String nonExistentMetadata = "non-existent-metadata"; + boolean ejected = cache.ejectKey(CUST_ID, "non-existent-namespace", + ManagedKeyData.constructMetadataHash(nonExistentMetadata)); + assertFalse("Should return false when key is not present", ejected); + assertEquals(0, cache.getActiveCacheEntryCount()); + } + + @Test + public void testEjectKey_MultipleKeys() throws Exception { + // Load multiple keys into both caches + ManagedKeyData key1 = cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL); + ManagedKeyData key2 = cache.getActiveEntry(CUST_ID, "namespace1"); + ManagedKeyData key3 = cache.getActiveEntry(CUST_ID, "namespace2"); + assertNotNull(key1); + assertNotNull(key2); + assertNotNull(key3); + assertEquals(3, cache.getActiveCacheEntryCount()); + + // Also load all keys into the generic cache + cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, key1.getKeyMetadata(), null); + cache.getEntry(CUST_ID, "namespace1", key2.getKeyMetadata(), null); + cache.getEntry(CUST_ID, "namespace2", key3.getKeyMetadata(), null); + assertEquals(3, cache.getGenericCacheEntryCount()); + + // Eject only the middle key from both caches + boolean ejected = cache.ejectKey(CUST_ID, "namespace1", key2.getKeyMetadataHash()); + assertTrue("Key should be ejected from both caches", ejected); + assertEquals(2, cache.getActiveCacheEntryCount()); + assertEquals(2, cache.getGenericCacheEntryCount()); + + // Verify only key2 was ejected - key1 and key3 should still be there + clearInvocations(testProvider); + assertEquals(key1, cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); + assertEquals(key3, cache.getActiveEntry(CUST_ID, "namespace2")); + // These getActiveEntry() calls should not trigger provider calls since keys are still cached + verify(testProvider, never()).getManagedKey(any(), any(String.class)); + + // Verify generic cache still has key1 and key3 + assertEquals(key1.getKeyMetadata(), + cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, key1.getKeyMetadata(), null).getKeyMetadata()); + assertEquals(key3.getKeyMetadata(), + cache.getEntry(CUST_ID, "namespace2", key3.getKeyMetadata(), null).getKeyMetadata()); + + // Try to eject key2 again - should return false since it's already gone from both caches + boolean ejectedAgain = cache.ejectKey(CUST_ID, "namespace1", key2.getKeyMetadataHash()); + assertFalse("Should return false when key is already ejected", ejectedAgain); + assertEquals(2, cache.getActiveCacheEntryCount()); + assertEquals(2, cache.getGenericCacheEntryCount()); + } + + @Test + public void testEjectKey_DifferentCustodian() throws Exception { + // Load a key for one custodian into both caches + ManagedKeyData key = cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL); + assertNotNull(key); + String metadata = key.getKeyMetadata(); + assertEquals(1, cache.getActiveCacheEntryCount()); + + // Also load into the generic cache + cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Try to eject with a different custodian - should not eject from either cache + byte[] differentCustodian = "different-cust".getBytes(); + boolean ejected = + cache.ejectKey(differentCustodian, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertFalse("Should not eject key for different custodian", ejected); + assertEquals(1, cache.getActiveCacheEntryCount()); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Verify the original key is still in both caches + assertEquals(key, cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); + assertEquals(metadata, + cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, metadata, null).getKeyMetadata()); + } + + @Test + public void testEjectKey_AfterClearCache() throws Exception { + // Load a key into both caches + ManagedKeyData key = cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL); + assertNotNull(key); + String metadata = key.getKeyMetadata(); + assertEquals(1, cache.getActiveCacheEntryCount()); + + // Also load into the generic cache + cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, metadata, null); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Clear both caches + cache.clearCache(); + assertEquals(0, cache.getActiveCacheEntryCount()); + assertEquals(0, cache.getGenericCacheEntryCount()); + + // Try to eject the key after both caches are cleared + boolean ejected = cache.ejectKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash()); + assertFalse("Should return false when both caches are empty", ejected); + assertEquals(0, cache.getActiveCacheEntryCount()); + assertEquals(0, cache.getGenericCacheEntryCount()); + } + + @Test + public void testGetEntry_HashCollisionOrMismatchDetection() throws Exception { + // Create a key and get it into the cache + ManagedKeyData key1 = cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL); + assertNotNull(key1); + + // Now simulate a hash collision by trying to get an entry with the same hash + // but different custodian/namespace + byte[] differentCust = "different-cust".getBytes(); + String differentNamespace = "different-namespace"; + + // This should return null due to custodian/namespace mismatch (collision detection) + ManagedKeyData result = + cache.getEntry(differentCust, differentNamespace, key1.getKeyMetadata(), null); + + // Result should be null because of hash collision detection + // The cache finds an entry with the same metadata hash, but custodian/namespace don't match + assertNull("Should return null when hash collision is detected", result); + } + + @Test + public void testEjectKey_HashCollisionOrMismatchProtection() throws Exception { + // Create two keys with potential hash collision scenario + byte[] cust1 = "cust1".getBytes(); + byte[] cust2 = "cust2".getBytes(); + String namespace1 = "namespace1"; + + // Load a key for cust1 + ManagedKeyData key1 = cache.getActiveEntry(cust1, namespace1); + assertNotNull(key1); + assertEquals(1, cache.getActiveCacheEntryCount()); + + // Try to eject using same metadata hash but different custodian + // This should not eject the key due to custodian mismatch protection + boolean ejected = cache.ejectKey(cust2, namespace1, key1.getKeyMetadataHash()); + assertFalse("Should not eject key with different custodian even if hash matches", ejected); + assertEquals(1, cache.getActiveCacheEntryCount()); + + // Verify the original key is still there + assertEquals(key1, cache.getActiveEntry(cust1, namespace1)); + } + + @Test + public void testEjectKey_HashCollisionInBothCaches() throws Exception { + // This test covers the scenario where rejectedValue is set during the first cache check + // (activeKeysCache) and then the second cache check (cacheByMetadataHash) takes the + // early return path because rejectedValue is already set. + byte[] cust1 = "cust1".getBytes(); + byte[] cust2 = "cust2".getBytes(); + String namespace1 = "namespace1"; + + // Load a key for cust1 - this will put it in BOTH activeKeysCache and cacheByMetadataHash + ManagedKeyData key1 = cache.getActiveEntry(cust1, namespace1); + assertNotNull(key1); + + // Also access via generic cache to ensure it's in both caches + ManagedKeyData key1viaGeneric = + cache.getEntry(cust1, namespace1, key1.getKeyMetadata(), null); + assertNotNull(key1viaGeneric); + assertEquals(key1, key1viaGeneric); + + // Verify both cache counts + assertEquals(1, cache.getActiveCacheEntryCount()); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Try to eject using same metadata hash but different custodian + // This will trigger the collision detection in BOTH caches: + // 1. First check in activeKeysCache will detect mismatch and set rejectedValue + // 2. Second check in cacheByMetadataHash should take early return (line 234) + boolean ejected = cache.ejectKey(cust2, namespace1, key1.getKeyMetadataHash()); + assertFalse("Should not eject key with different custodian even if hash matches", ejected); + + // Verify both caches still have the entry + assertEquals(1, cache.getActiveCacheEntryCount()); + assertEquals(1, cache.getGenericCacheEntryCount()); + + // Verify the original key is still accessible + assertEquals(key1, cache.getActiveEntry(cust1, namespace1)); + assertEquals(key1, cache.getEntry(cust1, namespace1, key1.getKeyMetadata(), null)); + } } @RunWith(BlockJUnit4ClassRunner.class) @@ -391,71 +669,74 @@ public void setUp() { @Test public void testGenericCacheNonExistentKeyInL2Cache() throws Exception { assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, "test-metadata", null)); - verify(mockL2).getKey(any(), any(String.class), any(String.class)); + verify(mockL2).getKey(any(), any(String.class), any(byte[].class)); clearInvocations(mockL2); assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, "test-metadata", null)); - verify(mockL2, never()).getKey(any(), any(String.class), any(String.class)); + verify(mockL2, never()).getKey(any(), any(String.class), any(byte[].class)); } @Test public void testGenericCacheRetrievalFromL2Cache() throws Exception { ManagedKeyData key = testProvider.getManagedKey(CUST_ID, KEY_SPACE_GLOBAL); - when(mockL2.getKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadata())).thenReturn(key); + when(mockL2.getKey(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadataHash())).thenReturn(key); assertEquals(key, cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadata(), null)); - verify(mockL2).getKey(any(), any(String.class), any(String.class)); + verify(mockL2).getKey(any(), any(String.class), any(byte[].class)); } @Test public void testActiveKeysCacheNonExistentKeyInL2Cache() throws Exception { assertNull(cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); - verify(mockL2).getActiveKey(any(), any(String.class)); + verify(mockL2).getKeyManagementStateMarker(any(), any(String.class)); clearInvocations(mockL2); assertNull(cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); - verify(mockL2, never()).getActiveKey(any(), any(String.class)); + verify(mockL2, never()).getKeyManagementStateMarker(any(), any(String.class)); } @Test public void testActiveKeysCacheRetrievalFromL2Cache() throws Exception { ManagedKeyData key = testProvider.getManagedKey(CUST_ID, KEY_SPACE_GLOBAL); - when(mockL2.getActiveKey(CUST_ID, KEY_SPACE_GLOBAL)).thenReturn(key); + when(mockL2.getKeyManagementStateMarker(CUST_ID, KEY_SPACE_GLOBAL)).thenReturn(key); + assertEquals(key, cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); + verify(mockL2).getKeyManagementStateMarker(any(), any(String.class)); + clearInvocations(mockL2); assertEquals(key, cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); - verify(mockL2).getActiveKey(any(), any(String.class)); + verify(mockL2, never()).getKeyManagementStateMarker(any(), any(String.class)); } @Test public void testGenericCacheWithKeymetaAccessorException() throws Exception { - when(mockL2.getKey(CUST_ID, KEY_SPACE_GLOBAL, "test-metadata")) + when(mockL2.getKey(eq(CUST_ID), eq(KEY_SPACE_GLOBAL), any(byte[].class))) .thenThrow(new IOException("Test exception")); assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, "test-metadata", null)); - verify(mockL2).getKey(any(), any(String.class), any(String.class)); + verify(mockL2).getKey(any(), any(String.class), any(byte[].class)); clearInvocations(mockL2); assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, "test-metadata", null)); - verify(mockL2, never()).getKey(any(), any(String.class), any(String.class)); + verify(mockL2, never()).getKey(any(), any(String.class), any(byte[].class)); } @Test public void testGetActiveEntryWithKeymetaAccessorException() throws Exception { - when(mockL2.getActiveKey(CUST_ID, KEY_SPACE_GLOBAL)) + when(mockL2.getKeyManagementStateMarker(CUST_ID, KEY_SPACE_GLOBAL)) .thenThrow(new IOException("Test exception")); assertNull(cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); - verify(mockL2).getActiveKey(any(), any(String.class)); + verify(mockL2).getKeyManagementStateMarker(any(), any(String.class)); clearInvocations(mockL2); assertNull(cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); - verify(mockL2, never()).getActiveKey(any(), any(String.class)); + verify(mockL2, never()).getKeyManagementStateMarker(any(), any(String.class)); } @Test public void testActiveKeysCacheUsesKeymetaAccessorWhenGenericCacheEmpty() throws Exception { // Ensure generic cache is empty - cache.invalidateAll(); + cache.clearCache(); // Mock the keymetaAccessor to return a key ManagedKeyData key = testProvider.getManagedKey(CUST_ID, KEY_SPACE_GLOBAL); - when(mockL2.getActiveKey(CUST_ID, KEY_SPACE_GLOBAL)).thenReturn(key); + when(mockL2.getKeyManagementStateMarker(CUST_ID, KEY_SPACE_GLOBAL)).thenReturn(key); // Get the active entry - it should call keymetaAccessor since generic cache is empty assertEquals(key, cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); - verify(mockL2).getActiveKey(any(), any(String.class)); + verify(mockL2).getKeyManagementStateMarker(any(), any(String.class)); } } @@ -479,7 +760,7 @@ public void testGenericCacheRetrivalFromProviderWhenKeyNotFoundInL2Cache() throw ManagedKeyData key = testProvider.getManagedKey(CUST_ID, KEY_SPACE_GLOBAL); doReturn(key).when(testProvider).unwrapKey(any(String.class), any()); assertEquals(key, cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, key.getKeyMetadata(), null)); - verify(mockL2).getKey(any(), any(String.class), any(String.class)); + verify(mockL2).getKey(any(), any(String.class), any(byte[].class)); verify(mockL2).addKey(any(ManagedKeyData.class)); } @@ -492,16 +773,6 @@ public void testAddKeyFailure() throws Exception { verify(mockL2).addKey(any(ManagedKeyData.class)); } - @Test - public void testGenericCacheDynamicLookupUnexpectedException() throws Exception { - doThrow(new RuntimeException("Test exception")).when(testProvider) - .unwrapKey(any(String.class), any()); - assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, "test-metadata", null)); - assertNull(cache.getEntry(CUST_ID, KEY_SPACE_GLOBAL, "test-metadata", null)); - verify(mockL2).getKey(any(), any(String.class), any(String.class)); - verify(mockL2, never()).addKey(any(ManagedKeyData.class)); - } - @Test public void testActiveKeysCacheDynamicLookupWithUnexpectedException() throws Exception { doThrow(new RuntimeException("Test exception")).when(testProvider).getManagedKey(any(), @@ -519,7 +790,7 @@ public void testActiveKeysCacheRetrivalFromProviderWhenKeyNotFoundInL2Cache() th ManagedKeyData key = testProvider.getManagedKey(CUST_ID, KEY_SPACE_GLOBAL); doReturn(key).when(testProvider).getManagedKey(any(), any(String.class)); assertEquals(key, cache.getActiveEntry(CUST_ID, KEY_SPACE_GLOBAL)); - verify(mockL2).getActiveKey(any(), any(String.class)); + verify(mockL2).getKeyManagementStateMarker(any(), any(String.class)); } @Test diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestManagedKeymeta.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestManagedKeymeta.java index 75beb9f8370f..d04dee3853e9 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestManagedKeymeta.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/keymeta/TestManagedKeymeta.java @@ -31,24 +31,25 @@ import java.lang.reflect.Field; import java.security.KeyException; import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; +import org.apache.commons.lang3.NotImplementedException; import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.io.crypto.Encryption; import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; import org.apache.hadoop.hbase.io.crypto.ManagedKeyState; import org.apache.hadoop.hbase.io.crypto.MockManagedKeyProvider; import org.apache.hadoop.hbase.master.HMaster; -import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos; import org.apache.hadoop.hbase.regionserver.HRegionServer; import org.apache.hadoop.hbase.testclassification.MasterTests; import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hadoop.hbase.util.Bytes; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException; +import org.apache.hadoop.hbase.shaded.protobuf.generated.ManagedKeysProtos; + /** * Tests the admin API via both RPC and local calls. */ @@ -59,6 +60,23 @@ public class TestManagedKeymeta extends ManagedKeyTestBase { public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(TestManagedKeymeta.class); + /** + * Functional interface for setup operations that can throw ServiceException. + */ + @FunctionalInterface + interface SetupFunction { + void setup(ManagedKeysProtos.ManagedKeysService.BlockingInterface mockStub, + ServiceException networkError) throws ServiceException; + } + + /** + * Functional interface for test operations that can throw checked exceptions. + */ + @FunctionalInterface + interface TestFunction { + void test(KeymetaAdminClient client) throws IOException, KeyException; + } + @Test public void testEnableLocal() throws Exception { HMaster master = TEST_UTIL.getHBaseCluster().getMaster(); @@ -76,16 +94,32 @@ private void doTestEnable(KeymetaAdmin adminClient) throws IOException, KeyExcep HMaster master = TEST_UTIL.getHBaseCluster().getMaster(); MockManagedKeyProvider managedKeyProvider = (MockManagedKeyProvider) Encryption.getManagedKeyProvider(master.getConfiguration()); + managedKeyProvider.setMultikeyGenMode(true); String cust = "cust1"; byte[] custBytes = cust.getBytes(); ManagedKeyData managedKey = adminClient.enableKeyManagement(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); assertKeyDataSingleKey(managedKey, ManagedKeyState.ACTIVE); + // Enable must have persisted the key, but it won't be read back until we call into the cache. + // We have the multi key gen mode enabled, but since the key should be loaded from L2, we + // should get the same key even after ejecting it. + HRegionServer regionServer = TEST_UTIL.getHBaseCluster().getRegionServer(0); + ManagedKeyDataCache managedKeyDataCache = regionServer.getManagedKeyDataCache(); + ManagedKeyData activeEntry = + managedKeyDataCache.getActiveEntry(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + assertNotNull(activeEntry); + assertTrue(Bytes.equals(managedKey.getKeyMetadataHash(), activeEntry.getKeyMetadataHash())); + assertTrue(managedKeyDataCache.ejectKey(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL, + managedKey.getKeyMetadataHash())); + activeEntry = managedKeyDataCache.getActiveEntry(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + assertNotNull(activeEntry); + assertTrue(Bytes.equals(managedKey.getKeyMetadataHash(), activeEntry.getKeyMetadataHash())); + List managedKeys = adminClient.getManagedKeys(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); assertEquals(managedKeyProvider.getLastGeneratedKeyData(cust, ManagedKeyData.KEY_SPACE_GLOBAL) - .cloneWithoutKey(), managedKeys.get(0).cloneWithoutKey()); + .createClientFacingInstance(), managedKeys.get(0).createClientFacingInstance()); String nonExistentCust = "nonExistentCust"; byte[] nonExistentBytes = nonExistentCust.getBytes(); @@ -121,43 +155,17 @@ public void testEnableKeyManagementWithExceptionOnGetManagedKey() throws Excepti @Test public void testEnableKeyManagementWithClientSideServiceException() throws Exception { - doTestWithClientSideServiceException((mockStub, networkError) -> { - try { - when(mockStub.enableKeyManagement(any(), any())).thenThrow(networkError); - } catch (ServiceException e) { - // We are just setting up the mock, so no exception is expected here. - throw new RuntimeException("Unexpected ServiceException", e); - } - return null; - }, (client) -> { - try { - client.enableKeyManagement(new byte[0], "namespace"); - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - }); + doTestWithClientSideServiceException((mockStub, + networkError) -> when(mockStub.enableKeyManagement(any(), any())).thenThrow(networkError), + (client) -> client.enableKeyManagement(new byte[0], "namespace")); } @Test public void testGetManagedKeysWithClientSideServiceException() throws Exception { // Similar test for getManagedKeys method - doTestWithClientSideServiceException((mockStub, networkError) -> { - try { - when(mockStub.getManagedKeys(any(), any())).thenThrow(networkError); - } catch (ServiceException e) { - // We are just setting up the mock, so no exception is expected here. - throw new RuntimeException("Unexpected ServiceException", e); - } - return null; - }, (client) -> { - try { - client.getManagedKeys(new byte[0], "namespace"); - } catch (IOException | KeyException e) { - throw new RuntimeException(e); - } - return null; - }); + doTestWithClientSideServiceException((mockStub, + networkError) -> when(mockStub.getManagedKeys(any(), any())).thenThrow(networkError), + (client) -> client.getManagedKeys(new byte[0], "namespace")); } @Test @@ -212,28 +220,13 @@ public void testRotateSTKWithExceptionOnGetSystemKey() throws Exception { @Test public void testRotateSTKWithClientSideServiceException() throws Exception { - doTestWithClientSideServiceException((mockStub, networkError) -> { - try { - when(mockStub.rotateSTK(any(), any())).thenThrow(networkError); - } catch (ServiceException e) { - // We are just setting up the mock, so no exception is expected here. - throw new RuntimeException("Unexpected ServiceException", e); - } - return null; - }, (client) -> { - try { - client.rotateSTK(); - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - }); - } - - private void - doTestWithClientSideServiceException(BiFunction< - ManagedKeysProtos.ManagedKeysService.BlockingInterface, ServiceException, Void> setupFunction, - Function testFunction) throws Exception { + doTestWithClientSideServiceException( + (mockStub, networkError) -> when(mockStub.rotateSTK(any(), any())).thenThrow(networkError), + (client) -> client.rotateSTK()); + } + + private void doTestWithClientSideServiceException(SetupFunction setupFunction, + TestFunction testFunction) throws Exception { ManagedKeysProtos.ManagedKeysService.BlockingInterface mockStub = mock(ManagedKeysProtos.ManagedKeysService.BlockingInterface.class); @@ -246,16 +239,205 @@ public void testRotateSTKWithClientSideServiceException() throws Exception { stubField.setAccessible(true); stubField.set(client, mockStub); - setupFunction.apply(mockStub, networkError); + // Setup the mock + setupFunction.setup(mockStub, networkError); - IOException exception = assertThrows(IOException.class, () -> { - try { - testFunction.apply(client); - } catch (RuntimeException e) { - throw e.getCause(); - } - }); + // Execute test function and expect IOException + IOException exception = assertThrows(IOException.class, () -> testFunction.test(client)); assertTrue(exception.getMessage().contains("Network error")); } + + @Test + public void testDisableKeyManagementLocal() throws Exception { + HMaster master = TEST_UTIL.getHBaseCluster().getMaster(); + KeymetaAdmin keymetaAdmin = master.getKeymetaAdmin(); + doTestDisableKeyManagement(keymetaAdmin); + } + + @Test + public void testDisableKeyManagementOverRPC() throws Exception { + KeymetaAdmin adminClient = new KeymetaAdminClient(TEST_UTIL.getConnection()); + doTestDisableKeyManagement(adminClient); + } + + private void doTestDisableKeyManagement(KeymetaAdmin adminClient) + throws IOException, KeyException { + String cust = "cust2"; + byte[] custBytes = cust.getBytes(); + + // First enable key management + ManagedKeyData managedKey = + adminClient.enableKeyManagement(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + assertNotNull(managedKey); + assertKeyDataSingleKey(managedKey, ManagedKeyState.ACTIVE); + + // Now disable it + ManagedKeyData disabledKey = + adminClient.disableKeyManagement(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + assertNotNull(disabledKey); + assertEquals(ManagedKeyState.DISABLED, disabledKey.getKeyState().getExternalState()); + } + + @Test + public void testDisableKeyManagementWithClientSideServiceException() throws Exception { + doTestWithClientSideServiceException( + (mockStub, networkError) -> when(mockStub.disableKeyManagement(any(), any())) + .thenThrow(networkError), + (client) -> client.disableKeyManagement(new byte[0], "namespace")); + } + + @Test + public void testDisableManagedKeyLocal() throws Exception { + HMaster master = TEST_UTIL.getHBaseCluster().getMaster(); + KeymetaAdmin keymetaAdmin = master.getKeymetaAdmin(); + doTestDisableManagedKey(keymetaAdmin); + } + + @Test + public void testDisableManagedKeyOverRPC() throws Exception { + KeymetaAdmin adminClient = new KeymetaAdminClient(TEST_UTIL.getConnection()); + doTestDisableManagedKey(adminClient); + } + + private void doTestDisableManagedKey(KeymetaAdmin adminClient) throws IOException, KeyException { + String cust = "cust3"; + byte[] custBytes = cust.getBytes(); + + // First enable key management to create a key + ManagedKeyData managedKey = + adminClient.enableKeyManagement(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + assertNotNull(managedKey); + assertKeyDataSingleKey(managedKey, ManagedKeyState.ACTIVE); + byte[] keyMetadataHash = managedKey.getKeyMetadataHash(); + + // Now disable the specific key + ManagedKeyData disabledKey = + adminClient.disableManagedKey(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL, keyMetadataHash); + assertNotNull(disabledKey); + assertEquals(ManagedKeyState.DISABLED, disabledKey.getKeyState().getExternalState()); + } + + @Test + public void testDisableManagedKeyWithClientSideServiceException() throws Exception { + doTestWithClientSideServiceException( + (mockStub, networkError) -> when(mockStub.disableManagedKey(any(), any())) + .thenThrow(networkError), + (client) -> client.disableManagedKey(new byte[0], "namespace", new byte[0])); + } + + @Test + public void testRotateManagedKeyWithClientSideServiceException() throws Exception { + doTestWithClientSideServiceException((mockStub, + networkError) -> when(mockStub.rotateManagedKey(any(), any())).thenThrow(networkError), + (client) -> client.rotateManagedKey(new byte[0], "namespace")); + } + + @Test + public void testRefreshManagedKeysWithClientSideServiceException() throws Exception { + doTestWithClientSideServiceException((mockStub, + networkError) -> when(mockStub.refreshManagedKeys(any(), any())).thenThrow(networkError), + (client) -> client.refreshManagedKeys(new byte[0], "namespace")); + } + + @Test + public void testRotateManagedKeyLocal() throws Exception { + HMaster master = TEST_UTIL.getHBaseCluster().getMaster(); + KeymetaAdmin keymetaAdmin = master.getKeymetaAdmin(); + doTestRotateManagedKey(keymetaAdmin); + } + + @Test + public void testRotateManagedKeyOverRPC() throws Exception { + KeymetaAdmin adminClient = new KeymetaAdminClient(TEST_UTIL.getConnection()); + doTestRotateManagedKey(adminClient); + } + + private void doTestRotateManagedKey(KeymetaAdmin adminClient) throws IOException, KeyException { + // This test covers the success path (line 133 in KeymetaAdminClient for RPC) + HMaster master = TEST_UTIL.getHBaseCluster().getMaster(); + MockManagedKeyProvider managedKeyProvider = + (MockManagedKeyProvider) Encryption.getManagedKeyProvider(master.getConfiguration()); + managedKeyProvider.setMultikeyGenMode(true); + + String cust = "cust1"; + byte[] custBytes = cust.getBytes(); + + // Enable key management first to have a key to rotate + adminClient.enableKeyManagement(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + + // Now rotate the key + ManagedKeyData rotatedKey = + adminClient.rotateManagedKey(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + + assertNotNull("Rotated key should not be null", rotatedKey); + assertEquals("Rotated key should be ACTIVE", ManagedKeyState.ACTIVE, rotatedKey.getKeyState()); + assertEquals("Rotated key should have correct custodian", 0, + Bytes.compareTo(custBytes, rotatedKey.getKeyCustodian())); + assertEquals("Rotated key should have correct namespace", ManagedKeyData.KEY_SPACE_GLOBAL, + rotatedKey.getKeyNamespace()); + } + + @Test + public void testRefreshManagedKeysLocal() throws Exception { + HMaster master = TEST_UTIL.getHBaseCluster().getMaster(); + KeymetaAdmin keymetaAdmin = master.getKeymetaAdmin(); + doTestRefreshManagedKeys(keymetaAdmin); + } + + @Test + public void testRefreshManagedKeysOverRPC() throws Exception { + KeymetaAdmin adminClient = new KeymetaAdminClient(TEST_UTIL.getConnection()); + doTestRefreshManagedKeys(adminClient); + } + + private void doTestRefreshManagedKeys(KeymetaAdmin adminClient) throws IOException, KeyException { + // This test covers the success path (line 148 in KeymetaAdminClient for RPC) + String cust = "cust1"; + byte[] custBytes = cust.getBytes(); + + // Enable key management first to have keys to refresh + adminClient.enableKeyManagement(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + + // Should complete without exception - covers the normal return path + adminClient.refreshManagedKeys(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + + // Verify keys still exist after refresh + List keys = + adminClient.getManagedKeys(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL); + assertNotNull("Keys should exist after refresh", keys); + assertFalse("Should have at least one key after refresh", keys.isEmpty()); + } + + // ========== NotImplementedException Tests ========== + + @Test + public void testEjectManagedKeyDataCacheEntryNotSupported() throws Exception { + // This test covers lines 89-90 in KeymetaAdminClient + KeymetaAdminClient client = new KeymetaAdminClient(TEST_UTIL.getConnection()); + String cust = "cust1"; + byte[] custBytes = cust.getBytes(); + + NotImplementedException exception = assertThrows(NotImplementedException.class, () -> client + .ejectManagedKeyDataCacheEntry(custBytes, ManagedKeyData.KEY_SPACE_GLOBAL, "metadata")); + + assertTrue("Exception message should indicate method is not supported", + exception.getMessage().contains("ejectManagedKeyDataCacheEntry not supported")); + assertTrue("Exception message should mention KeymetaAdminClient", + exception.getMessage().contains("KeymetaAdminClient")); + } + + @Test + public void testClearManagedKeyDataCacheNotSupported() throws Exception { + // This test covers lines 95-96 in KeymetaAdminClient + KeymetaAdminClient client = new KeymetaAdminClient(TEST_UTIL.getConnection()); + + NotImplementedException exception = + assertThrows(NotImplementedException.class, () -> client.clearManagedKeyDataCache()); + + assertTrue("Exception message should indicate method is not supported", + exception.getMessage().contains("clearManagedKeyDataCache not supported")); + assertTrue("Exception message should mention KeymetaAdminClient", + exception.getMessage().contains("KeymetaAdminClient")); + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java index 402e8697fe91..b11ccefaadb5 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java @@ -144,7 +144,9 @@ import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.ScanRequest; import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos.ScanResponse; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BooleanMsg; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest; import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaSnapshotsRequest; import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaSnapshotsResponse; @@ -718,6 +720,18 @@ public EmptyMsg refreshSystemKeyCache(RpcController controller, EmptyMsg request return null; } + @Override + public BooleanMsg ejectManagedKeyDataCacheEntry(RpcController controller, + ManagedKeyEntryRequest request) throws ServiceException { + return null; + } + + @Override + public EmptyMsg clearManagedKeyDataCache(RpcController controller, EmptyMsg request) + throws ServiceException { + return null; + } + @Override public Connection createConnection(Configuration conf) throws IOException { return null; diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestKeymetaAdminImpl.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestKeymetaAdminImpl.java index 9500a54b31e8..9c3e5991c6e7 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestKeymetaAdminImpl.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestKeymetaAdminImpl.java @@ -30,22 +30,29 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.security.Key; import java.security.KeyException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.HBaseTestingUtil; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.Server; @@ -75,11 +82,14 @@ import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; import org.junit.runners.Suite; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @RunWith(Suite.class) @Suite.SuiteClasses({ TestKeymetaAdminImpl.TestWhenDisabled.class, TestKeymetaAdminImpl.TestAdminImpl.class, TestKeymetaAdminImpl.TestForKeyProviderNullReturn.class, - TestKeymetaAdminImpl.TestRotateSTK.class }) + TestKeymetaAdminImpl.TestMiscAPIs.class, + TestKeymetaAdminImpl.TestNewKeyManagementAdminMethods.class }) @Category({ MasterTests.class, SmallTests.class }) public class TestKeymetaAdminImpl { @@ -160,9 +170,9 @@ public static class TestAdminImpl extends TestKeymetaAdminImpl { @Parameters(name = "{index},keySpace={0},keyState={1}") public static Collection data() { - return Arrays.asList(new Object[][] { { KEY_SPACE_GLOBAL, ACTIVE, false }, - { "ns1", ACTIVE, false }, { KEY_SPACE_GLOBAL, FAILED, true }, - { KEY_SPACE_GLOBAL, INACTIVE, false }, { KEY_SPACE_GLOBAL, DISABLED, true }, }); + return Arrays + .asList(new Object[][] { { KEY_SPACE_GLOBAL, ACTIVE, false }, { "ns1", ACTIVE, false }, + { KEY_SPACE_GLOBAL, FAILED, true }, { KEY_SPACE_GLOBAL, DISABLED, true }, }); } @Test @@ -170,16 +180,16 @@ public void testEnableAndGet() throws Exception { MockManagedKeyProvider managedKeyProvider = (MockManagedKeyProvider) Encryption.getManagedKeyProvider(conf); managedKeyProvider.setMockedKeyState(CUST, keyState); - when(keymetaAccessor.getActiveKey(CUST.getBytes(), keySpace)) + when(keymetaAccessor.getKeyManagementStateMarker(CUST.getBytes(), keySpace)) .thenReturn(managedKeyProvider.getManagedKey(CUST.getBytes(), keySpace)); ManagedKeyData managedKey = keymetaAdmin.enableKeyManagement(CUST_BYTES, keySpace); assertNotNull(managedKey); assertEquals(keyState, managedKey.getKeyState()); - verify(keymetaAccessor).getActiveKey(CUST.getBytes(), keySpace); + verify(keymetaAccessor).getKeyManagementStateMarker(CUST.getBytes(), keySpace); keymetaAdmin.getManagedKeys(CUST_BYTES, keySpace); - verify(keymetaAccessor).getAllKeys(CUST.getBytes(), keySpace); + verify(keymetaAccessor).getAllKeys(CUST.getBytes(), keySpace, false); } @Test @@ -244,15 +254,15 @@ public void addKey(ManagedKeyData keyData) throws IOException { } @Override - public List getAllKeys(byte[] key_cust, String keyNamespace) - throws IOException, KeyException { - return keymetaAccessor.getAllKeys(key_cust, keyNamespace); + public List getAllKeys(byte[] key_cust, String keyNamespace, + boolean includeMarkers) throws IOException, KeyException { + return keymetaAccessor.getAllKeys(key_cust, keyNamespace, includeMarkers); } @Override - public ManagedKeyData getActiveKey(byte[] key_cust, String keyNamespace) + public ManagedKeyData getKeyManagementStateMarker(byte[] key_cust, String keyNamespace) throws IOException, KeyException { - return keymetaAccessor.getActiveKey(key_cust, keyNamespace); + return keymetaAccessor.getKeyManagementStateMarker(key_cust, keyNamespace); } } @@ -276,10 +286,10 @@ protected boolean assertKeyData(ManagedKeyData keyData, ManagedKeyState expKeySt */ @RunWith(BlockJUnit4ClassRunner.class) @Category({ MasterTests.class, SmallTests.class }) - public static class TestRotateSTK extends TestKeymetaAdminImpl { + public static class TestMiscAPIs extends TestKeymetaAdminImpl { @ClassRule public static final HBaseClassTestRule CLASS_RULE = - HBaseClassTestRule.forClass(TestRotateSTK.class); + HBaseClassTestRule.forClass(TestMiscAPIs.class); private ServerManager mockServerManager = mock(ServerManager.class); private AsyncClusterConnection mockConnection; @@ -295,6 +305,71 @@ public void setUp() throws Exception { when(mockConnection.getAdmin()).thenReturn(mockAsyncAdmin); } + @Test + public void testEnableWithInactiveKey() throws Exception { + MockManagedKeyProvider managedKeyProvider = + (MockManagedKeyProvider) Encryption.getManagedKeyProvider(conf); + managedKeyProvider.setMockedKeyState(CUST, INACTIVE); + when(keymetaAccessor.getKeyManagementStateMarker(CUST.getBytes(), KEY_SPACE_GLOBAL)) + .thenReturn(managedKeyProvider.getManagedKey(CUST.getBytes(), KEY_SPACE_GLOBAL)); + + IOException exception = assertThrows(IOException.class, + () -> keymetaAdmin.enableKeyManagement(CUST_BYTES, KEY_SPACE_GLOBAL)); + assertTrue(exception.getMessage(), + exception.getMessage().contains("Expected key to be ACTIVE, but got an INACTIVE key")); + } + + /** + * Helper method to test that a method throws IOException when not called on master. + * @param adminAction the action to test, taking a KeymetaAdminImpl instance + * @param expectedMessageFragment the expected fragment in the error message + */ + private void assertNotOnMasterThrowsException(Consumer adminAction, + String expectedMessageFragment) { + // Create a non-master server mock + Server mockRegionServer = mock(Server.class); + KeyManagementService mockKeyService = mock(KeyManagementService.class); + when(mockRegionServer.getKeyManagementService()).thenReturn(mockKeyService); + when(mockKeyService.getConfiguration()).thenReturn(conf); + when(mockRegionServer.getConfiguration()).thenReturn(conf); + when(mockRegionServer.getFileSystem()).thenReturn(mockFileSystem); + + KeymetaAdminImpl admin = new KeymetaAdminImpl(mockRegionServer) { + @Override + protected AsyncAdmin getAsyncAdmin(MasterServices master) { + throw new RuntimeException("Shouldn't be called since we are not on master"); + } + }; + + RuntimeException runtimeEx = + assertThrows(RuntimeException.class, () -> adminAction.accept(admin)); + assertTrue(runtimeEx.getCause() instanceof IOException); + IOException ex = (IOException) runtimeEx.getCause(); + assertTrue(ex.getMessage().contains(expectedMessageFragment)); + } + + /** + * Helper method to test that a method throws IOException when key management is disabled. + * @param adminAction the action to test, taking a KeymetaAdminImpl instance + */ + private void assertDisabledThrowsException(Consumer adminAction) { + TEST_UTIL.getConfiguration().set(HConstants.CRYPTO_MANAGED_KEYS_ENABLED_CONF_KEY, "false"); + + KeymetaAdminImpl admin = new KeymetaAdminImpl(mockServer) { + @Override + protected AsyncAdmin getAsyncAdmin(MasterServices master) { + throw new RuntimeException("Shouldn't be called since we are disabled"); + } + }; + + RuntimeException runtimeEx = + assertThrows(RuntimeException.class, () -> adminAction.accept(admin)); + assertTrue(runtimeEx.getCause() instanceof IOException); + IOException ex = (IOException) runtimeEx.getCause(); + assertTrue("Exception message should contain 'not enabled', but was: " + ex.getMessage(), + ex.getMessage().contains("not enabled")); + } + /** * Test rotateSTK when a new key is detected. Now that we can mock SystemKeyManager via * master.getSystemKeyManager(), we can properly test the success scenario: 1. @@ -388,40 +463,615 @@ public void testRotateSTKWithFailedServerRefresh() throws Exception { @Test public void testRotateSTKNotOnMaster() throws Exception { - // Create a non-master server mock - Server mockRegionServer = mock(Server.class); - KeyManagementService mockKeyService = mock(KeyManagementService.class); - // Mock KeyManagementService - required by KeyManagementBase constructor - when(mockRegionServer.getKeyManagementService()).thenReturn(mockKeyService); - when(mockKeyService.getConfiguration()).thenReturn(conf); - when(mockRegionServer.getConfiguration()).thenReturn(conf); - when(mockRegionServer.getFileSystem()).thenReturn(mockFileSystem); + assertNotOnMasterThrowsException(admin -> { + try { + admin.rotateSTK(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, "rotateSTK can only be called on master"); + } - KeymetaAdminImpl admin = new KeymetaAdminImpl(mockRegionServer) { - @Override - protected AsyncAdmin getAsyncAdmin(MasterServices master) { - throw new RuntimeException("Shouldn't be called since we are not on master"); + @Test + public void testEjectManagedKeyDataCacheEntryNotOnMaster() throws Exception { + byte[] keyCustodian = Bytes.toBytes("testCustodian"); + String keyNamespace = "testNamespace"; + String keyMetadata = "testMetadata"; + + assertNotOnMasterThrowsException(admin -> { + try { + admin.ejectManagedKeyDataCacheEntry(keyCustodian, keyNamespace, keyMetadata); + } catch (IOException e) { + throw new RuntimeException(e); } - }; + }, "ejectManagedKeyDataCacheEntry can only be called on master"); + } - IOException ex = assertThrows(IOException.class, () -> admin.rotateSTK()); - assertTrue(ex.getMessage().contains("rotateSTK can only be called on master")); + @Test + public void testClearManagedKeyDataCacheNotOnMaster() throws Exception { + assertNotOnMasterThrowsException(admin -> { + try { + admin.clearManagedKeyDataCache(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, "clearManagedKeyDataCache can only be called on master"); } @Test public void testRotateSTKWhenDisabled() throws Exception { - TEST_UTIL.getConfiguration().set(HConstants.CRYPTO_MANAGED_KEYS_ENABLED_CONF_KEY, "false"); + assertDisabledThrowsException(admin -> { + try { + admin.rotateSTK(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } - KeymetaAdminImpl admin = new KeymetaAdminImpl(mockServer) { - @Override - protected AsyncAdmin getAsyncAdmin(MasterServices master) { - throw new RuntimeException("Shouldn't be called since we are disabled"); + @Test + public void testEjectManagedKeyDataCacheEntryWhenDisabled() throws Exception { + byte[] keyCustodian = Bytes.toBytes("testCustodian"); + String keyNamespace = "testNamespace"; + String keyMetadata = "testMetadata"; + + assertDisabledThrowsException(admin -> { + try { + admin.ejectManagedKeyDataCacheEntry(keyCustodian, keyNamespace, keyMetadata); + } catch (IOException e) { + throw new RuntimeException(e); } - }; + }); + } - IOException ex = assertThrows(IOException.class, () -> admin.rotateSTK()); - assertTrue("Exception message should contain 'not enabled', but was: " + ex.getMessage(), - ex.getMessage().contains("not enabled")); + @Test + public void testClearManagedKeyDataCacheWhenDisabled() throws Exception { + assertDisabledThrowsException(admin -> { + try { + admin.clearManagedKeyDataCache(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + /** + * Test ejectManagedKeyDataCacheEntry API - verify it calls the AsyncAdmin method with correct + * parameters + */ + @Test + public void testEjectManagedKeyDataCacheEntry() throws Exception { + byte[] keyCustodian = Bytes.toBytes("testCustodian"); + String keyNamespace = "testNamespace"; + String keyMetadata = "testMetadata"; + + when(mockAsyncAdmin.ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockServer, keymetaAccessor); + + // Call the method + admin.ejectManagedKeyDataCacheEntry(keyCustodian, keyNamespace, keyMetadata); + + // Verify the AsyncAdmin method was called + verify(mockAsyncAdmin).ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), any()); + } + + /** + * Test ejectManagedKeyDataCacheEntry when it fails + */ + @Test + public void testEjectManagedKeyDataCacheEntryWithFailure() throws Exception { + byte[] keyCustodian = Bytes.toBytes("testCustodian"); + String keyNamespace = "testNamespace"; + String keyMetadata = "testMetadata"; + + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.completeExceptionally(new IOException("eject failed")); + when(mockAsyncAdmin.ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), any())) + .thenReturn(failedFuture); + + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockServer, keymetaAccessor); + + // Call the method and expect IOException + IOException ex = assertThrows(IOException.class, + () -> admin.ejectManagedKeyDataCacheEntry(keyCustodian, keyNamespace, keyMetadata)); + + assertTrue(ex.getMessage().contains("eject failed")); + verify(mockAsyncAdmin).ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), any()); + } + + /** + * Test clearManagedKeyDataCache API - verify it calls the AsyncAdmin method + */ + @Test + public void testClearManagedKeyDataCache() throws Exception { + when(mockAsyncAdmin.clearManagedKeyDataCacheOnServers(any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockServer, keymetaAccessor); + + // Call the method + admin.clearManagedKeyDataCache(); + + // Verify the AsyncAdmin method was called + verify(mockAsyncAdmin).clearManagedKeyDataCacheOnServers(any()); + } + + /** + * Test clearManagedKeyDataCache when it fails + */ + @Test + public void testClearManagedKeyDataCacheWithFailure() throws Exception { + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.completeExceptionally(new IOException("clear failed")); + when(mockAsyncAdmin.clearManagedKeyDataCacheOnServers(any())).thenReturn(failedFuture); + + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockServer, keymetaAccessor); + + // Call the method and expect IOException + IOException ex = assertThrows(IOException.class, () -> admin.clearManagedKeyDataCache()); + + assertTrue(ex.getMessage().contains("clear failed")); + verify(mockAsyncAdmin).clearManagedKeyDataCacheOnServers(any()); + } + } + + /** + * Tests for new key management admin methods. + */ + @RunWith(BlockJUnit4ClassRunner.class) + @Category({ MasterTests.class, SmallTests.class }) + public static class TestNewKeyManagementAdminMethods { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestNewKeyManagementAdminMethods.class); + + @Mock + private MasterServices mockMasterServices; + @Mock + private AsyncAdmin mockAsyncAdmin; + @Mock + private AsyncClusterConnection mockAsyncClusterConnection; + @Mock + private ServerManager mockServerManager; + @Mock + private KeymetaTableAccessor mockAccessor; + @Mock + private ManagedKeyProvider mockProvider; + @Mock + private KeyManagementService mockKeyManagementService; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + when(mockMasterServices.getAsyncClusterConnection()).thenReturn(mockAsyncClusterConnection); + when(mockAsyncClusterConnection.getAdmin()).thenReturn(mockAsyncAdmin); + when(mockMasterServices.getServerManager()).thenReturn(mockServerManager); + when(mockServerManager.getOnlineServersList()).thenReturn(new ArrayList<>()); + + // Setup KeyManagementService mock + Configuration conf = HBaseConfiguration.create(); + conf.setBoolean(HConstants.CRYPTO_MANAGED_KEYS_ENABLED_CONF_KEY, true); + when(mockKeyManagementService.getConfiguration()).thenReturn(conf); + when(mockMasterServices.getKeyManagementService()).thenReturn(mockKeyManagementService); + } + + @Test + public void testDisableKeyManagement() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + ManagedKeyData activeKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + ManagedKeyData disabledMarker = + new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, ManagedKeyState.DISABLED); + + when(mockAccessor.getKeyManagementStateMarker(any(), any())).thenReturn(activeKey) + .thenReturn(disabledMarker); + + ManagedKeyData result = admin.disableKeyManagement(CUST_BYTES, KEY_SPACE_GLOBAL); + + assertNotNull(result); + assertEquals(ManagedKeyState.DISABLED, result.getKeyState()); + verify(mockAccessor, times(2)).getKeyManagementStateMarker(CUST_BYTES, KEY_SPACE_GLOBAL); + verify(mockAccessor).updateActiveState(activeKey, ManagedKeyState.INACTIVE); + + // Repeat the call for idempotency check. + clearInvocations(mockAccessor); + when(mockAccessor.getKeyManagementStateMarker(any(), any())).thenReturn(disabledMarker); + result = admin.disableKeyManagement(CUST_BYTES, KEY_SPACE_GLOBAL); + assertNotNull(result); + assertEquals(ManagedKeyState.DISABLED, result.getKeyState()); + verify(mockAccessor, times(2)).getKeyManagementStateMarker(CUST_BYTES, KEY_SPACE_GLOBAL); + verify(mockAccessor, never()).updateActiveState(any(), any()); + } + + @Test + public void testDisableManagedKey() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + ManagedKeyData disabledKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.DISABLED, "metadata1", 123L); + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash("metadata1"); + when(mockAccessor.getKey(any(), any(), any())).thenReturn(disabledKey); + + CompletableFuture successFuture = CompletableFuture.completedFuture(null); + when(mockAsyncAdmin.ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), any())) + .thenReturn(successFuture); + + IOException exception = assertThrows(IOException.class, + () -> admin.disableManagedKey(CUST_BYTES, KEY_SPACE_GLOBAL, keyMetadataHash)); + assertTrue(exception.getMessage(), + exception.getMessage().contains("Key is already disabled")); + verify(mockAccessor, never()).disableKey(any(ManagedKeyData.class)); + } + + @Test + public void testDisableManagedKeyNotFound() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash("metadata1"); + // Return null to simulate key not found + when(mockAccessor.getKey(any(), any(), any())).thenReturn(null); + + IOException exception = assertThrows(IOException.class, + () -> admin.disableManagedKey(CUST_BYTES, KEY_SPACE_GLOBAL, keyMetadataHash)); + assertTrue(exception.getMessage(), + exception.getMessage() + .contains("Key not found for (custodian: Y3VzdDE=, namespace: *) with metadata hash: " + + ManagedKeyProvider.encodeToStr(keyMetadataHash))); + verify(mockAccessor).getKey(CUST_BYTES, KEY_SPACE_GLOBAL, keyMetadataHash); + } + + @Test + public void testRotateManagedKeyNoActiveKey() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + // Return null to simulate no active key exists + when(mockAccessor.getKeyManagementStateMarker(any(), any())).thenReturn(null); + + IOException exception = + assertThrows(IOException.class, () -> admin.rotateManagedKey(CUST_BYTES, KEY_SPACE_GLOBAL)); + assertTrue(exception.getMessage().contains("No active key found")); + verify(mockAccessor).getKeyManagementStateMarker(CUST_BYTES, KEY_SPACE_GLOBAL); + } + + @Test + public void testRotateManagedKey() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + ManagedKeyData currentKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + ManagedKeyData newKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata2", 124L); + + when(mockAccessor.getKeyManagementStateMarker(any(), any())).thenReturn(currentKey); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockProvider.getManagedKey(any(), any())).thenReturn(newKey); + + ManagedKeyData result = admin.rotateManagedKey(CUST_BYTES, KEY_SPACE_GLOBAL); + + assertNotNull(result); + assertEquals(newKey, result); + verify(mockAccessor).addKey(newKey); + verify(mockAccessor).updateActiveState(currentKey, ManagedKeyState.INACTIVE); + } + + @Test + public void testRefreshManagedKeysWithNoStateChange() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + List keys = new ArrayList<>(); + ManagedKeyData key1 = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + keys.add(key1); + + when(mockAccessor.getAllKeys(any(), any(), anyBoolean())).thenReturn(keys); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockProvider.unwrapKey(any(), any())).thenReturn(key1); + + admin.refreshManagedKeys(CUST_BYTES, KEY_SPACE_GLOBAL); + + verify(mockAccessor).getAllKeys(CUST_BYTES, KEY_SPACE_GLOBAL, false); + verify(mockAccessor, never()).updateActiveState(any(), any()); + verify(mockAccessor, never()).disableKey(any()); + } + + @Test + public void testRotateManagedKeyIgnoresFailedKey() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + ManagedKeyData currentKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + ManagedKeyData newKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.FAILED, "metadata1", 124L); + + when(mockAccessor.getKeyManagementStateMarker(any(), any())).thenReturn(currentKey); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockProvider.getManagedKey(any(), any())).thenReturn(newKey); + // Mock the AsyncAdmin for ejectManagedKeyDataCacheEntry + when(mockAsyncAdmin.ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + ManagedKeyData result = admin.rotateManagedKey(CUST_BYTES, KEY_SPACE_GLOBAL); + + assertNull(result); + // Verify that the active key was not marked as inactive + verify(mockAccessor, never()).addKey(any()); + verify(mockAccessor, never()).updateActiveState(any(), any()); + } + + @Test + public void testRotateManagedKeyNoRotation() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + // Current and new keys have the same metadata hash, so no rotation should occur + ManagedKeyData currentKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + + when(mockAccessor.getKeyManagementStateMarker(any(), any())).thenReturn(currentKey); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockProvider.getManagedKey(any(), any())).thenReturn(currentKey); + + ManagedKeyData result = admin.rotateManagedKey(CUST_BYTES, KEY_SPACE_GLOBAL); + + assertNull(result); + verify(mockAccessor, never()).updateActiveState(any(), any()); + verify(mockAccessor, never()).addKey(any()); + verify(mockAsyncAdmin, never()).ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), + any()); + } + + @Test + public void testRefreshManagedKeysWithStateChange() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + List keys = new ArrayList<>(); + ManagedKeyData key1 = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + keys.add(key1); + + // Refreshed key has a different state (INACTIVE) + ManagedKeyData refreshedKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.INACTIVE, "metadata1", 123L); + + when(mockAccessor.getAllKeys(any(), any(), anyBoolean())).thenReturn(keys); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockProvider.unwrapKey(any(), any())).thenReturn(refreshedKey); + + admin.refreshManagedKeys(CUST_BYTES, KEY_SPACE_GLOBAL); + + verify(mockAccessor).getAllKeys(CUST_BYTES, KEY_SPACE_GLOBAL, false); + verify(mockAccessor).updateActiveState(key1, ManagedKeyState.INACTIVE); + } + + @Test + public void testRefreshManagedKeysWithDisabledState() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + List keys = new ArrayList<>(); + ManagedKeyData key1 = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + keys.add(key1); + + // Refreshed key is DISABLED + ManagedKeyData disabledKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.DISABLED, "metadata1", 123L); + + when(mockAccessor.getAllKeys(any(), any(), anyBoolean())).thenReturn(keys); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockProvider.unwrapKey(any(), any())).thenReturn(disabledKey); + // Mock the ejectManagedKeyDataCacheEntry to cover line 263 + when(mockAsyncAdmin.ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(null)); + + admin.refreshManagedKeys(CUST_BYTES, KEY_SPACE_GLOBAL); + + verify(mockAccessor).getAllKeys(CUST_BYTES, KEY_SPACE_GLOBAL, false); + verify(mockAccessor).disableKey(key1); + // Verify cache ejection was called (line 263) + verify(mockAsyncAdmin).ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), any()); + } + + @Test + public void testRefreshManagedKeysWithException() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + List keys = new ArrayList<>(); + ManagedKeyData key1 = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + ManagedKeyData key2 = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata2", 124L); + ManagedKeyData refreshedKey1 = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.INACTIVE, "metadata1", 123L); + keys.add(key1); + keys.add(key2); + + when(mockAccessor.getAllKeys(any(), any(), anyBoolean())).thenReturn(keys); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + // First key throws IOException, second key should still be refreshed + doThrow(new IOException("Simulated error")).when(mockAccessor) + .updateActiveState(any(ManagedKeyData.class), any(ManagedKeyState.class)); + when(mockProvider.unwrapKey(key1.getKeyMetadata(), null)).thenReturn(refreshedKey1); + when(mockProvider.unwrapKey(key2.getKeyMetadata(), null)).thenReturn(key2); + + // Should not throw exception, should continue refreshing other keys + IOException exception = assertThrows(IOException.class, + () -> admin.refreshManagedKeys(CUST_BYTES, KEY_SPACE_GLOBAL)); + + assertTrue(exception.getCause() instanceof IOException); + assertTrue(exception.getCause().getMessage(), + exception.getCause().getMessage().contains("Simulated error")); + verify(mockAccessor).getAllKeys(CUST_BYTES, KEY_SPACE_GLOBAL, false); + verify(mockAccessor, never()).disableKey(any()); + verify(mockProvider).unwrapKey(key1.getKeyMetadata(), null); + verify(mockProvider).unwrapKey(key2.getKeyMetadata(), null); + verify(mockAsyncAdmin, never()).ejectManagedKeyDataCacheEntryOnServers(any(), any(), any(), + any()); + } + + @Test + public void testRefreshKeyWithMetadataValidationFailure() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + ManagedKeyData originalKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + // Refreshed key has different metadata (which should not happen and indicates a serious + // error) + ManagedKeyData refreshedKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata2", 124L); + + List keys = Arrays.asList(originalKey); + when(mockAccessor.getAllKeys(any(), any(), anyBoolean())).thenReturn(keys); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockProvider.unwrapKey(originalKey.getKeyMetadata(), null)).thenReturn(refreshedKey); + + // The metadata mismatch triggers a KeyException which gets wrapped in an IOException + IOException exception = assertThrows(IOException.class, + () -> admin.refreshManagedKeys(CUST_BYTES, KEY_SPACE_GLOBAL)); + assertTrue(exception.getCause() instanceof KeyException); + assertTrue(exception.getCause().getMessage(), + exception.getCause().getMessage().contains("Key metadata changed during refresh")); + verify(mockProvider).unwrapKey(originalKey.getKeyMetadata(), null); + // No state updates should happen due to the exception + verify(mockAccessor, never()).updateActiveState(any(), any()); + verify(mockAccessor, never()).disableKey(any()); + } + + @Test + public void testRefreshKeyWithFailedStateIgnored() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + ManagedKeyData originalKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 123L); + // Refreshed key is in FAILED state (provider issue) - using byte[] metadata hash constructor + ManagedKeyData failedKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.FAILED, "metadata1", 124L); + + List keys = Arrays.asList(originalKey); + when(mockAccessor.getAllKeys(any(), any(), anyBoolean())).thenReturn(keys); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockProvider.unwrapKey(originalKey.getKeyMetadata(), null)).thenReturn(failedKey); + + admin.refreshManagedKeys(CUST_BYTES, KEY_SPACE_GLOBAL); + + // Should not update state when refreshed key is FAILED + verify(mockAccessor, never()).updateActiveState(any(), any()); + verify(mockAccessor, never()).disableKey(any()); + verify(mockProvider).unwrapKey(originalKey.getKeyMetadata(), null); + } + + @Test + public void testRefreshKeyRecoveryFromPriorEnableFailure() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + // FAILED key with null metadata (lines 119-135 in KeyManagementUtils) + ManagedKeyData failedKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, FAILED, 123L); + + // Provider returns a recovered key + ManagedKeyData recoveredKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, null, + ManagedKeyState.ACTIVE, "metadata1", 124L); + + List keys = Arrays.asList(failedKey); + when(mockAccessor.getAllKeys(any(), any(), anyBoolean())).thenReturn(keys); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockAccessor.getKeyManagementStateMarker(CUST_BYTES, KEY_SPACE_GLOBAL)) + .thenReturn(failedKey); + when(mockProvider.getManagedKey(failedKey.getKeyCustodian(), failedKey.getKeyNamespace())) + .thenReturn(recoveredKey); + + admin.refreshManagedKeys(CUST_BYTES, KEY_SPACE_GLOBAL); + + // Should call getManagedKey for FAILED key with null metadata (line 125) + verify(mockProvider).getManagedKey(failedKey.getKeyCustodian(), failedKey.getKeyNamespace()); + // Should add recovered key (line 130) + verify(mockAccessor).addKey(recoveredKey); + } + + @Test + public void testRefreshKeyNoRecoveryFromPriorEnableFailure() throws Exception { + KeymetaAdminImplForTest admin = new KeymetaAdminImplForTest(mockMasterServices, mockAccessor); + + // FAILED key with null metadata + ManagedKeyData failedKey = new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, FAILED, 123L); + + // Provider returns another FAILED key (recovery didn't work) + ManagedKeyData stillFailedKey = + new ManagedKeyData(CUST_BYTES, KEY_SPACE_GLOBAL, ManagedKeyState.FAILED, 124L); + + List keys = Arrays.asList(failedKey); + when(mockAccessor.getAllKeys(any(), any(), anyBoolean())).thenReturn(keys); + when(mockAccessor.getKeyProvider()).thenReturn(mockProvider); + when(mockAccessor.getKeyManagementStateMarker(CUST_BYTES, KEY_SPACE_GLOBAL)) + .thenReturn(failedKey); + when(mockProvider.getManagedKey(failedKey.getKeyCustodian(), failedKey.getKeyNamespace())) + .thenReturn(stillFailedKey); + + admin.refreshManagedKeys(CUST_BYTES, KEY_SPACE_GLOBAL); + + // Should call getManagedKey for FAILED key with null metadata + verify(mockProvider).getManagedKey(failedKey.getKeyCustodian(), failedKey.getKeyNamespace()); + verify(mockAccessor, never()).addKey(any()); + } + + private class KeymetaAdminImplForTest extends KeymetaAdminImpl { + private final KeymetaTableAccessor accessor; + + public KeymetaAdminImplForTest(MasterServices server, KeymetaTableAccessor accessor) + throws IOException { + super(server); + this.accessor = accessor; + } + + @Override + protected AsyncAdmin getAsyncAdmin(MasterServices master) { + return mockAsyncAdmin; + } + + @Override + public List getAllKeys(byte[] keyCust, String keyNamespace, + boolean includeMarkers) throws IOException, KeyException { + return accessor.getAllKeys(keyCust, keyNamespace, includeMarkers); + } + + @Override + public ManagedKeyData getKey(byte[] keyCust, String keyNamespace, byte[] keyMetadataHash) + throws IOException, KeyException { + return accessor.getKey(keyCust, keyNamespace, keyMetadataHash); + } + + @Override + public void disableKey(ManagedKeyData keyData) throws IOException { + accessor.disableKey(keyData); + } + + @Override + public ManagedKeyData getKeyManagementStateMarker(byte[] keyCust, String keyNamespace) + throws IOException, KeyException { + return accessor.getKeyManagementStateMarker(keyCust, keyNamespace); + } + + @Override + public void addKeyManagementStateMarker(byte[] keyCust, String keyNamespace, + ManagedKeyState state) throws IOException { + accessor.addKeyManagementStateMarker(keyCust, keyNamespace, state); + } + + @Override + public ManagedKeyProvider getKeyProvider() { + return accessor.getKeyProvider(); + } + + @Override + public void addKey(ManagedKeyData keyData) throws IOException { + accessor.addKey(keyData); + } + + @Override + public void updateActiveState(ManagedKeyData keyData, ManagedKeyState newState) + throws IOException { + accessor.updateActiveState(keyData, newState); + } } } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestSystemKeyAccessorAndManager.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestSystemKeyAccessorAndManager.java index 0dc765ba7291..09e409b11e7d 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestSystemKeyAccessorAndManager.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestSystemKeyAccessorAndManager.java @@ -378,7 +378,7 @@ protected String loadKeyMetadata(Path keyPath) throws IOException { } @Override - protected ManagedKeyProvider getKeyProvider() { + public ManagedKeyProvider getKeyProvider() { return realProvider; } }; @@ -407,7 +407,7 @@ protected String loadKeyMetadata(Path keyPath) throws IOException { } @Override - protected ManagedKeyProvider getKeyProvider() { + public ManagedKeyProvider getKeyProvider() { return realProvider; } }; @@ -444,7 +444,7 @@ protected String loadKeyMetadata(Path keyPath) throws IOException { } @Override - protected ManagedKeyProvider getKeyProvider() { + public ManagedKeyProvider getKeyProvider() { return mock(ManagedKeyProvider.class); } }; @@ -465,7 +465,7 @@ protected String loadKeyMetadata(Path keyPath) throws IOException { } @Override - protected ManagedKeyProvider getKeyProvider() { + public ManagedKeyProvider getKeyProvider() { throw new RuntimeException("Key provider not available"); } }; @@ -516,7 +516,7 @@ public MockSystemKeyManager(MasterServices master, ManagedKeyProvider keyProvide } @Override - protected ManagedKeyProvider getKeyProvider() { + public ManagedKeyProvider getKeyProvider() { return keyProvider; } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestRSRpcServices.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestRSRpcServices.java index 3d367d820127..9efce81d9573 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestRSRpcServices.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestRSRpcServices.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -35,10 +36,14 @@ import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.client.RegionInfoBuilder; +import org.apache.hadoop.hbase.io.crypto.ManagedKeyData; import org.apache.hadoop.hbase.ipc.RpcCall; import org.apache.hadoop.hbase.ipc.RpcServer; +import org.apache.hadoop.hbase.keymeta.KeyManagementService; +import org.apache.hadoop.hbase.keymeta.ManagedKeyDataCache; import org.apache.hadoop.hbase.testclassification.MediumTests; import org.apache.hadoop.hbase.testclassification.RegionServerTests; +import org.apache.hadoop.hbase.util.Bytes; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -46,10 +51,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.hbase.thirdparty.com.google.protobuf.ByteString; import org.apache.hbase.thirdparty.com.google.protobuf.RpcController; import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BooleanMsg; import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.EmptyMsg; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyEntryRequest; +import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ManagedKeyRequest; /** * Test parts of {@link RSRpcServices} @@ -200,4 +209,179 @@ public void testRefreshSystemKeyCacheWhenRebuildFails() throws Exception { // Verify that rebuildSystemKeyCache was called verify(mockServer).rebuildSystemKeyCache(); } + + /** + * Test the ejectManagedKeyDataCacheEntry RPC method that is used to eject a specific managed key + * entry from the cache on region servers. + */ + @Test + public void testEjectManagedKeyDataCacheEntry() throws Exception { + // Create mocks + HRegionServer mockServer = mock(HRegionServer.class); + Configuration conf = HBaseConfiguration.create(); + FileSystem mockFs = mock(FileSystem.class); + KeyManagementService mockKeyService = mock(KeyManagementService.class); + ManagedKeyDataCache mockCache = mock(ManagedKeyDataCache.class); + + when(mockServer.getConfiguration()).thenReturn(conf); + when(mockServer.isOnline()).thenReturn(true); + when(mockServer.isAborted()).thenReturn(false); + when(mockServer.isStopped()).thenReturn(false); + when(mockServer.isDataFileSystemOk()).thenReturn(true); + when(mockServer.getFileSystem()).thenReturn(mockFs); + when(mockServer.getKeyManagementService()).thenReturn(mockKeyService); + when(mockKeyService.getManagedKeyDataCache()).thenReturn(mockCache); + // Mock the ejectKey to return true + when(mockCache.ejectKey(any(), any(), any())).thenReturn(true); + + // Create RSRpcServices + RSRpcServices rpcServices = new RSRpcServices(mockServer); + + // Create request + byte[] keyCustodian = Bytes.toBytes("testCustodian"); + String keyNamespace = "testNamespace"; + String keyMetadata = "testMetadata"; + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash(keyMetadata); + + ManagedKeyEntryRequest request = ManagedKeyEntryRequest.newBuilder() + .setKeyCustNs(ManagedKeyRequest.newBuilder().setKeyCust(ByteString.copyFrom(keyCustodian)) + .setKeyNamespace(keyNamespace).build()) + .setKeyMetadataHash(ByteString.copyFrom(keyMetadataHash)).build(); + + RpcController controller = mock(RpcController.class); + + // Call the RPC method + BooleanMsg response = rpcServices.ejectManagedKeyDataCacheEntry(controller, request); + + // Verify the response is not null and contains the expected boolean value + assertNotNull("Response should not be null", response); + assertTrue("Response should indicate key was ejected", response.getBoolMsg()); + + // Verify that ejectKey was called on the cache + verify(mockCache).ejectKey(keyCustodian, keyNamespace, keyMetadataHash); + + LOG.info("ejectManagedKeyDataCacheEntry test completed successfully"); + } + + /** + * Test that ejectManagedKeyDataCacheEntry throws ServiceException when server is stopped + */ + @Test + public void testEjectManagedKeyDataCacheEntryWhenServerStopped() throws Exception { + // Create mocks + HRegionServer mockServer = mock(HRegionServer.class); + Configuration conf = HBaseConfiguration.create(); + FileSystem mockFs = mock(FileSystem.class); + + when(mockServer.getConfiguration()).thenReturn(conf); + when(mockServer.isOnline()).thenReturn(true); + when(mockServer.isAborted()).thenReturn(false); + when(mockServer.isStopped()).thenReturn(true); // Server is stopped + when(mockServer.isDataFileSystemOk()).thenReturn(true); + when(mockServer.getFileSystem()).thenReturn(mockFs); + + // Create RSRpcServices + RSRpcServices rpcServices = new RSRpcServices(mockServer); + + // Create request + byte[] keyCustodian = Bytes.toBytes("testCustodian"); + String keyNamespace = "testNamespace"; + String keyMetadata = "testMetadata"; + byte[] keyMetadataHash = ManagedKeyData.constructMetadataHash(keyMetadata); + + ManagedKeyEntryRequest request = ManagedKeyEntryRequest.newBuilder() + .setKeyCustNs(ManagedKeyRequest.newBuilder().setKeyCust(ByteString.copyFrom(keyCustodian)) + .setKeyNamespace(keyNamespace).build()) + .setKeyMetadataHash(ByteString.copyFrom(keyMetadataHash)).build(); + + RpcController controller = mock(RpcController.class); + + // Call the RPC method and expect ServiceException + try { + rpcServices.ejectManagedKeyDataCacheEntry(controller, request); + fail("Expected ServiceException when server is stopped"); + } catch (ServiceException e) { + // Expected + assertTrue("Exception should mention server stopping", + e.getCause().getMessage().contains("stopping")); + LOG.info("Correctly threw ServiceException when server is stopped"); + } + } + + /** + * Test the clearManagedKeyDataCache RPC method that is used to clear all cached entries in the + * ManagedKeyDataCache. + */ + @Test + public void testClearManagedKeyDataCache() throws Exception { + // Create mocks + HRegionServer mockServer = mock(HRegionServer.class); + Configuration conf = HBaseConfiguration.create(); + FileSystem mockFs = mock(FileSystem.class); + KeyManagementService mockKeyService = mock(KeyManagementService.class); + ManagedKeyDataCache mockCache = mock(ManagedKeyDataCache.class); + + when(mockServer.getConfiguration()).thenReturn(conf); + when(mockServer.isOnline()).thenReturn(true); + when(mockServer.isAborted()).thenReturn(false); + when(mockServer.isStopped()).thenReturn(false); + when(mockServer.isDataFileSystemOk()).thenReturn(true); + when(mockServer.getFileSystem()).thenReturn(mockFs); + when(mockServer.getKeyManagementService()).thenReturn(mockKeyService); + when(mockKeyService.getManagedKeyDataCache()).thenReturn(mockCache); + + // Create RSRpcServices + RSRpcServices rpcServices = new RSRpcServices(mockServer); + + // Create request + EmptyMsg request = EmptyMsg.getDefaultInstance(); + RpcController controller = mock(RpcController.class); + + // Call the RPC method + EmptyMsg response = rpcServices.clearManagedKeyDataCache(controller, request); + + // Verify the response is not null + assertNotNull("Response should not be null", response); + + // Verify that clearCache was called on the cache + verify(mockCache).clearCache(); + + LOG.info("clearManagedKeyDataCache test completed successfully"); + } + + /** + * Test that clearManagedKeyDataCache throws ServiceException when server is stopped + */ + @Test + public void testClearManagedKeyDataCacheWhenServerStopped() throws Exception { + // Create mocks + HRegionServer mockServer = mock(HRegionServer.class); + Configuration conf = HBaseConfiguration.create(); + FileSystem mockFs = mock(FileSystem.class); + + when(mockServer.getConfiguration()).thenReturn(conf); + when(mockServer.isOnline()).thenReturn(true); + when(mockServer.isAborted()).thenReturn(false); + when(mockServer.isStopped()).thenReturn(true); // Server is stopped + when(mockServer.isDataFileSystemOk()).thenReturn(true); + when(mockServer.getFileSystem()).thenReturn(mockFs); + + // Create RSRpcServices + RSRpcServices rpcServices = new RSRpcServices(mockServer); + + // Create request + EmptyMsg request = EmptyMsg.getDefaultInstance(); + RpcController controller = mock(RpcController.class); + + // Call the RPC method and expect ServiceException + try { + rpcServices.clearManagedKeyDataCache(controller, request); + fail("Expected ServiceException when server is stopped"); + } catch (ServiceException e) { + // Expected + assertTrue("Exception should mention server stopping", + e.getCause().getMessage().contains("stopping")); + LOG.info("Correctly threw ServiceException when server is stopped"); + } + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/rsgroup/VerifyingRSGroupAdmin.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/rsgroup/VerifyingRSGroupAdmin.java index d65b0e3956c5..60acb8a6acb4 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/rsgroup/VerifyingRSGroupAdmin.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/rsgroup/VerifyingRSGroupAdmin.java @@ -1001,7 +1001,19 @@ public boolean isReplicationPeerModificationEnabled() throws IOException { } @Override - public void refreshSystemKeyCacheOnServers(Set regionServers) throws IOException { + public void refreshSystemKeyCacheOnServers(List regionServers) throws IOException { admin.refreshSystemKeyCacheOnServers(regionServers); } + + @Override + public void ejectManagedKeyDataCacheEntryOnServers(List regionServers, + byte[] keyCustodian, String keyNamespace, String keyMetadata) throws IOException { + admin.ejectManagedKeyDataCacheEntryOnServers(regionServers, keyCustodian, keyNamespace, + keyMetadata); + } + + @Override + public void clearManagedKeyDataCacheOnServers(List regionServers) throws IOException { + admin.clearManagedKeyDataCacheOnServers(regionServers); + } } diff --git a/hbase-shell/src/main/ruby/hbase/keymeta_admin.rb b/hbase-shell/src/main/ruby/hbase/keymeta_admin.rb index 89b42c4070b0..12cd5445b066 100644 --- a/hbase-shell/src/main/ruby/hbase/keymeta_admin.rb +++ b/hbase-shell/src/main/ruby/hbase/keymeta_admin.rb @@ -47,6 +47,27 @@ def get_key_statuses(key_info) @admin.getManagedKeys(cust, namespace) end + def disable_key_management(key_info) + cust, namespace = extract_cust_info(key_info) + @admin.disableKeyManagement(cust, namespace) + end + + def disable_managed_key(key_info, key_metadata_hash_base64) + cust, namespace = extract_cust_info(key_info) + key_metadata_hash_bytes = decode_to_bytes(key_metadata_hash_base64) + @admin.disableManagedKey(cust, namespace, key_metadata_hash_bytes) + end + + def rotate_managed_key(key_info) + cust, namespace = extract_cust_info(key_info) + @admin.rotateManagedKey(cust, namespace) + end + + def refresh_managed_keys(key_info) + cust, namespace = extract_cust_info(key_info) + @admin.refreshManagedKeys(cust, namespace) + end + def rotate_stk @admin.rotateSTK end @@ -57,15 +78,18 @@ def extract_cust_info(key_info) custodian = cust_info[0] namespace = cust_info.length > 1 ? cust_info[1] : ManagedKeyData::KEY_SPACE_GLOBAL + cust_bytes = decode_to_bytes custodian + + [cust_bytes, namespace] + end + def decode_to_bytes(base64_string) begin - cust_bytes = ManagedKeyProvider.decodeToBytes(custodian) + ManagedKeyProvider.decodeToBytes(base64_string) rescue Java::JavaIo::IOException => e message = e.cause&.message || e.message - raise(ArgumentError, "Failed to decode key custodian '#{custodian}': #{message}") + raise(ArgumentError, "Failed to decode Base64 encoded string '#{base64_string}': #{message}") end - - [cust_bytes, namespace] end end end diff --git a/hbase-shell/src/main/ruby/shell.rb b/hbase-shell/src/main/ruby/shell.rb index 47dc7541b968..665fa4d06bbd 100644 --- a/hbase-shell/src/main/ruby/shell.rb +++ b/hbase-shell/src/main/ruby/shell.rb @@ -630,6 +630,10 @@ def self.exception_handler(hide_traceback) enable_key_management show_key_status rotate_stk + disable_key_management + disable_managed_key + refresh_managed_keys + rotate_managed_key ] ) diff --git a/hbase-shell/src/main/ruby/shell/commands/disable_key_management.rb b/hbase-shell/src/main/ruby/shell/commands/disable_key_management.rb new file mode 100644 index 000000000000..ead9dce96e8f --- /dev/null +++ b/hbase-shell/src/main/ruby/shell/commands/disable_key_management.rb @@ -0,0 +1,45 @@ +# +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# frozen_string_literal: true + +require 'shell/commands/keymeta_command_base' + +module Shell + module Commands + # DisableKeyManagement is a class that provides a Ruby interface to disable key management via + # HBase Key Management API. + class DisableKeyManagement < KeymetaCommandBase + def help + <<-EOF +Disable key management for a given cust:namespace (cust in Base64 format). +If no namespace is specified, the global namespace (*) is used. + +Example: + hbase> disable_key_management 'cust:namespace' + hbase> disable_key_management 'cust' + EOF + end + + def command(key_info) + statuses = [keymeta_admin.disable_key_management(key_info)] + print_key_statuses(statuses) + end + end + end +end diff --git a/hbase-shell/src/main/ruby/shell/commands/disable_managed_key.rb b/hbase-shell/src/main/ruby/shell/commands/disable_managed_key.rb new file mode 100644 index 000000000000..4384c0f3c825 --- /dev/null +++ b/hbase-shell/src/main/ruby/shell/commands/disable_managed_key.rb @@ -0,0 +1,45 @@ +# +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# frozen_string_literal: true + +require 'shell/commands/keymeta_command_base' + +module Shell + module Commands + # DisableManagedKey is a class that provides a Ruby interface to disable a managed key via + # HBase Key Management API. + class DisableManagedKey < KeymetaCommandBase + def help + <<-EOF +Disable a managed key for a given cust:namespace (cust in Base64 encoded) and key metadata hash +(Base64 encoded). If no namespace is specified, the global namespace (*) is used. + +Example: + hbase> disable_managed_key 'cust:namespace key_metadata_hash_base64' + hbase> disable_managed_key 'cust key_metadata_hash_base64' + EOF + end + + def command(key_info, key_metadata_hash_base64) + statuses = [keymeta_admin.disable_managed_key(key_info, key_metadata_hash_base64)] + print_key_statuses(statuses) + end + end + end +end diff --git a/hbase-shell/src/main/ruby/shell/commands/enable_key_management.rb b/hbase-shell/src/main/ruby/shell/commands/enable_key_management.rb index da3fe6ad8c91..d594fa024b68 100644 --- a/hbase-shell/src/main/ruby/shell/commands/enable_key_management.rb +++ b/hbase-shell/src/main/ruby/shell/commands/enable_key_management.rb @@ -27,7 +27,7 @@ module Commands class EnableKeyManagement < KeymetaCommandBase def help <<-EOF -Enable key management for a given cust:namespace (cust in Base64 format). +Enable key management for a given cust:namespace (cust in Base64 encoded). If no namespace is specified, the global namespace (*) is used. Example: diff --git a/hbase-shell/src/main/ruby/shell/commands/refresh_managed_keys.rb b/hbase-shell/src/main/ruby/shell/commands/refresh_managed_keys.rb new file mode 100644 index 000000000000..f4c462ceee19 --- /dev/null +++ b/hbase-shell/src/main/ruby/shell/commands/refresh_managed_keys.rb @@ -0,0 +1,45 @@ +# +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# frozen_string_literal: true + +require 'shell/commands/keymeta_command_base' + +module Shell + module Commands + # RefreshManagedKeys is a class that provides a Ruby interface to refresh managed keys via + # HBase Key Management API. + class RefreshManagedKeys < KeymetaCommandBase + def help + <<-EOF +Refresh all managed keys for a given cust:namespace (cust in Base64 encoded). +If no namespace is specified, the global namespace (*) is used. + +Example: + hbase> refresh_managed_keys 'cust:namespace' + hbase> refresh_managed_keys 'cust' + EOF + end + + def command(key_info) + keymeta_admin.refresh_managed_keys(key_info) + puts "Managed keys refreshed successfully" + end + end + end +end diff --git a/hbase-shell/src/main/ruby/shell/commands/rotate_managed_key.rb b/hbase-shell/src/main/ruby/shell/commands/rotate_managed_key.rb new file mode 100644 index 000000000000..6372d30839e5 --- /dev/null +++ b/hbase-shell/src/main/ruby/shell/commands/rotate_managed_key.rb @@ -0,0 +1,45 @@ +# +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# frozen_string_literal: true + +require 'shell/commands/keymeta_command_base' + +module Shell + module Commands + # RotateManagedKey is a class that provides a Ruby interface to rotate a managed key via + # HBase Key Management API. + class RotateManagedKey < KeymetaCommandBase + def help + <<-EOF +Rotate the ACTIVE managed key for a given cust:namespace (cust in Base64 encoded). +If no namespace is specified, the global namespace (*) is used. + +Example: + hbase> rotate_managed_key 'cust:namespace' + hbase> rotate_managed_key 'cust' + EOF + end + + def command(key_info) + statuses = [keymeta_admin.rotate_managed_key(key_info)] + print_key_statuses(statuses) + end + end + end +end diff --git a/hbase-shell/src/test/ruby/shell/admin_keymeta_mock_provider_test.rb b/hbase-shell/src/test/ruby/shell/admin_keymeta_mock_provider_test.rb new file mode 100644 index 000000000000..061e3fc71230 --- /dev/null +++ b/hbase-shell/src/test/ruby/shell/admin_keymeta_mock_provider_test.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +# +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'hbase_shell' +require 'stringio' +require 'hbase_constants' +require 'hbase/hbase' +require 'hbase/table' + +java_import org.apache.hadoop.hbase.io.crypto.Encryption +java_import org.apache.hadoop.hbase.io.crypto.MockManagedKeyProvider +java_import org.apache.hadoop.hbase.io.crypto.ManagedKeyProvider +java_import org.apache.hadoop.hbase.ipc.RemoteWithExtrasException + +module Hbase + # Test class for keymeta admin functionality with MockManagedKeyProvider + class KeymetaAdminMockProviderTest < Test::Unit::TestCase + include TestHelpers + + def setup + setup_hbase + @key_provider = Encryption.getManagedKeyProvider($TEST_CLUSTER.getConfiguration) + # Enable multikey generation mode for dynamic key creation on rotate + @key_provider.setMultikeyGenMode(true) + + # Set up custodian variables + @glob_cust = '*' + @glob_cust_encoded = ManagedKeyProvider.encodeToStr(@glob_cust.bytes.to_a) + end + + define_test 'Test rotate managed key operation' do + test_rotate_key(@glob_cust_encoded, '*') + test_rotate_key(@glob_cust_encoded, 'test_namespace') + end + + def test_rotate_key(cust, namespace) + cust_and_namespace = "#{cust}:#{namespace}" + puts "Testing rotate_managed_key for #{cust_and_namespace}" + + # 1. Enable key management first + output = capture_stdout { @shell.command('enable_key_management', cust_and_namespace) } + puts "enable_key_management output: #{output}" + assert(output.include?("#{cust} #{namespace} ACTIVE"), + "Expected ACTIVE key after enable, got: #{output}") + + # Verify initial state - should have 1 ACTIVE key + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status before rotation: #{output}" + assert(output.include?('1 row(s)'), "Expected 1 key before rotation, got: #{output}") + + # 2. Rotate the managed key (mock provider will generate a new key due to multikeyGenMode) + output = capture_stdout { @shell.command('rotate_managed_key', cust_and_namespace) } + puts "rotate_managed_key output: #{output}" + assert(output.include?("#{cust} #{namespace}"), + "Expected key info in rotation output, got: #{output}") + + # 3. Verify we now have both ACTIVE and INACTIVE keys + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status after rotation: #{output}" + assert(output.include?('ACTIVE'), + "Expected ACTIVE key after rotation, got: #{output}") + assert(output.include?('INACTIVE'), + "Expected INACTIVE key after rotation, got: #{output}") + assert(output.include?('2 row(s)'), + "Expected 2 keys after rotation, got: #{output}") + + # 4. Rotate again to test multiple rotations + output = capture_stdout { @shell.command('rotate_managed_key', cust_and_namespace) } + puts "rotate_managed_key (second) output: #{output}" + assert(output.include?("#{cust} #{namespace}"), + "Expected key info in second rotation output, got: #{output}") + + # Should now have 3 keys: 1 ACTIVE, 2 INACTIVE + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status after second rotation: #{output}" + assert(output.include?('3 row(s)'), + "Expected 3 keys after second rotation, got: #{output}") + + # Cleanup - disable all keys + @shell.command('disable_key_management', cust_and_namespace) + end + + define_test 'Test rotate without active key fails' do + cust_and_namespace = "#{@glob_cust_encoded}:nonexistent_namespace" + puts "Testing rotate_managed_key on non-existent namespace" + + # Attempt to rotate when no key management is enabled should fail + e = assert_raises(RemoteWithExtrasException) do + @shell.command('rotate_managed_key', cust_and_namespace) + end + assert_true(e.is_do_not_retry) + end + + define_test 'Test refresh managed keys with mock provider' do + cust_and_namespace = "#{@glob_cust_encoded}:test_refresh" + puts "Testing refresh_managed_keys for #{cust_and_namespace}" + + # 1. Enable key management + output = capture_stdout { @shell.command('enable_key_management', cust_and_namespace) } + puts "enable_key_management output: #{output}" + assert(output.include?("#{@glob_cust_encoded} test_refresh ACTIVE")) + + # 2. Rotate to create multiple keys + output = capture_stdout { @shell.command('rotate_managed_key', cust_and_namespace) } + puts "rotate_managed_key output: #{output}" + assert(output.include?("#{@glob_cust_encoded} test_refresh"), + "Expected key info in rotation output, got: #{output}") + + # 3. Refresh managed keys - should succeed without changing state + output = capture_stdout { @shell.command('refresh_managed_keys', cust_and_namespace) } + puts "refresh_managed_keys output: #{output}" + assert(output.include?('Managed keys refreshed successfully'), + "Expected success message, got: #{output}") + + # Verify keys still exist after refresh + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + assert(output.include?('ACTIVE'), "Expected ACTIVE key after refresh") + assert(output.include?('INACTIVE'), "Expected INACTIVE key after refresh") + + # Cleanup + @shell.command('disable_key_management', cust_and_namespace) + end + end +end + diff --git a/hbase-shell/src/test/ruby/shell/admin_keymeta_test.rb b/hbase-shell/src/test/ruby/shell/admin_keymeta_test.rb index 83a73842711a..ab413ecbb0bb 100644 --- a/hbase-shell/src/test/ruby/shell/admin_keymeta_test.rb +++ b/hbase-shell/src/test/ruby/shell/admin_keymeta_test.rb @@ -71,7 +71,123 @@ def test_key_management(cust, namespace) error = assert_raises(ArgumentError) do @shell.command('show_key_status', '!!!:namespace') end - assert_match(/Failed to decode key custodian/, error.message) + assert_match(/Failed to decode Base64 encoded string '!!!'/, error.message) + end + + define_test 'Test key management operations without rotation' do + test_key_operations($CUST1_ENCODED, '*') + test_key_operations($CUST1_ENCODED, 'test_namespace') + test_key_operations($GLOB_CUST_ENCODED, '*') + end + + def test_key_operations(cust, namespace) + cust_and_namespace = "#{cust}:#{namespace}" + puts "Testing key management operations for #{cust_and_namespace}" + + # 1. Enable key management + output = capture_stdout { @shell.command('enable_key_management', cust_and_namespace) } + puts "enable_key_management output: #{output}" + assert(output.include?("#{cust} #{namespace} ACTIVE"), + "Expected ACTIVE key after enable, got: #{output}") + + # 2. Get the initial key metadata hash for use in disable_managed_key test + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status output: #{output}" + # Extract the metadata hash from the output (it's in the 5th column) + # Output format: ENCODED-KEY NAMESPACE STATUS METADATA METADATA-HASH REFRESH-TIMESTAMP + lines = output.split("\n") + key_line = lines.find { |line| line.include?(cust) && line.include?(namespace) } + assert_not_nil(key_line, "Could not find key line in output") + # Parse the key metadata hash (Base64 encoded) + key_metadata_hash = key_line.split[3] + assert_not_nil(key_metadata_hash, "Could not extract key metadata hash") + puts "Extracted key metadata hash: #{key_metadata_hash}" + + # 3. Refresh managed keys + output = capture_stdout { @shell.command('refresh_managed_keys', cust_and_namespace) } + puts "refresh_managed_keys output: #{output}" + assert(output.include?('Managed keys refreshed successfully'), + "Expected success message, got: #{output}") + # Verify keys still exist after refresh + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status after refresh: #{output}" + assert(output.include?('ACTIVE'), "Expected ACTIVE key after refresh, got: #{output}") + + # 4. Disable a specific managed key + output = capture_stdout do + @shell.command('disable_managed_key', cust_and_namespace, key_metadata_hash) + end + puts "disable_managed_key output: #{output}" + assert(output.include?("#{cust} #{namespace} DISABLED"), + "Expected INACTIVE key, got: #{output}") + # Verify the key is now INACTIVE + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status after disable_managed_key: #{output}" + assert(output.include?('DISABLED'), "Expected DISABLED state, got: #{output}") + + # 5. Re-enable key management for next step + @shell.command('enable_key_management', cust_and_namespace) + + # 6. Disable all key management + output = capture_stdout { @shell.command('disable_key_management', cust_and_namespace) } + puts "disable_key_management output: #{output}" + assert(output.include?("#{cust} #{namespace} DISABLED"), + "Expected DISABLED keys, got: #{output}") + # Verify all keys are now INACTIVE + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status after disable_key_management: #{output}" + # All rows should show INACTIVE state + lines = output.split("\n") + key_lines = lines.select { |line| line.include?(cust) && line.include?(namespace) } + key_lines.each do |line| + assert(line.include?('INACTIVE'), "Expected all keys to be INACTIVE, but found: #{line}") + end + + # 7. Refresh shouldn't do anything since the key management is disabled. + output = capture_stdout do + @shell.command('refresh_managed_keys', cust_and_namespace) + end + puts "refresh_managed_keys output: #{output}" + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status after refresh_managed_keys: #{output}" + assert(!output.include?(' ACTIVE '), "Expected all keys to be INACTIVE, but found: #{output}") + + # 7. Enable key management again + @shell.command('enable_key_management', cust_and_namespace) + + # 8. Get the key metadata hash for the enabled key + output = capture_stdout { @shell.command('show_key_status', cust_and_namespace) } + puts "show_key_status after enable_key_management: #{output}" + assert(output.include?('ACTIVE'), "Expected ACTIVE key after enable_key_management, got: #{output}") + assert(output.include?('1 row(s)')) + end + + define_test 'Test refresh error handling' do + # Test refresh on non-existent key management (should not fail, just no-op) + cust_and_namespace = "#{$CUST1_ENCODED}:nonexistent_namespace" + output = capture_stdout do + @shell.command('refresh_managed_keys', cust_and_namespace) + end + puts "refresh_managed_keys on non-existent namespace: #{output}" + assert(output.include?('Managed keys refreshed successfully'), + "Expected success message even for non-existent namespace, got: #{output}") + end + + define_test 'Test disable operations error handling' do + # Test disable_managed_key with invalid metadata hash + cust_and_namespace = "#{$CUST1_ENCODED}:*" + error = assert_raises(ArgumentError) do + @shell.command('disable_managed_key', cust_and_namespace, '!!!invalid!!!') + end + assert_match(/Failed to decode Base64 encoded string '!!!invalid!!!'/, error.message) + + # Test disable_key_management on non-existent namespace (should succeed, no-op) + cust_and_namespace = "#{$CUST1_ENCODED}:nonexistent_for_disable" + output = capture_stdout { @shell.command('disable_key_management', cust_and_namespace) } + puts "disable_key_management on non-existent namespace: #{output}" + # Should show 0 rows since no keys exist + assert(output.include?('1 row(s)')) + assert(output.include?(" DISABLED "), "Expected DISABLED key, got: #{output}") end end end diff --git a/hbase-shell/src/test/ruby/shell/key_provider_keymeta_migration_test.rb b/hbase-shell/src/test/ruby/shell/key_provider_keymeta_migration_test.rb index 8a179c8af2f6..d527eea8240c 100644 --- a/hbase-shell/src/test/ruby/shell/key_provider_keymeta_migration_test.rb +++ b/hbase-shell/src/test/ruby/shell/key_provider_keymeta_migration_test.rb @@ -429,11 +429,10 @@ def migrate_table_to_managed_key(table_name, cf_name, namespace, puts " >> Altered #{table_name} CF #{cf_name} to use namespace #{namespace}" - # The new encryption attribute won't be used unless HStore is reinitialized. - # To force reinitialization, disable and enable the table. - command(:disable, table_name) - command(:enable, table_name) - # sleep for 5s to ensure region is reopened and store is reinitialized + # The CF alter should trigger an online schema change and should cause the stores to be + # reopened and the encryption context to be reinitialized, but it is asynchronous and may take + # some time, so we sleep for 5s, following the same pattern as in the + # TestEncryptionKeyRotation.testCFKeyRotation(). sleep(5) # Scan all existing data to verify accessibility diff --git a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java index b26cf1ead4d5..3b52b916efe8 100644 --- a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java +++ b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java @@ -1378,8 +1378,21 @@ public boolean isReplicationPeerModificationEnabled() throws IOException { } @Override - public void refreshSystemKeyCacheOnServers(Set regionServers) throws IOException { + public void refreshSystemKeyCacheOnServers(List regionServers) throws IOException { throw new NotImplementedException( "refreshSystemKeyCacheOnServers not supported in ThriftAdmin"); } + + @Override + public void ejectManagedKeyDataCacheEntryOnServers(List regionServers, + byte[] keyCustodian, String keyNamespace, String keyMetadata) throws IOException { + throw new NotImplementedException( + "ejectManagedKeyDataCacheEntryOnServers not supported in ThriftAdmin"); + } + + @Override + public void clearManagedKeyDataCacheOnServers(List regionServers) throws IOException { + throw new NotImplementedException( + "clearManagedKeyDataCacheOnServers not supported in ThriftAdmin"); + } }