diff --git a/services-security-parent/services-security-vault/pom.xml b/services-security-parent/services-security-vault/pom.xml index 0f3a313d8..5e1caf63d 100644 --- a/services-security-parent/services-security-vault/pom.xml +++ b/services-security-parent/services-security-vault/pom.xml @@ -18,6 +18,11 @@ scalecube-services ${project.version} + + io.scalecube + scalecube-services-security + ${project.version} + io.scalecube diff --git a/services-security-parent/services-security-vault/src/main/java/io/scalecube/services/security/vault/VaultServiceRolesInstaller.java b/services-security-parent/services-security-vault/src/main/java/io/scalecube/services/security/vault/VaultServiceRolesInstaller.java new file mode 100644 index 000000000..a1c12b5ec --- /dev/null +++ b/services-security-parent/services-security-vault/src/main/java/io/scalecube/services/security/vault/VaultServiceRolesInstaller.java @@ -0,0 +1,307 @@ +package io.scalecube.services.security.vault; + +import com.bettercloud.vault.json.Json; +import com.bettercloud.vault.rest.Rest; +import com.bettercloud.vault.rest.RestException; +import io.scalecube.services.security.vault.VaultServiceRolesInstaller.ServiceRoles.Role; +import java.io.InputStream; +import java.util.Base64; +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 org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import reactor.core.Exceptions; + +public final class VaultServiceRolesInstaller { + + private static final Logger LOGGER = LoggerFactory.getLogger(VaultServiceRolesInstaller.class); + + private static final String VAULT_TOKEN_HEADER = "X-Vault-Token"; + + private String vaultAddress; + private Supplier vaultTokenSupplier; + private Supplier keyNameSupplier; + private Function roleNameBuilder; + private String inputFileName = "service-roles.yaml"; + 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.inputFileName = other.inputFileName; + this.keyAlgorithm = other.keyAlgorithm; + this.keyRotationPeriod = other.keyRotationPeriod; + this.keyVerificationTtl = other.keyVerificationTtl; + this.roleTtl = other.roleTtl; + } + + /** + * 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(Supplier 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 inputFileName. + * + * @param inputFileName inputFileName + * @return new instance with applied setting + */ + public VaultServiceRolesInstaller inputFileName(String inputFileName) { + final VaultServiceRolesInstaller c = copy(); + c.inputFileName = inputFileName; + 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 serviceRolesFileName (access-file.yaml)} and builds micro-infrastructure for + * machine-to-machine authentication in the vault. + */ + public void install() { + if (isNullOrNoneOrEmpty(vaultAddress)) { + return; + } + + final ServiceRoles serviceRoles = loadServiceRoles(); + if (serviceRoles == null) { + return; + } + + final Rest rest = new Rest().header(VAULT_TOKEN_HEADER, vaultTokenSupplier.get()); + + if (!serviceRoles.roles.isEmpty()) { + String keyName = keyNameSupplier.get(); + createVaultIdentityKey(keyName, () -> rest.url(buildVaultIdentityKeyUri(keyName))); + + for (Role role : serviceRoles.roles) { + String roleName = roleNameBuilder.apply(role.role); + createVaultIdentityRole( + keyName, + roleName, + role.permissions, + () -> rest.url(buildVaultIdentityRoleUri(roleName))); + } + } + } + + private ServiceRoles loadServiceRoles() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + InputStream inputStream = classLoader.getResourceAsStream(inputFileName); + return inputStream != null + ? new Yaml(new Constructor(ServiceRoles.class)).load(inputStream) + : 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(String keyName, Supplier restSupplier) { + 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(restSupplier.get().body(body).post().getStatus(), "createVaultIdentityKey"); + } catch (RestException e) { + throw Exceptions.propagate(e); + } + } + + private void createVaultIdentityRole( + String keyName, String roleName, List permissions, Supplier restSupplier) { + LOGGER.debug("[createVaultIdentityRole] {}", roleName); + + byte[] body = + Json.object() + .add("key", keyName) + .add("template", createTemplate(permissions)) + .add("ttl", roleTtl) + .toString() + .getBytes(); + + try { + verifyOk(restSupplier.get().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 VaultServiceRolesInstaller copy() { + return new VaultServiceRolesInstaller(this); + } + + 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; + } + + 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; + } + } + } +} diff --git a/services-security-parent/services-security-vault/src/main/java/io/scalecube/services/security/vault/VaultServiceTokenCredentialsSupplier.java b/services-security-parent/services-security-vault/src/main/java/io/scalecube/services/security/vault/VaultServiceTokenCredentialsSupplier.java new file mode 100644 index 000000000..1121be769 --- /dev/null +++ b/services-security-parent/services-security-vault/src/main/java/io/scalecube/services/security/vault/VaultServiceTokenCredentialsSupplier.java @@ -0,0 +1,147 @@ +package io.scalecube.services.security.vault; + +import com.bettercloud.vault.json.Json; +import com.bettercloud.vault.rest.Rest; +import com.bettercloud.vault.rest.RestException; +import com.bettercloud.vault.rest.RestResponse; +import io.scalecube.services.ServiceReference; +import io.scalecube.services.auth.CredentialsSupplier; +import io.scalecube.services.security.ServiceTokens; +import io.scalecube.utils.MaskUtil; +import java.util.Collections; +import java.util.Map; +import java.util.StringJoiner; +import java.util.function.BiFunction; +import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Exceptions; +import reactor.core.publisher.Mono; + +public final class VaultServiceTokenCredentialsSupplier implements CredentialsSupplier { + + private static final Logger LOGGER = + LoggerFactory.getLogger(VaultServiceTokenCredentialsSupplier.class); + + private static final String VAULT_TOKEN_HEADER = "X-Vault-Token"; + + private String serviceRole; + private String vaultAddress; + private Supplier vaultTokenSupplier; + private BiFunction, String> serviceTokenNameBuilder; + + public VaultServiceTokenCredentialsSupplier() {} + + private VaultServiceTokenCredentialsSupplier(VaultServiceTokenCredentialsSupplier other) { + this.serviceRole = other.serviceRole; + this.vaultAddress = other.vaultAddress; + this.vaultTokenSupplier = other.vaultTokenSupplier; + this.serviceTokenNameBuilder = other.serviceTokenNameBuilder; + } + + /** + * Setter for serviceRole. + * + * @param serviceRole serviceRole + * @return new instance with applied setting + */ + public VaultServiceTokenCredentialsSupplier serviceRole(String serviceRole) { + final VaultServiceTokenCredentialsSupplier c = copy(); + c.serviceRole = serviceRole; + return c; + } + + /** + * Setter for vaultAddress. + * + * @param vaultAddress vaultAddress + * @return new instance with applied setting + */ + public VaultServiceTokenCredentialsSupplier vaultAddress(String vaultAddress) { + final VaultServiceTokenCredentialsSupplier c = copy(); + c.vaultAddress = vaultAddress; + return c; + } + + /** + * Setter for vaultTokenSupplier. + * + * @param vaultTokenSupplier vaultTokenSupplier + * @return new instance with applied setting + */ + public VaultServiceTokenCredentialsSupplier vaultTokenSupplier( + Supplier vaultTokenSupplier) { + final VaultServiceTokenCredentialsSupplier c = copy(); + c.vaultTokenSupplier = vaultTokenSupplier; + return c; + } + + /** + * Setter for serviceTokenNameBuilder. + * + * @param serviceTokenNameBuilder serviceTokenNameBuilder + * @return new instance with applied setting + */ + public VaultServiceTokenCredentialsSupplier serviceTokenNameBuilder( + BiFunction, String> serviceTokenNameBuilder) { + final VaultServiceTokenCredentialsSupplier c = copy(); + c.serviceTokenNameBuilder = serviceTokenNameBuilder; + return c; + } + + @Override + public Mono> apply(ServiceReference serviceReference) { + return Mono.fromCallable(vaultTokenSupplier::get) + .map(vaultToken -> rpcGetServiceToken(serviceReference.tags(), vaultToken)) + .doOnNext(response -> verifyOk(response.getStatus())) + .map(this::toCredentials) + .doOnSuccess( + creds -> + LOGGER.info( + "[rpcGetServiceToken] Successfully obtained vault service token: {}", + MaskUtil.mask(creds))); + } + + private Map toCredentials(RestResponse response) { + return Collections.singletonMap( + ServiceTokens.SERVICE_TOKEN_HEADER, + Json.parse(new String(response.getBody())) + .asObject() + .get("data") + .asObject() + .get("token") + .asString()); + } + + private RestResponse rpcGetServiceToken(Map tags, String vaultToken) { + String uri = buildVaultServiceTokenUri(tags); + LOGGER.info("[rpcGetServiceToken] Getting vault service token (uri='{}')", uri); + try { + return new Rest().header(VAULT_TOKEN_HEADER, vaultToken).url(uri).get(); + } catch (RestException e) { + LOGGER.error( + "[rpcGetServiceToken] Failed to get vault service token (uri='{}'), cause: {}", + uri, + e.toString()); + throw Exceptions.propagate(e); + } + } + + private static void verifyOk(int status) { + if (status != 200) { + LOGGER.error("[rpcGetServiceToken] Not expected status ({}) returned", status); + throw new IllegalStateException("Not expected status returned, status=" + status); + } + } + + private String buildVaultServiceTokenUri(Map tags) { + return new StringJoiner("/", vaultAddress, "") + .add("v1/identity/oidc/token") + .add(serviceTokenNameBuilder.apply(serviceRole, tags)) + .toString(); + } + + private VaultServiceTokenCredentialsSupplier copy() { + return new VaultServiceTokenCredentialsSupplier(this); + } +}