diff --git a/pom.xml b/pom.xml index 0dfe833..1cc0417 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -35,13 +37,18 @@ jwt tokens + vault + 0.4.16 + 1.0.13 + 0.11.2 - Dysprosium-SR9 + 2020.0.5 2.11.0 1.7.30 + 1.26 3.1.0 1.3 @@ -54,6 +61,18 @@ + + + io.scalecube + scalecube-config-vault + ${scalecube-config.version} + + + + io.scalecube + scalecube-commons + ${scalecube-commons.version} + io.projectreactor @@ -68,6 +87,12 @@ slf4j-api ${slf4j.version} + + + org.yaml + snakeyaml + ${snakeyaml.version} + io.jsonwebtoken diff --git a/vault/pom.xml b/vault/pom.xml new file mode 100644 index 0000000..a16f1e3 --- /dev/null +++ b/vault/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + io.scalecube + scalecube-security-parent + 1.0.19-SNAPSHOT + + + scalecube-security-vault + + + + io.scalecube + scalecube-config-vault + + + io.scalecube + scalecube-commons + + + io.projectreactor + reactor-core + + + org.slf4j + slf4j-api + + + org.yaml + snakeyaml + + + + diff --git a/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java b/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java new file mode 100644 index 0000000..930355a --- /dev/null +++ b/vault/src/main/java/io/scalecube/security/vault/VaultServiceRolesInstaller.java @@ -0,0 +1,307 @@ +package io.scalecube.security.vault; + +import com.bettercloud.vault.json.Json; +import com.bettercloud.vault.rest.Rest; +import com.bettercloud.vault.rest.RestException; +import io.scalecube.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/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java b/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java new file mode 100644 index 0000000..5230235 --- /dev/null +++ b/vault/src/main/java/io/scalecube/security/vault/VaultServiceTokenSupplier.java @@ -0,0 +1,143 @@ +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.bettercloud.vault.rest.RestResponse; +import io.scalecube.utils.MaskUtil; +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 VaultServiceTokenSupplier { + + private static final Logger LOGGER = LoggerFactory.getLogger(VaultServiceTokenSupplier.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 VaultServiceTokenSupplier() {} + + private VaultServiceTokenSupplier(VaultServiceTokenSupplier 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 VaultServiceTokenSupplier serviceRole(String serviceRole) { + final VaultServiceTokenSupplier c = copy(); + c.serviceRole = serviceRole; + return c; + } + + /** + * Setter for vaultAddress. + * + * @param vaultAddress vaultAddress + * @return new instance with applied setting + */ + public VaultServiceTokenSupplier vaultAddress(String vaultAddress) { + final VaultServiceTokenSupplier c = copy(); + c.vaultAddress = vaultAddress; + return c; + } + + /** + * Setter for vaultTokenSupplier. + * + * @param vaultTokenSupplier vaultTokenSupplier + * @return new instance with applied setting + */ + public VaultServiceTokenSupplier vaultTokenSupplier(Supplier vaultTokenSupplier) { + final VaultServiceTokenSupplier c = copy(); + c.vaultTokenSupplier = vaultTokenSupplier; + return c; + } + + /** + * Setter for serviceTokenNameBuilder. + * + * @param serviceTokenNameBuilder serviceTokenNameBuilder; inputs for this function are {@code + * serviceRole} and {@code tags} attributes + * @return new instance with applied setting + */ + public VaultServiceTokenSupplier serviceTokenNameBuilder( + BiFunction, String> serviceTokenNameBuilder) { + final VaultServiceTokenSupplier c = copy(); + c.serviceTokenNameBuilder = serviceTokenNameBuilder; + return c; + } + + /** + * Returns credentials as {@code Map} for the given args. + * + * @param tags tags attributes + * @return vault service token + */ + public Mono getServiceToken(Map tags) { + return Mono.fromCallable(vaultTokenSupplier::get) + .map(vaultToken -> rpcGetServiceToken(tags, vaultToken)) + .doOnNext(response -> verifyOk(response.getStatus())) + .map( + response -> + Json.parse(new String(response.getBody())) + .asObject() + .get("data") + .asObject() + .get("token") + .asString()) + .doOnSuccess( + creds -> + LOGGER.info( + "[rpcGetServiceToken] Successfully obtained vault service token: {}", + MaskUtil.mask(creds))); + } + + 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 VaultServiceTokenSupplier copy() { + return new VaultServiceTokenSupplier(this); + } +}