AllowlistPersistor.java

/*
 * Copyright 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.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.Collections;
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 org.apache.tuweni.toml.Toml;
import org.apache.tuweni.toml.TomlParseResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AllowlistPersistor {
  private static final Logger LOG = LoggerFactory.getLogger(AllowlistPersistor.class);

  private final File configurationFile;

  public enum ALLOWLIST_TYPE {
    ACCOUNTS("accounts-allowlist"),
    NODES("nodes-allowlist");

    private final String tomlKey;

    ALLOWLIST_TYPE(final String tomlKey) {
      this.tomlKey = tomlKey;
    }

    public String getTomlKey() {
      return tomlKey;
    }
  }

  public AllowlistPersistor(final String configurationFile) {
    this.configurationFile = new File(configurationFile);
  }

  public static boolean verifyConfigFileMatchesState(
      final ALLOWLIST_TYPE allowlistType,
      final Collection<String> checkLists,
      final Path configurationFilePath)
      throws IOException, AllowlistFileSyncException {

    final Map<ALLOWLIST_TYPE, Collection<String>> configItems =
        existingConfigItems(configurationFilePath);
    final Collection<String> existingValues =
        configItems.get(allowlistType) != null
            ? configItems.get(allowlistType)
            : Collections.emptyList();

    if (!existingValues.containsAll(checkLists)) {
      LOG.atDebug()
          .setMessage("\n LISTS DO NOT MATCH configFile::")
          .addArgument(existingValues)
          .addArgument(configurationFilePath)
          .log();
      LOG.atDebug().setMessage("\nLISTS DO NOT MATCH in-memory ::").addArgument(checkLists).log();
      throw new AllowlistFileSyncException();
    }
    return true;
  }

  public boolean verifyConfigFileMatchesState(
      final ALLOWLIST_TYPE allowlistType, final Collection<String> checkLists)
      throws IOException, AllowlistFileSyncException {
    return verifyConfigFileMatchesState(allowlistType, checkLists, configurationFile.toPath());
  }

  public synchronized void updateConfig(
      final ALLOWLIST_TYPE allowlistType, final Collection<String> updatedAllowlistValues)
      throws IOException {
    removeExistingConfigItem(allowlistType);
    addNewConfigItem(allowlistType, updatedAllowlistValues);
  }

  private static Map<ALLOWLIST_TYPE, Collection<String>> existingConfigItems(
      final Path configurationFilePath) throws IOException {
    TomlParseResult parsedToml = Toml.parse(configurationFilePath);

    return Arrays.stream(ALLOWLIST_TYPE.values())
        .filter(k -> parsedToml.contains(k.getTomlKey()))
        .map(
            allowlist_type ->
                new AbstractMap.SimpleImmutableEntry<>(
                    allowlist_type, parsedToml.getArrayOrEmpty(allowlist_type.getTomlKey())))
        .collect(
            Collectors.toMap(
                o -> o.getKey(),
                o ->
                    o.getValue().toList().parallelStream()
                        .map(Object::toString)
                        .collect(Collectors.toList())));
  }

  @VisibleForTesting
  void removeExistingConfigItem(final ALLOWLIST_TYPE allowlistType) throws IOException {
    List<String> otherConfigItems =
        existingConfigItems(configurationFile.toPath()).entrySet().parallelStream()
            .filter(listType -> !listType.getKey().equals(allowlistType))
            .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 ALLOWLIST_TYPE allowlistType,
      final Collection<String> allowlistValues,
      final Path configFilePath)
      throws IOException {
    String newConfigItem = valueListToTomlArray(allowlistType, allowlistValues);

    Files.write(
        configFilePath,
        newConfigItem.getBytes(Charsets.UTF_8),
        StandardOpenOption.WRITE,
        StandardOpenOption.APPEND);
  }

  @VisibleForTesting
  void addNewConfigItem(
      final ALLOWLIST_TYPE allowlistType, final Collection<String> allowlistValues)
      throws IOException {
    addNewConfigItem(allowlistType, allowlistValues, configurationFile.toPath());
  }

  private static String valueListToTomlArray(
      final ALLOWLIST_TYPE allowlistType, final Collection<String> allowlistValues) {
    return String.format(
        "%s=[%s]",
        allowlistType.getTomlKey(),
        allowlistValues.parallelStream()
            .map(uri -> String.format("\"%s\"", uri))
            .collect(Collectors.joining(",")));
  }
}