mirror of https://github.com/hyperledger/besu
[NC-1968] Permissioning whitelist persistence. (#763)
* [NC-1968] Initial commit. * [NC-1968] Acceptance tests. * [NC-1968] PR fixes. * [NC-1968] Merge conflicts. Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
caa5acfa72
commit
7622541714
@ -0,0 +1,50 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* Licensed 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 tech.pegasys.pantheon.tests.acceptance.dsl.condition.perm; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.permissioning.WhitelistPersistor; |
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.condition.Condition; |
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; |
||||
|
||||
import java.nio.file.Path; |
||||
import java.util.Collection; |
||||
|
||||
public class WhiteListContainsKeyAndValue implements Condition { |
||||
private final WhitelistPersistor.WHITELIST_TYPE whitelistType; |
||||
private final Collection<String> whitelistValues; |
||||
private final Path configFilePath; |
||||
|
||||
public WhiteListContainsKeyAndValue( |
||||
final WhitelistPersistor.WHITELIST_TYPE whitelistType, |
||||
final Collection<String> whitelistValues, |
||||
final Path configFilePath) { |
||||
this.whitelistType = whitelistType; |
||||
this.whitelistValues = whitelistValues; |
||||
this.configFilePath = configFilePath; |
||||
} |
||||
|
||||
@Override |
||||
public void verify(final Node node) { |
||||
boolean result; |
||||
try { |
||||
result = |
||||
WhitelistPersistor.verifyConfigFileMatchesState( |
||||
whitelistType, whitelistValues, configFilePath); |
||||
} catch (final Exception e) { |
||||
result = false; |
||||
} |
||||
assertThat(result).isTrue(); |
||||
} |
||||
} |
@ -0,0 +1,102 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* Licensed 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 tech.pegasys.pantheon.tests.acceptance.permissioning; |
||||
|
||||
import static tech.pegasys.pantheon.ethereum.permissioning.WhitelistPersistor.WHITELIST_TYPE; |
||||
|
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; |
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.account.Account; |
||||
import tech.pegasys.pantheon.tests.acceptance.dsl.node.Node; |
||||
|
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
|
||||
import org.assertj.core.util.Lists; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
public class WhitelistPersistorAcceptanceTest extends AcceptanceTestBase { |
||||
|
||||
private Node node; |
||||
private Account senderA; |
||||
private Account senderB; |
||||
private Path tempFile; |
||||
|
||||
private final String enode1 = |
||||
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; |
||||
private final String enode2 = |
||||
"enode://5f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; |
||||
private final String enode3 = |
||||
"enode://4f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; |
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
senderA = accounts.getPrimaryBenefactor(); |
||||
senderB = accounts.getSecondaryBenefactor(); |
||||
tempFile = Files.createTempFile("test", "test"); |
||||
node = |
||||
pantheon.createNodeWithWhitelistsEnabled( |
||||
"node", |
||||
new ArrayList<>(), |
||||
Collections.singletonList(senderA.getAddress()), |
||||
tempFile.toAbsolutePath().toString()); |
||||
cluster.start(node); |
||||
} |
||||
|
||||
@Test |
||||
public void manipulatedAccountsWhitelistIsPersisted() { |
||||
node.verify( |
||||
perm.expectPermissioningWhitelistFileKeyValue( |
||||
WHITELIST_TYPE.ACCOUNTS, Collections.singleton(senderA.getAddress()), tempFile)); |
||||
|
||||
node.execute(transactions.addAccountsToWhitelist(senderB.getAddress())); |
||||
node.verify(perm.expectAccountsWhitelist(senderA.getAddress(), senderB.getAddress())); |
||||
node.verify( |
||||
perm.expectPermissioningWhitelistFileKeyValue( |
||||
WHITELIST_TYPE.ACCOUNTS, |
||||
Lists.list(senderA.getAddress(), senderB.getAddress()), |
||||
tempFile)); |
||||
|
||||
node.execute(transactions.removeAccountsFromWhitelist(senderB.getAddress())); |
||||
node.verify(perm.expectAccountsWhitelist(senderA.getAddress())); |
||||
node.verify( |
||||
perm.expectPermissioningWhitelistFileKeyValue( |
||||
WHITELIST_TYPE.ACCOUNTS, Collections.singleton(senderA.getAddress()), tempFile)); |
||||
|
||||
node.execute(transactions.removeAccountsFromWhitelist(senderA.getAddress())); |
||||
node.verify(perm.expectAccountsWhitelist()); |
||||
node.verify( |
||||
perm.expectPermissioningWhitelistFileKeyValue( |
||||
WHITELIST_TYPE.ACCOUNTS, Collections.emptyList(), tempFile)); |
||||
} |
||||
|
||||
@Test |
||||
public void manipulatedNodesWhitelistIsPersisted() { |
||||
node.verify(perm.addNodesToWhitelist(Lists.newArrayList(enode1, enode2))); |
||||
node.verify( |
||||
perm.expectPermissioningWhitelistFileKeyValue( |
||||
WHITELIST_TYPE.NODES, Lists.newArrayList(enode1, enode2), tempFile)); |
||||
|
||||
node.verify(perm.removeNodesFromWhitelist(Lists.newArrayList(enode1))); |
||||
node.verify( |
||||
perm.expectPermissioningWhitelistFileKeyValue( |
||||
WHITELIST_TYPE.NODES, Collections.singleton(enode2), tempFile)); |
||||
|
||||
node.verify(perm.addNodesToWhitelist(Lists.newArrayList(enode1, enode3))); |
||||
node.verify( |
||||
perm.expectPermissioningWhitelistFileKeyValue( |
||||
WHITELIST_TYPE.NODES, Lists.newArrayList(enode2, enode1, enode3), tempFile)); |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* Licensed 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 tech.pegasys.pantheon.ethereum.permissioning; |
||||
|
||||
public class WhitelistFileSyncException extends Exception {} |
@ -0,0 +1,152 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* Licensed 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 tech.pegasys.pantheon.ethereum.permissioning; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.nio.file.StandardOpenOption; |
||||
import java.util.AbstractMap; |
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import com.google.common.annotations.VisibleForTesting; |
||||
import com.google.common.base.Charsets; |
||||
import net.consensys.cava.toml.Toml; |
||||
import net.consensys.cava.toml.TomlParseResult; |
||||
|
||||
public class WhitelistPersistor { |
||||
|
||||
private File configurationFile; |
||||
|
||||
public enum WHITELIST_TYPE { |
||||
ACCOUNTS("accounts-whitelist"), |
||||
NODES("nodes-whitelist"); |
||||
|
||||
private String tomlKey; |
||||
|
||||
WHITELIST_TYPE(final String tomlKey) { |
||||
this.tomlKey = tomlKey; |
||||
} |
||||
|
||||
public String getTomlKey() { |
||||
return tomlKey; |
||||
} |
||||
} |
||||
|
||||
public WhitelistPersistor(final String configurationFile) { |
||||
this.configurationFile = new File(configurationFile); |
||||
} |
||||
|
||||
public static boolean verifyConfigFileMatchesState( |
||||
final WHITELIST_TYPE whitelistType, |
||||
final Collection<String> checkLists, |
||||
final Path configurationFilePath) |
||||
throws IOException, WhitelistFileSyncException { |
||||
boolean listsMatch = |
||||
new HashSet<>(existingConfigItems(configurationFilePath).get(whitelistType)) |
||||
.equals(new HashSet<>(checkLists)); |
||||
if (!listsMatch) { |
||||
throw new WhitelistFileSyncException(); |
||||
} |
||||
return listsMatch; |
||||
} |
||||
|
||||
public boolean verifyConfigFileMatchesState( |
||||
final WHITELIST_TYPE whitelistType, final Collection<String> checkLists) |
||||
throws IOException, WhitelistFileSyncException { |
||||
return verifyConfigFileMatchesState(whitelistType, checkLists, configurationFile.toPath()); |
||||
} |
||||
|
||||
public synchronized void updateConfig( |
||||
final WHITELIST_TYPE whitelistType, final Collection<String> updatedWhitelistValues) |
||||
throws IOException { |
||||
removeExistingConfigItem(whitelistType); |
||||
addNewConfigItem(whitelistType, updatedWhitelistValues); |
||||
} |
||||
|
||||
private static Map<WHITELIST_TYPE, Collection<String>> existingConfigItems( |
||||
final Path configurationFilePath) throws IOException { |
||||
TomlParseResult parsedToml = Toml.parse(configurationFilePath); |
||||
|
||||
return Arrays.stream(WHITELIST_TYPE.values()) |
||||
.map( |
||||
whitelist_type -> |
||||
new AbstractMap.SimpleImmutableEntry<>( |
||||
whitelist_type, parsedToml.getArrayOrEmpty(whitelist_type.getTomlKey()))) |
||||
.collect( |
||||
Collectors.toMap( |
||||
o -> o.getKey(), |
||||
o -> |
||||
o.getValue() |
||||
.toList() |
||||
.parallelStream() |
||||
.map(Object::toString) |
||||
.collect(Collectors.toList()))); |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
void removeExistingConfigItem(final WHITELIST_TYPE whitelistType) throws IOException { |
||||
List<String> otherConfigItems = |
||||
existingConfigItems(configurationFile.toPath()) |
||||
.entrySet() |
||||
.parallelStream() |
||||
.filter(listType -> !listType.getKey().equals(whitelistType)) |
||||
.map(keyVal -> valueListToTomlArray(keyVal.getKey(), keyVal.getValue())) |
||||
.collect(Collectors.toList()); |
||||
|
||||
Files.write( |
||||
configurationFile.toPath(), |
||||
otherConfigItems, |
||||
StandardOpenOption.WRITE, |
||||
StandardOpenOption.TRUNCATE_EXISTING); |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
public static void addNewConfigItem( |
||||
final WHITELIST_TYPE whitelistType, |
||||
final Collection<String> whitelistValues, |
||||
final Path configFilePath) |
||||
throws IOException { |
||||
String newConfigItem = valueListToTomlArray(whitelistType, whitelistValues); |
||||
|
||||
Files.write( |
||||
configFilePath, |
||||
newConfigItem.getBytes(Charsets.UTF_8), |
||||
StandardOpenOption.WRITE, |
||||
StandardOpenOption.APPEND); |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
void addNewConfigItem( |
||||
final WHITELIST_TYPE whitelistType, final Collection<String> whitelistValues) |
||||
throws IOException { |
||||
addNewConfigItem(whitelistType, whitelistValues, configurationFile.toPath()); |
||||
} |
||||
|
||||
private static String valueListToTomlArray( |
||||
final WHITELIST_TYPE whitelistType, final Collection<String> whitelistValues) { |
||||
return String.format( |
||||
"%s=[%s]", |
||||
whitelistType.getTomlKey(), |
||||
whitelistValues |
||||
.parallelStream() |
||||
.map(uri -> String.format("\"%s\"", uri)) |
||||
.collect(Collectors.joining(","))); |
||||
} |
||||
} |
@ -0,0 +1,140 @@ |
||||
/* |
||||
* Copyright 2019 ConsenSys AG. |
||||
* |
||||
* Licensed 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 tech.pegasys.pantheon.ethereum.permissioning; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.permissioning.WhitelistPersistor.WHITELIST_TYPE; |
||||
|
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.nio.file.Files; |
||||
import java.nio.file.StandardOpenOption; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
import java.util.stream.Stream; |
||||
|
||||
import com.google.common.collect.Lists; |
||||
import org.junit.After; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
public class WhitelistPersistorTest { |
||||
|
||||
private WhitelistPersistor whitelistPersistor; |
||||
private File tempFile; |
||||
private final String accountsWhitelist = |
||||
String.format("%s=[%s]", WHITELIST_TYPE.ACCOUNTS.getTomlKey(), "\"account1\",\"account2\""); |
||||
private final String nodesWhitelist = |
||||
String.format("%s=[%s]", WHITELIST_TYPE.NODES.getTomlKey(), "\"node1\",\"node2\""); |
||||
|
||||
@Before |
||||
public void setUp() throws IOException { |
||||
List<String> lines = Lists.newArrayList(nodesWhitelist, accountsWhitelist); |
||||
tempFile = File.createTempFile("test", "test"); |
||||
Files.write(tempFile.toPath(), lines, StandardOpenOption.WRITE, StandardOpenOption.CREATE); |
||||
whitelistPersistor = new WhitelistPersistor(tempFile.getAbsolutePath()); |
||||
} |
||||
|
||||
@Test |
||||
public void lineShouldBeRemoved() throws IOException { |
||||
WHITELIST_TYPE keyForRemoval = WHITELIST_TYPE.ACCOUNTS; |
||||
|
||||
assertThat(countLines()).isEqualTo(2); |
||||
assertThat(hasKey(keyForRemoval)).isTrue(); |
||||
|
||||
whitelistPersistor.removeExistingConfigItem(keyForRemoval); |
||||
|
||||
assertThat(countLines()).isEqualTo(1); |
||||
assertThat(hasKey(keyForRemoval)).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void lineShouldBeAdded() throws IOException { |
||||
final WHITELIST_TYPE key = WHITELIST_TYPE.NODES; |
||||
final Set<String> updatedWhitelist = Collections.singleton("node5"); |
||||
|
||||
assertThat(countLines()).isEqualTo(2); |
||||
assertThat(hasKey(key)).isTrue(); |
||||
|
||||
whitelistPersistor.removeExistingConfigItem(WHITELIST_TYPE.NODES); |
||||
|
||||
assertThat(countLines()).isEqualTo(1); |
||||
assertThat(hasKey(key)).isFalse(); |
||||
|
||||
whitelistPersistor.addNewConfigItem(key, updatedWhitelist); |
||||
|
||||
assertThat(countLines()).isEqualTo(2); |
||||
assertThat(hasKey(key)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void lineShouldBeReplaced() throws IOException { |
||||
WHITELIST_TYPE key = WHITELIST_TYPE.NODES; |
||||
String newValue = "node5"; |
||||
|
||||
assertThat(countLines()).isEqualTo(2); |
||||
assertThat(hasKeyAndExactLineContent(key, nodesWhitelist)).isTrue(); |
||||
|
||||
whitelistPersistor.updateConfig(key, Collections.singleton(newValue)); |
||||
|
||||
assertThat(countLines()).isEqualTo(2); |
||||
assertThat(hasKeyAndContainsValue(key, newValue)).isTrue(); |
||||
assertThat(hasKeyAndExactLineContent(key, nodesWhitelist)).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void outputIsValidTOML() throws IOException { |
||||
WHITELIST_TYPE key = WHITELIST_TYPE.ACCOUNTS; |
||||
List<String> newValue = Lists.newArrayList("account5", "account6", "account4"); |
||||
String expectedValue = |
||||
String.format("%s=[%s]", "accounts-whitelist", "\"account5\",\"account6\",\"account4\""); |
||||
|
||||
whitelistPersistor.updateConfig(key, newValue); |
||||
|
||||
assertThat(hasKey(key)).isTrue(); |
||||
assertThat(hasKeyAndExactLineContent(key, expectedValue)).isTrue(); |
||||
} |
||||
|
||||
@After |
||||
public void tearDown() { |
||||
tempFile.delete(); |
||||
} |
||||
|
||||
private long countLines() throws IOException { |
||||
try (Stream<String> lines = Files.lines(tempFile.toPath())) { |
||||
return lines.count(); |
||||
} |
||||
} |
||||
|
||||
private boolean hasKey(final WHITELIST_TYPE key) throws IOException { |
||||
try (Stream<String> lines = Files.lines(tempFile.toPath())) { |
||||
return lines.anyMatch(s -> s.startsWith(key.getTomlKey())); |
||||
} |
||||
} |
||||
|
||||
private boolean hasKeyAndContainsValue(final WHITELIST_TYPE key, final String value) |
||||
throws IOException { |
||||
try (Stream<String> lines = Files.lines(tempFile.toPath())) { |
||||
return lines.anyMatch(s -> s.startsWith(key.getTomlKey()) && s.contains(value)); |
||||
} |
||||
} |
||||
|
||||
private boolean hasKeyAndExactLineContent(final WHITELIST_TYPE key, final String exactKeyValue) |
||||
throws IOException { |
||||
try (Stream<String> lines = Files.lines(tempFile.toPath())) { |
||||
return lines.anyMatch(s -> s.startsWith(key.getTomlKey()) && s.equals(exactKeyValue)); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
# Permissioning TOML file (node whitelist only) |
||||
|
||||
nodes-whitelist=[ |
||||
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.1:4567", |
||||
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.2:4567", |
||||
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.3:4567", |
||||
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.4:4567", |
||||
"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.5:4567", |
||||
] |
Loading…
Reference in new issue