blob: bcceee7c132bdb496f00217f696f420126b7325f [file] [log] [blame]
/*
* 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.ignite.internal.processors.cache.persistence.snapshot;
import java.io.File;
import java.util.Collections;
import java.util.function.Function;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteDataStreamer;
import org.apache.ignite.IgniteException;
import org.apache.ignite.cluster.ClusterState;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.internal.encryption.AbstractEncryptionTest;
import org.apache.ignite.internal.processors.cache.verify.IdleVerifyResultV2;
import org.apache.ignite.internal.util.distributed.FullMessage;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.CU;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteFuture;
import org.apache.ignite.spi.IgniteSpiException;
import org.apache.ignite.testframework.GridTestUtils;
import org.junit.Test;
import org.junit.runners.Parameterized;
import static org.apache.ignite.cluster.ClusterState.ACTIVE;
import static org.apache.ignite.configuration.IgniteConfiguration.DFLT_SNAPSHOT_DIRECTORY;
/**
* Snapshot test for encrypted-only snapshots.
*/
public class EncryptedSnapshotTest extends AbstractSnapshotSelfTest {
/** Second cache name. */
private static final String CACHE2 = "cache2";
/** Parameters. */
@Parameterized.Parameters(name = "Encryption is enabled.")
public static Iterable<Boolean> enableEncryption() {
return Collections.singletonList(true);
}
/** {@inheritDoc} */
@Override protected Function<Integer, Object> valueBuilder() {
return (i -> new Account(i, i));
}
/** Checks creation of encrypted cache with same name after putting plain cache in snapshot. */
@Test
public void testEncryptedCacheCreatedAfterPlainCacheSnapshotting() throws Exception {
testCacheCreatedAfterSnapshotting(true);
}
/** Checks creation of plain cache with same name after putting encrypted cache in snapshot. */
@Test
public void testPlainCacheCreatedAfterEncryptedCacheSnapshotting() throws Exception {
testCacheCreatedAfterSnapshotting(false);
}
/** Checks re-encryption fails during snapshot restoration. */
@Test
public void testReencryptDuringRestore() throws Exception {
checkActionFailsDuringSnapshotOperation(true, this::chageCacheKey, "Cache group key change " +
"was rejected.", IgniteException.class);
}
/** Checks master key changing fails during snapshot restoration. */
@Test
public void testMasterKeyChangeDuringRestore() throws Exception {
checkActionFailsDuringSnapshotOperation(true, this::chageMasterKey, "Master key change was " +
"rejected.", IgniteException.class);
}
/** Checks re-encryption fails during snapshot creation. */
@Test
public void testReencryptDuringSnapshot() throws Exception {
checkActionFailsDuringSnapshotOperation(false, this::chageCacheKey, "Cache group key change " +
"was rejected.", IgniteException.class);
}
/** Checks master key changing fails during snapshot creation. */
@Test
public void testMasterKeyChangeDuringSnapshot() throws Exception {
checkActionFailsDuringSnapshotOperation(false, this::chageMasterKey, "Master key change was " +
"rejected.", IgniteException.class);
}
/** Checks snapshot action fail during cache group key change. */
@Test
public void testSnapshotFailsDuringCacheKeyChange() throws Exception {
checkSnapshotActionFailsDuringReencryption(this::chageCacheKey, "Caches re-encryption process " +
"is not finished yet");
}
/** Checks snapshot action fail during master key change. */
@Test
public void testSnapshotFailsDuringMasterKeyChange() throws Exception {
checkSnapshotActionFailsDuringReencryption(this::chageMasterKey, "Master key changing process " +
"is not finished yet.");
}
/** Checks snapshot restoration fails if different master key is used. */
@Test
public void testSnapshotRestoringFailsWithOtherMasterKey() throws Exception {
IgniteEx ig = startGridsWithCache(2, CACHE_KEYS_RANGE, valueBuilder(), dfltCacheCfg);
snp(ig).createSnapshot(SNAPSHOT_NAME).get();
ig.destroyCache(dfltCacheCfg.getName());
ensureCacheAbsent(dfltCacheCfg);
stopAllGrids(false);
masterKeyName = AbstractEncryptionTest.MASTER_KEY_NAME_2;
final IgniteEx ig1 = startGrids(2);
ig1.cluster().state(ACTIVE);
GridTestUtils.assertThrowsAnyCause(
log,
() -> snp(ig1).restoreSnapshot(SNAPSHOT_NAME, Collections.singletonList(dfltCacheCfg.getName())).get(TIMEOUT),
IgniteCheckedException.class,
"different master key digest"
);
}
/** Checks both encrypted and plain caches can be restored from same snapshot. */
@Test
public void testRestoringEncryptedAndPlainCaches() throws Exception {
start2GridsWithEncryptesAndPlainCachesSnapshot();
grid(1).snapshot().restoreSnapshot(SNAPSHOT_NAME, null).get(TIMEOUT);
assertCacheKeys(grid(1).cache(DEFAULT_CACHE_NAME), CACHE_KEYS_RANGE);
assertCacheKeys(grid(1).cache(CACHE2), CACHE_KEYS_RANGE);
}
/** Checks both encrypted and plain caches can be restored from same snapshot. */
@Test
public void testStartingWithEncryptedAndPlainCaches() throws Exception {
start2GridsWithEncryptesAndPlainCachesSnapshot();
stopAllGrids();
IgniteEx ig = startGridsFromSnapshot(2, SNAPSHOT_NAME);
assertCacheKeys(ig.cache(DEFAULT_CACHE_NAME), CACHE_KEYS_RANGE);
assertCacheKeys(ig.cache(CACHE2), CACHE_KEYS_RANGE);
}
/** Checks snapshot after single reencryption. */
@Test
public void testSnapshotRestoringAfterSingleReencryption() throws Exception {
checkSnapshotWithReencryptedCache(1);
}
/** Checks snapshot after multiple reencryption. */
@Test
public void testSnapshotRestoringAfterMultipleReencryption() throws Exception {
checkSnapshotWithReencryptedCache(3);
}
/** Checks snapshot validati fails if different master key is used. */
@Test
public void testValidatingSnapshotFailsWithOtherMasterKey() throws Exception {
IgniteEx ig = startGridsWithCache(2, CACHE_KEYS_RANGE, valueBuilder(), dfltCacheCfg);
ig.snapshot().createSnapshot(SNAPSHOT_NAME).get();
ig.destroyCache(dfltCacheCfg.getName());
ensureCacheAbsent(dfltCacheCfg);
stopAllGrids(false);
masterKeyName = AbstractEncryptionTest.MASTER_KEY_NAME_2;
ig = startGrids(2);
IdleVerifyResultV2 snpCheckRes = snp(ig).checkSnapshot(SNAPSHOT_NAME, null).get();
for (Exception e : snpCheckRes.exceptions().values()) {
if (e.getMessage().contains("different master key digest"))
return;
}
throw new IllegalStateException("Snapshot validation must contain error due to different master key.");
}
/** @throws Exception If fails. */
@Test
public void testValidatingSnapshotFailsWithNoEncryption() throws Exception {
File tmpSnpDir = null;
try {
startGridsWithSnapshot(3, CACHE_KEYS_RANGE, false);
stopAllGrids();
encryption = false;
dfltCacheCfg = null;
File snpDir = U.resolveWorkDirectory(U.defaultWorkDirectory(), DFLT_SNAPSHOT_DIRECTORY, false);
assertTrue(snpDir.isDirectory() && snpDir.listFiles().length > 0);
tmpSnpDir = new File(snpDir.getAbsolutePath() + "_tmp");
assertTrue(tmpSnpDir.length() == 0);
assertTrue(snpDir.renameTo(tmpSnpDir));
cleanPersistenceDir();
assertTrue(tmpSnpDir.renameTo(snpDir));
IgniteEx ig = startGrids(3);
snpDir.renameTo(U.resolveWorkDirectory(U.defaultWorkDirectory(), DFLT_SNAPSHOT_DIRECTORY, false));
ig.cluster().state(ACTIVE);
IdleVerifyResultV2 snpCheckRes = snp(ig).checkSnapshot(SNAPSHOT_NAME, null).get();
for (Exception e : snpCheckRes.exceptions().values()) {
if (e.getMessage().contains("has encrypted caches while encryption is disabled"))
return;
}
throw new IllegalStateException("Snapshot validation must contain error due to encryption is currently " +
"disabled.");
}
finally {
if (tmpSnpDir != null)
U.delete(tmpSnpDir);
}
}
/** Checks snapshot restoration fails if different master key is contained in the snapshot. */
@Test
public void testStartFromSnapshotFailedWithOtherMasterKey() throws Exception {
IgniteEx ig = startGridsWithCache(2, CACHE_KEYS_RANGE, valueBuilder(), dfltCacheCfg);
ig.snapshot().createSnapshot(SNAPSHOT_NAME).get();
ig.destroyCache(dfltCacheCfg.getName());
ensureCacheAbsent(dfltCacheCfg);
stopAllGrids(false);
masterKeyName = AbstractEncryptionTest.MASTER_KEY_NAME_2;
GridTestUtils.assertThrowsAnyCause(
log,
() -> startGridsFromSnapshot(2, SNAPSHOT_NAME),
IgniteSpiException.class,
"bad key is used during decryption"
);
}
/** Checks it is unavailable to register snapshot task for encrypted caches without metastore. */
@Test
public void testSnapshotTaskIsBlockedWithoutMetastore() throws Exception {
// Start grid node with data before each test.
IgniteEx ig = startGridsWithCache(1, CACHE_KEYS_RANGE, valueBuilder(), dfltCacheCfg);
GridTestUtils.assertThrowsAnyCause(log,
() -> snp(ig).registerSnapshotTask(SNAPSHOT_NAME, ig.localNode().id(),
null, F.asMap(CU.cacheId(dfltCacheCfg.getName()), null), false,
snp(ig).localSnapshotSenderFactory().apply(SNAPSHOT_NAME, null)).get(TIMEOUT),
IgniteCheckedException.class,
"Metastore is required because it holds encryption keys");
}
/** {@inheritDoc} */
@Override protected void ensureCacheAbsent(
CacheConfiguration<?, ?> ccfg) throws IgniteCheckedException, InterruptedException {
awaitPartitionMapExchange();
super.ensureCacheAbsent(ccfg);
}
/**
* Ensures that same-name-cache is created after putting cache into snapshot and deleting.
*
* @param encryptedFirst If {@code true}, creates encrypted cache before snapshoting and deleting. In reverse order
* {@code false}.
*/
private void testCacheCreatedAfterSnapshotting(boolean encryptedFirst) throws Exception {
startGrids(2);
grid(0).cluster().state(ClusterState.ACTIVE);
addCache(encryptedFirst);
grid(1).snapshot().createSnapshot(SNAPSHOT_NAME).get(TIMEOUT);
awaitPartitionMapExchange();
grid(0).destroyCache(CACHE2);
awaitPartitionMapExchange();
addCache(!encryptedFirst);
}
/**
* Checks snapshot after reencryption.
*
* @param reencryptionIterations Number re-encryptions turns.
*/
private void checkSnapshotWithReencryptedCache(int reencryptionIterations) throws Exception {
IgniteEx ig = startGridsWithCache(2, CACHE_KEYS_RANGE, valueBuilder(), dfltCacheCfg.setName(CACHE2));
for (int r = 0; r < reencryptionIterations; ++r) {
chageCacheKey(0).get(TIMEOUT);
for (int g = 0; g < 2; ++g)
grid(g).context().encryption().reencryptionFuture(CU.cacheId(dfltCacheCfg.getName())).get();
}
ig.snapshot().createSnapshot(SNAPSHOT_NAME).get(TIMEOUT);
ig.cache(dfltCacheCfg.getName()).destroy();
ensureCacheAbsent(dfltCacheCfg);
ig.snapshot().restoreSnapshot(SNAPSHOT_NAME, null).get(TIMEOUT);
assertCacheKeys(grid(1).cache(dfltCacheCfg.getName()), CACHE_KEYS_RANGE);
stopAllGrids();
startGridsFromSnapshot(2, SNAPSHOT_NAME);
assertCacheKeys(grid(1).cache(dfltCacheCfg.getName()), CACHE_KEYS_RANGE);
}
/**
* Checks {@code action} is blocked with {@code errPrefix} and {@code errEncrypType} during active snapshot.
*
* @param restore If {@code true}, snapshot restoration is activated during the test. Snapshot creation otherwise.
* @param action Action to call during snapshot operation. Its param is the grid num.
* @param errPrefix Prefix of error message text to search for.
* @param errType Type of exception to search for.
*/
private void checkActionFailsDuringSnapshotOperation(boolean restore, Function<Integer, IgniteFuture<?>> action,
String errPrefix, Class<? extends Exception> errType) throws Exception {
startGridsWithCache(3, CACHE_KEYS_RANGE, valueBuilder(), dfltCacheCfg,
new CacheConfiguration<>(dfltCacheCfg).setName(CACHE2));
BlockingCustomMessageDiscoverySpi spi0 = discoSpi(grid(0));
IgniteFuture<Void> fut;
if (restore) {
grid(1).snapshot().createSnapshot(SNAPSHOT_NAME).get(TIMEOUT);
grid(1).cache(dfltCacheCfg.getName()).destroy();
ensureCacheAbsent(dfltCacheCfg);
spi0.block((msg) -> msg instanceof FullMessage && ((FullMessage<?>)msg).error().isEmpty());
fut = grid(1).snapshot().restoreSnapshot(SNAPSHOT_NAME, Collections.singletonList(dfltCacheCfg.getName()));
}
else {
spi0.block((msg) -> msg instanceof FullMessage && ((FullMessage<?>)msg).error().isEmpty());
fut = grid(1).snapshot().createSnapshot(SNAPSHOT_NAME);
}
spi0.waitBlocked(TIMEOUT);
GridTestUtils.assertThrowsAnyCause(log, () -> action.apply(2).get(TIMEOUT), errType,
errPrefix + " Snapshot operation is in progress.");
spi0.unblock();
fut.get(TIMEOUT);
}
/**
* Checks snapshot action is blocked during {@code reencryption}.
*
* @param reencryption Any kind of re-encryption action.
*/
private void checkSnapshotActionFailsDuringReencryption(Function<Integer, IgniteFuture<?>> reencryption,
String expectedError) throws Exception {
startGridsWithCache(3, CACHE_KEYS_RANGE, valueBuilder(), dfltCacheCfg,
new CacheConfiguration<>(dfltCacheCfg).setName(CACHE2));
grid(1).snapshot().createSnapshot(SNAPSHOT_NAME).get(TIMEOUT);
grid(2).destroyCache(dfltCacheCfg.getName());
ensureCacheAbsent(dfltCacheCfg);
BlockingCustomMessageDiscoverySpi discoSpi = discoSpi(grid(0));
discoSpi.block(msg -> msg instanceof FullMessage && ((FullMessage<?>)msg).error().isEmpty());
IgniteFuture<?> fut = reencryption.apply(1);
discoSpi.waitBlocked(TIMEOUT);
GridTestUtils.assertThrowsAnyCause(log,
() -> grid(1).snapshot().restoreSnapshot(SNAPSHOT_NAME, Collections.singletonList(CACHE2)).get(TIMEOUT),
IgniteCheckedException.class,
expectedError);
GridTestUtils.assertThrowsAnyCause(log,
() -> grid(2).snapshot().createSnapshot(SNAPSHOT_NAME + "_v2").get(TIMEOUT), IgniteCheckedException.class,
expectedError);
discoSpi.unblock();
fut.get(TIMEOUT);
}
/**
* Adds cache to the grid. Fills it and waits for PME.
*
* @param encrypted If {@code true}, created encrypted cache.
* @return CacheConfiguration of the created cache.
*/
private CacheConfiguration<?, ?> addCache(boolean encrypted) throws IgniteCheckedException {
CacheConfiguration<?, ?> cacheCfg = new CacheConfiguration<>(dfltCacheCfg).setName(CACHE2).
setEncryptionEnabled(encrypted);
grid(0).createCache(cacheCfg);
Function<Integer, Object> valBuilder = valueBuilder();
IgniteDataStreamer<Integer, Object> streamer = grid(0).dataStreamer(CACHE2);
for (int i = 0; i < CACHE_KEYS_RANGE; i++)
streamer.addData(i, valBuilder.apply(i));
streamer.flush();
forceCheckpoint();
return cacheCfg;
}
/**
* Starts 2 nodes, creates encrypted and plain caches, creates snapshot, destroes the caches. Ensures caches absent.
*/
private void start2GridsWithEncryptesAndPlainCachesSnapshot() throws Exception {
startGridsWithCache(2, CACHE_KEYS_RANGE, valueBuilder(), dfltCacheCfg);
CacheConfiguration<?, ?> ccfg = addCache(false);
grid(1).snapshot().createSnapshot(SNAPSHOT_NAME).get(TIMEOUT);
grid(1).cache(DEFAULT_CACHE_NAME).destroy();
grid(1).cache(CACHE2).destroy();
ensureCacheAbsent(dfltCacheCfg);
ensureCacheAbsent(ccfg);
}
/**
* @return Cache group key change action.
*/
private IgniteFuture<?> chageCacheKey(int gridNum) {
return grid(gridNum).encryption().changeCacheGroupKey(Collections.singletonList(CACHE2));
}
/**
* @return Master key change action.
*/
private IgniteFuture<?> chageMasterKey(int gridNum) {
return grid(gridNum).encryption().changeMasterKey(AbstractEncryptionTest.MASTER_KEY_NAME_2);
}
}