All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.scalecube.security.vault.VaultServiceRolesInstaller Maven / Gradle / Ivy

package io.scalecube.security.vault;

import com.bettercloud.vault.json.Json;
import com.bettercloud.vault.rest.Rest;
import com.bettercloud.vault.rest.RestException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.scalecube.security.vault.VaultServiceRolesInstaller.ServiceRoles.Role;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

public final class VaultServiceRolesInstaller {

  private static final Logger LOGGER = LoggerFactory.getLogger(VaultServiceRolesInstaller.class);

  private static final String VAULT_TOKEN_HEADER = "X-Vault-Token";

  private static final List> DEFAULT_SERVICE_ROLES_SOURCES =
      Collections.singletonList(new ResourcesServiceRolesSupplier());

  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory());

  private String vaultAddress;
  private Mono vaultTokenSupplier;
  private Supplier keyNameSupplier;
  private Function roleNameBuilder;
  private List> serviceRolesSources = DEFAULT_SERVICE_ROLES_SOURCES;
  private String keyAlgorithm = "RS256";
  private String keyRotationPeriod = "1h";
  private String keyVerificationTtl = "1h";
  private String roleTtl = "1m";

  public VaultServiceRolesInstaller() {}

  private VaultServiceRolesInstaller(VaultServiceRolesInstaller other) {
    this.vaultAddress = other.vaultAddress;
    this.vaultTokenSupplier = other.vaultTokenSupplier;
    this.keyNameSupplier = other.keyNameSupplier;
    this.roleNameBuilder = other.roleNameBuilder;
    this.serviceRolesSources = other.serviceRolesSources;
    this.keyAlgorithm = other.keyAlgorithm;
    this.keyRotationPeriod = other.keyRotationPeriod;
    this.keyVerificationTtl = other.keyVerificationTtl;
    this.roleTtl = other.roleTtl;
  }

  private VaultServiceRolesInstaller copy() {
    return new VaultServiceRolesInstaller(this);
  }

  /**
   * Setter for vaultAddress.
   *
   * @param vaultAddress vaultAddress
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller vaultAddress(String vaultAddress) {
    final VaultServiceRolesInstaller c = copy();
    c.vaultAddress = vaultAddress;
    return c;
  }

  /**
   * Setter for vaultTokenSupplier.
   *
   * @param vaultTokenSupplier vaultTokenSupplier
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller vaultTokenSupplier(Mono vaultTokenSupplier) {
    final VaultServiceRolesInstaller c = copy();
    c.vaultTokenSupplier = vaultTokenSupplier;
    return c;
  }

  /**
   * Setter for keyNameSupplier.
   *
   * @param keyNameSupplier keyNameSupplier
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller keyNameSupplier(Supplier keyNameSupplier) {
    final VaultServiceRolesInstaller c = copy();
    c.keyNameSupplier = keyNameSupplier;
    return c;
  }

  /**
   * Setter for roleNameBuilder.
   *
   * @param roleNameBuilder roleNameBuilder
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller roleNameBuilder(Function roleNameBuilder) {
    final VaultServiceRolesInstaller c = copy();
    c.roleNameBuilder = roleNameBuilder;
    return c;
  }

  /**
   * Setter for serviceRolesSources.
   *
   * @param serviceRolesSources serviceRolesSources
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller serviceRolesSources(
      List> serviceRolesSources) {
    final VaultServiceRolesInstaller c = copy();
    c.serviceRolesSources = serviceRolesSources;
    return c;
  }

  /**
   * Setter for serviceRolesSources.
   *
   * @param serviceRolesSources serviceRolesSources
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller serviceRolesSources(
      Supplier... serviceRolesSources) {
    final VaultServiceRolesInstaller c = copy();
    c.serviceRolesSources = Arrays.asList(serviceRolesSources);
    return c;
  }

  /**
   * Setter for keyAlgorithm.
   *
   * @param keyAlgorithm keyAlgorithm
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller keyAlgorithm(String keyAlgorithm) {
    final VaultServiceRolesInstaller c = copy();
    c.keyAlgorithm = keyAlgorithm;
    return c;
  }

  /**
   * Setter for keyRotationPeriod.
   *
   * @param keyRotationPeriod keyRotationPeriod
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller keyRotationPeriod(String keyRotationPeriod) {
    final VaultServiceRolesInstaller c = copy();
    c.keyRotationPeriod = keyRotationPeriod;
    return c;
  }

  /**
   * Setter for keyVerificationTtl.
   *
   * @param keyVerificationTtl keyVerificationTtl
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller keyVerificationTtl(String keyVerificationTtl) {
    final VaultServiceRolesInstaller c = copy();
    c.keyVerificationTtl = keyVerificationTtl;
    return c;
  }

  /**
   * Setter for roleTtl.
   *
   * @param roleTtl roleTtl
   * @return new instance with applied setting
   */
  public VaultServiceRolesInstaller roleTtl(String roleTtl) {
    final VaultServiceRolesInstaller c = copy();
    c.roleTtl = roleTtl;
    return c;
  }

  /**
   * Reads {@code inputFileName} and builds vault oidc micro-infrastructure (identity roles and
   * keys) to use it for machine-to-machine authentication.
   */
  public Mono install() {
    return Mono.defer(this::install0)
        .subscribeOn(Schedulers.boundedElastic())
        .doOnError(th -> LOGGER.error("Failed to install serviceRoles, cause: {}", th.toString()));
  }

  private Mono install0() {
    if (isNullOrNoneOrEmpty(vaultAddress)) {
      LOGGER.debug("Skipping serviceRoles installation, vaultAddress not set");
      return Mono.empty();
    }

    final ServiceRoles serviceRoles = loadServiceRoles();
    if (serviceRoles == null || serviceRoles.roles.isEmpty()) {
      LOGGER.debug("Skipping serviceRoles installation, serviceRoles not set");
      return Mono.empty();
    }

    return Mono.defer(() -> vaultTokenSupplier)
        .doOnSuccess(
            token -> {
              final Rest rest = new Rest().header(VAULT_TOKEN_HEADER, token);

              final String keyName = keyNameSupplier.get();
              createVaultIdentityKey(rest.url(buildVaultIdentityKeyUri(keyName)), keyName);

              for (Role role : serviceRoles.roles) {
                String roleName = roleNameBuilder.apply(role.role);
                createVaultIdentityRole(
                    rest.url(buildVaultIdentityRoleUri(roleName)),
                    keyName,
                    roleName,
                    role.permissions);
              }
            })
        .doOnSuccess(s -> LOGGER.debug("Installed serviceRoles ({})", serviceRoles))
        .then();
  }

  private ServiceRoles loadServiceRoles() {
    if (serviceRolesSources == null) {
      return null;
    }

    for (Supplier serviceRolesSource : serviceRolesSources) {
      try {
        final ServiceRoles serviceRoles = serviceRolesSource.get();
        if (serviceRoles != null) {
          return serviceRoles;
        }
      } catch (Throwable th) {
        LOGGER.warn(
            "Failed to load serviceRoles from {}, cause {}", serviceRolesSource, th.getMessage());
      }
    }

    return null;
  }

  private static void verifyOk(int status, String operation) {
    if (status != 200 && status != 204) {
      LOGGER.error("Not expected status ({}) returned on [{}]", status, operation);
      throw new IllegalStateException("Not expected status returned, status=" + status);
    }
  }

  private void createVaultIdentityKey(Rest rest, String keyName) {
    LOGGER.debug("[createVaultIdentityKey] {}", keyName);

    byte[] body =
        Json.object()
            .add("rotation_period", keyRotationPeriod)
            .add("verification_ttl", keyVerificationTtl)
            .add("allowed_client_ids", "*")
            .add("algorithm", keyAlgorithm)
            .toString()
            .getBytes();

    try {
      verifyOk(rest.body(body).post().getStatus(), "createVaultIdentityKey");
    } catch (RestException e) {
      throw Exceptions.propagate(e);
    }
  }

  private void createVaultIdentityRole(
      Rest rest, String keyName, String roleName, List permissions) {
    LOGGER.debug("[createVaultIdentityRole] {}", roleName);

    byte[] body =
        Json.object()
            .add("key", keyName)
            .add("template", createTemplate(permissions))
            .add("ttl", roleTtl)
            .toString()
            .getBytes();

    try {
      verifyOk(rest.body(body).post().getStatus(), "createVaultIdentityRole");
    } catch (RestException e) {
      throw Exceptions.propagate(e);
    }
  }

  private static String createTemplate(List permissions) {
    return Base64.getUrlEncoder()
        .encodeToString(
            Json.object().add("permissions", String.join(",", permissions)).toString().getBytes());
  }

  private String buildVaultIdentityKeyUri(String keyName) {
    return new StringJoiner("/", vaultAddress, "")
        .add("/v1/identity/oidc/key")
        .add(keyName)
        .toString();
  }

  private String buildVaultIdentityRoleUri(String roleName) {
    return new StringJoiner("/", vaultAddress, "")
        .add("/v1/identity/oidc/role")
        .add(roleName)
        .toString();
  }

  private static boolean isNullOrNoneOrEmpty(String value) {
    return Objects.isNull(value)
        || "none".equalsIgnoreCase(value)
        || "null".equals(value)
        || value.isEmpty();
  }

  public static class ServiceRoles {

    private List roles;

    public List getRoles() {
      return roles;
    }

    public void setRoles(List roles) {
      this.roles = roles;
    }

    @Override
    public String toString() {
      return new StringJoiner(", ", ServiceRoles.class.getSimpleName() + "[", "]")
          .add("roles=" + roles)
          .toString();
    }

    public static class Role {

      private String role;
      private List permissions;

      public String getRole() {
        return role;
      }

      public void setRole(String role) {
        this.role = role;
      }

      public List getPermissions() {
        return permissions;
      }

      public void setPermissions(List permissions) {
        this.permissions = permissions;
      }

      @Override
      public String toString() {
        return new StringJoiner(", ", Role.class.getSimpleName() + "[", "]")
            .add("role='" + role + "'")
            .add("permissions=" + permissions)
            .toString();
      }
    }
  }

  public static class ResourcesServiceRolesSupplier implements Supplier {

    public static final String DEFAULT_FILE_NAME = "service-roles.yaml";

    private final String fileName;

    public ResourcesServiceRolesSupplier() {
      this(DEFAULT_FILE_NAME);
    }

    public ResourcesServiceRolesSupplier(String fileName) {
      this.fileName = Objects.requireNonNull(fileName, "fileName");
    }

    @Override
    public ServiceRoles get() {
      try {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream(fileName);
        return inputStream != null
            ? OBJECT_MAPPER.readValue(inputStream, ServiceRoles.class)
            : null;
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    public String toString() {
      return new StringJoiner(", ", ResourcesServiceRolesSupplier.class.getSimpleName() + "[", "]")
          .add("fileName='" + fileName + "'")
          .toString();
    }
  }

  public static class EnvironmentServiceRolesSupplier implements Supplier {

    public static final String DEFAULT_ENV_KEY = "SERVICE_ROLES";

    private final String envKey;

    public EnvironmentServiceRolesSupplier() {
      this(DEFAULT_ENV_KEY);
    }

    public EnvironmentServiceRolesSupplier(String envKey) {
      this.envKey = Objects.requireNonNull(envKey, "envKey");
    }

    @Override
    public ServiceRoles get() {
      try {
        final String value = System.getenv(envKey);
        return value != null
            ? OBJECT_MAPPER.readValue(new StringReader(value), ServiceRoles.class)
            : null;
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    @Override
    public String toString() {
      return new StringJoiner(
              ", ", EnvironmentServiceRolesSupplier.class.getSimpleName() + "[", "]")
          .add("envKey='" + envKey + "'")
          .toString();
    }
  }

  public static class FileServiceRolesSupplier implements Supplier {

    public static final String DEFAULT_FILE = "service_roles.yaml";

    private final String file;

    public FileServiceRolesSupplier() {
      this(DEFAULT_FILE);
    }

    public FileServiceRolesSupplier(String file) {
      this.file = Objects.requireNonNull(file, "file");
    }

    @Override
    public ServiceRoles get() {
      try {
        final File file = new File(this.file);
        return file.exists()
            ? OBJECT_MAPPER.readValue(new FileInputStream(file), ServiceRoles.class)
            : null;
      } catch (Exception e) {
        throw Exceptions.propagate(e);
      }
    }

    @Override
    public String toString() {
      return new StringJoiner(", ", FileServiceRolesSupplier.class.getSimpleName() + "[", "]")
          .add("file='" + file + "'")
          .toString();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy