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

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

The newest version!
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.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public 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()).setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

  private final String vaultAddress;
  private final Supplier> vaultTokenSupplier;
  private final Supplier keyNameSupplier;
  private final Function roleNameBuilder;
  private final List> serviceRolesSources;
  private final String keyAlgorithm;
  private final String keyRotationPeriod;
  private final String keyVerificationTtl;
  private final String roleTtl;
  private final long timeout;
  private final TimeUnit timeUnit;

  private VaultServiceRolesInstaller(Builder builder) {
    this.vaultAddress = Objects.requireNonNull(builder.vaultAddress, "vaultAddress");
    this.vaultTokenSupplier =
        Objects.requireNonNull(builder.vaultTokenSupplier, "vaultTokenSupplier");
    this.keyNameSupplier = Objects.requireNonNull(builder.keyNameSupplier, "keyNameSupplier");
    this.roleNameBuilder = Objects.requireNonNull(builder.roleNameBuilder, "roleNameBuilder");
    this.serviceRolesSources =
        Objects.requireNonNull(builder.serviceRolesSources, "serviceRolesSources");
    this.keyAlgorithm = Objects.requireNonNull(builder.keyAlgorithm, "keyAlgorithm");
    this.keyRotationPeriod = Objects.requireNonNull(builder.keyRotationPeriod, "keyRotationPeriod");
    this.keyVerificationTtl =
        Objects.requireNonNull(builder.keyVerificationTtl, "keyVerificationTtl");
    this.roleTtl = Objects.requireNonNull(builder.roleTtl, "roleTtl");
    this.timeout = builder.timeout;
    this.timeUnit = builder.timeUnit;
  }

  public static Builder builder() {
    return new Builder();
  }

  /**
   * Builds vault oidc micro-infrastructure (identity roles and keys) to use it for
   * machine-to-machine authentication.
   */
  public void install() {
    if (isNullOrNoneOrEmpty(vaultAddress)) {
      LOGGER.debug("Skipping service roles installation, vault address not set");
      return;
    }

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

    try {
      vaultTokenSupplier
          .get()
          .thenAcceptAsync(
              token -> {
                final var rest = new Rest().header(VAULT_TOKEN_HEADER, token);
                final var keyName = keyNameSupplier.get();

                createVaultIdentityKey(rest.url(vaultIdentityKeyUri(keyName)), keyName);
                LOGGER.debug("Vault identity key: {}", keyName);

                for (var role : serviceRoles.roles) {
                  final var roleName = roleNameBuilder.apply(role.role);
                  createVaultIdentityRole(
                      rest.url(vaultIdentityRoleUri(roleName)),
                      keyName,
                      role.role,
                      role.permissions);
                  LOGGER.debug("Vault identity role: {}", roleName);
                }

                LOGGER.debug("Installed service roles: {}", serviceRoles);
              })
          .get(timeout, timeUnit);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private ServiceRoles loadServiceRoles() {
    for (Supplier serviceRolesSource : serviceRolesSources) {
      final ServiceRoles serviceRoles = serviceRolesSource.get();
      if (serviceRoles != null) {
        return serviceRoles;
      }
    }

    return null;
  }

  private static void awaitSuccess(int status) {
    if (status != 200 && status != 204) {
      throw new IllegalStateException("Not expected status returned, status=" + status);
    }
  }

  private void createVaultIdentityKey(Rest rest, String keyName) {
    final byte[] body =
        Json.object()
            .add("rotation_period", keyRotationPeriod)
            .add("verification_ttl", keyVerificationTtl)
            .add("allowed_client_ids", "*")
            .add("algorithm", keyAlgorithm)
            .toString()
            .getBytes(StandardCharsets.UTF_8);

    try {
      awaitSuccess(rest.body(body).post().getStatus());
    } catch (RestException e) {
      throw new RuntimeException("Failed to create vault identity key: " + keyName, e);
    }
  }

  private void createVaultIdentityRole(
      Rest rest, String keyName, String roleName, List permissions) {
    final byte[] body =
        Json.object()
            .add("key", keyName)
            .add("template", createTemplate(roleName, permissions))
            .add("ttl", roleTtl)
            .toString()
            .getBytes(StandardCharsets.UTF_8);

    try {
      awaitSuccess(rest.body(body).post().getStatus());
    } catch (RestException e) {
      throw new RuntimeException("Failed to create vault identity role: " + roleName, e);
    }
  }

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

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

  private String vaultIdentityRoleUri(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 roles() {
      return roles;
    }

    public ServiceRoles roles(List roles) {
      this.roles = roles;
      return this;
    }

    @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 role() {
        return role;
      }

      public Role role(String role) {
        this.role = role;
        return this;
      }

      public List permissions() {
        return permissions;
      }

      public Role permissions(List permissions) {
        this.permissions = permissions;
        return this;
      }

      @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);
        if (!file.exists()) {
          return null;
        }
        try (final FileInputStream fis = new FileInputStream(file)) {
          return OBJECT_MAPPER.readValue(fis, ServiceRoles.class);
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

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

  public static class Builder {

    private String vaultAddress;
    private Supplier> 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";
    private long timeout = 10;
    private TimeUnit timeUnit = TimeUnit.SECONDS;

    private Builder() {}

    public Builder vaultAddress(String vaultAddress) {
      this.vaultAddress = vaultAddress;
      return this;
    }

    public Builder vaultTokenSupplier(Supplier> vaultTokenSupplier) {
      this.vaultTokenSupplier = vaultTokenSupplier;
      return this;
    }

    public Builder keyNameSupplier(Supplier keyNameSupplier) {
      this.keyNameSupplier = keyNameSupplier;
      return this;
    }

    public Builder roleNameBuilder(Function roleNameBuilder) {
      this.roleNameBuilder = roleNameBuilder;
      return this;
    }

    public Builder serviceRolesSources(List> serviceRolesSources) {
      this.serviceRolesSources = serviceRolesSources;
      return this;
    }

    public Builder keyAlgorithm(String keyAlgorithm) {
      this.keyAlgorithm = keyAlgorithm;
      return this;
    }

    public Builder keyRotationPeriod(String keyRotationPeriod) {
      this.keyRotationPeriod = keyRotationPeriod;
      return this;
    }

    public Builder keyVerificationTtl(String keyVerificationTtl) {
      this.keyVerificationTtl = keyVerificationTtl;
      return this;
    }

    public Builder roleTtl(String roleTtl) {
      this.roleTtl = roleTtl;
      return this;
    }

    public Builder timeout(long timeout, TimeUnit timeUnit) {
      this.timeout = timeout;
      this.timeUnit = timeUnit;
      return this;
    }

    public VaultServiceRolesInstaller build() {
      return new VaultServiceRolesInstaller(this);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy