diff --git a/pom.xml b/pom.xml index bcf7dae69..21e906348 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,8 @@ 2.6.7.RC1 1.0.13 - 1.0.16 + 1.0.18 + 0.4.15 2020.0.5 2.11.0 @@ -71,6 +72,7 @@ 2.13.2 3.4.2 4.1.60.Final + 1.26 3.0.2 2.1.2 @@ -85,7 +87,7 @@ services-transport-parent services-discovery services-bytebuf-codec - services-security + services-security-parent services-examples @@ -106,6 +108,13 @@ ${scalecube-security-tokens.version} + + + io.scalecube + config-vault + ${scalecube-config.version} + + io.scalecube @@ -206,6 +215,13 @@ netty-common ${netty.version} + + + + org.yaml + snakeyaml + ${snakeyaml.version} + diff --git a/services-security-parent/pom.xml b/services-security-parent/pom.xml new file mode 100644 index 000000000..42d197147 --- /dev/null +++ b/services-security-parent/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + + io.scalecube + scalecube-services-parent + 2.10.13-SNAPSHOT + + + scalecube-services-security-parent + pom + + + services-security + services-security-vault + + + diff --git a/services-security-parent/services-security-vault/pom.xml b/services-security-parent/services-security-vault/pom.xml new file mode 100644 index 000000000..5e1caf63d --- /dev/null +++ b/services-security-parent/services-security-vault/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + io.scalecube + scalecube-services-security-parent + 2.10.13-SNAPSHOT + + + scalecube-services-security-vault + + + + io.scalecube + scalecube-services + ${project.version} + + + io.scalecube + scalecube-services-security + ${project.version} + + + + io.scalecube + config-vault + + + org.yaml + snakeyaml + + + + 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); + } +} diff --git a/services-security/pom.xml b/services-security-parent/services-security/pom.xml similarity index 90% rename from services-security/pom.xml rename to services-security-parent/services-security/pom.xml index 737f90ac1..5c3ec8cf4 100644 --- a/services-security/pom.xml +++ b/services-security-parent/services-security/pom.xml @@ -6,7 +6,7 @@ io.scalecube - scalecube-services-parent + scalecube-services-security-parent 2.10.13-SNAPSHOT @@ -18,6 +18,7 @@ scalecube-services ${project.version} + io.scalecube scalecube-security-tokens diff --git a/services-security/src/main/java/io/scalecube/services/security/CompositeAuthenticator.java b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/CompositeAuthenticator.java similarity index 100% rename from services-security/src/main/java/io/scalecube/services/security/CompositeAuthenticator.java rename to services-security-parent/services-security/src/main/java/io/scalecube/services/security/CompositeAuthenticator.java diff --git a/services-security/src/main/java/io/scalecube/services/security/Credentials.java b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/Credentials.java similarity index 97% rename from services-security/src/main/java/io/scalecube/services/security/Credentials.java rename to services-security-parent/services-security/src/main/java/io/scalecube/services/security/Credentials.java index 4826f8c1f..9e9d83ea9 100644 --- a/services-security/src/main/java/io/scalecube/services/security/Credentials.java +++ b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/Credentials.java @@ -15,6 +15,10 @@ public class Credentials { + private Credentials() { + // Do not instantiate + } + /** * Encodes the given credentials to the given stream. * diff --git a/services-security-parent/services-security/src/main/java/io/scalecube/services/security/RetryStrategies.java b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/RetryStrategies.java new file mode 100644 index 000000000..0de36f483 --- /dev/null +++ b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/RetryStrategies.java @@ -0,0 +1,53 @@ +package io.scalecube.services.security; + +import io.scalecube.security.tokens.jwt.KeyNotFoundException; +import java.time.Duration; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; +import reactor.util.retry.RetrySpec; + +public class RetryStrategies { + + private static final int MAX_ATTEMPTS = 20; + private static final Duration MIN_BACKOFF = Duration.ofMillis(200); + private static final Duration MAX_BACKOFF = Duration.ofSeconds(3); + + private RetryStrategies() { + // Do not instantiate + } + + /** + * Returns zero-retries strategy. + * + * @return {@link Retry} instance + */ + public static Retry noRetriesRetryStrategy() { + return Retry.max(0); + } + + /** + * Returns retry-strategy which reacts on {@link KeyNotFoundException}. + * + * @return {@link RetryBackoffSpec} instance + */ + public static RetryBackoffSpec keyNotFoundRetryStrategy() { + return RetrySpec.backoff(MAX_ATTEMPTS, MIN_BACKOFF) + .maxBackoff(MAX_BACKOFF) + .filter(ex -> ex instanceof KeyNotFoundException); + } + + /** + * Returns retry-strategy which reacts on {@link KeyNotFoundException}. + * + * @param maxAttempts maxAttempts + * @param minBackoff minBackoff + * @param maxBackoff maxBackoff + * @return {@link RetryBackoffSpec} instance + */ + public static RetryBackoffSpec keyNotFoundRetryStrategy( + int maxAttempts, Duration minBackoff, Duration maxBackoff) { + return RetrySpec.backoff(maxAttempts, minBackoff) + .maxBackoff(maxBackoff) + .filter(ex -> ex instanceof KeyNotFoundException); + } +} diff --git a/services-security/src/main/java/io/scalecube/services/security/ServiceClaims.java b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/ServiceClaims.java similarity index 100% rename from services-security/src/main/java/io/scalecube/services/security/ServiceClaims.java rename to services-security-parent/services-security/src/main/java/io/scalecube/services/security/ServiceClaims.java diff --git a/services-security/src/main/java/io/scalecube/services/security/ServiceTokenAuthenticator.java b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/ServiceTokenAuthenticator.java similarity index 65% rename from services-security/src/main/java/io/scalecube/services/security/ServiceTokenAuthenticator.java rename to services-security-parent/services-security/src/main/java/io/scalecube/services/security/ServiceTokenAuthenticator.java index 8b9273312..35efe4ac8 100644 --- a/services-security/src/main/java/io/scalecube/services/security/ServiceTokenAuthenticator.java +++ b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/ServiceTokenAuthenticator.java @@ -22,16 +22,38 @@ public final class ServiceTokenAuthenticator implements Authenticator authData) { } return new ServiceClaims(permissionsClaim); } + + private ServiceTokenAuthenticator copy() { + return new ServiceTokenAuthenticator(this); + } } diff --git a/services-security/src/main/java/io/scalecube/services/security/ServiceTokens.java b/services-security-parent/services-security/src/main/java/io/scalecube/services/security/ServiceTokens.java similarity index 100% rename from services-security/src/main/java/io/scalecube/services/security/ServiceTokens.java rename to services-security-parent/services-security/src/main/java/io/scalecube/services/security/ServiceTokens.java