From cc52dcb5b0eb08178bb86650593acef13e7dde7c Mon Sep 17 00:00:00 2001 From: Jan Heise Date: Tue, 12 Sep 2023 11:48:44 +0200 Subject: [PATCH] DataNode: using JWT for authentication/authorisation (#16334) * jwt implementation, adding preflight + versionprobe * adding JWT auth to anomaly detection client, some minor refactorings * added missing license * added provider, refactorings * fixing small bugs * set smaller default expiration * using the correct OpenSearch distro for the erchitecture * added config options * small refactorings, fixing tests, adding dependencies for running tests from inside IntelliJ * fixing import to correct Duration class * removing unnecessary debug output * fixing tests * fixing tests * adding JWT to Cluster test * fixing path * fixing path problems * fix architecture logic for DatanodeContainerizedBackend * fixing path problems * fixing JWT settings in tests * fixing JWT settings in tests * fixing cluster test * fixing cluster test * fixing cluster test * adding missing jwt token * small refactoring to force thread-safety on creation * the datanode uses mongodb - so the closing sequence needs to be adjusted * moving config changes into config file, keep only change that has to be done by code * adding breaking change info for JWT and clock synchronization. * renaming config settings * renaming config settings * removed debug settings that are no longer in use * renamed provider/annotation to be more precise * lowering log level * smaller refactorings/renamings * keep config change in code until PR is merged, remove code in a follow-up PR * moved changelog file to correct folder * clock synchronsation Co-authored-by: Dennis Oelkers * added jwt auth into changes after merge * upped the timeout * log curl connection information in DatanodeOpensearchWait * replacing with jwt auth * changing wait strategy * fixing imports * remove uname/pwd from opensearch rest base url * propagate jwt token provider to http rest client callback, obtain fresh token every time * inject IndexerJwtAuthTokenProvider in DatanodeConfigurationProvider * remove rest uname/pwd from datanode configuration --------- Co-authored-by: Tomas Dvorak Co-authored-by: Dennis Oelkers --- UPGRADING.md | 1 + changelog/unreleased/pr-16334.toml | 9 ++ data-node/pom.xml | 13 +++ .../org/graylog/datanode/Configuration.java | 35 +++---- .../configuration/DatanodeConfiguration.java | 4 +- .../DatanodeConfigurationProvider.java | 6 +- .../OpensearchConfigurationProvider.java | 12 ++- .../OpensearchSecurityConfiguration.java | 76 +++++++-------- .../management/OpensearchProcessImpl.java | 5 +- .../management/OpensearchRestClient.java | 20 ++-- .../process/OpensearchConfiguration.java | 2 - .../config/opensearch-security/config.yml | 6 +- .../opensearch-security/internal_users.yml | 7 -- .../datanode/DatanodeOpensearchWait.java | 35 +++++-- .../integration/DatanodeClusterIT.java | 13 ++- .../integration/DatanodeSecuritySetupIT.java | 30 +++--- .../DatanodeContainerizedBackend.java | 3 +- graylog-storage-opensearch2/pom.xml | 13 +++ .../RestHighLevelClientProvider.java | 41 ++++++-- .../opensearch2/testing/DatanodeInstance.java | 95 ++++--------------- .../testing/OpenSearchInstance.java | 5 +- graylog2-server/pom.xml | 13 +++ ...ylogCertificateProvisioningPeriodical.java | 30 ++---- .../ServerPreflightChecksModule.java | 6 +- .../ElasticsearchClientConfiguration.java | 11 ++- .../configuration/IndexerDiscoveryModule.java | 1 + .../configuration/RunsWithDataNode.java | 29 ++++++ .../RunsWithDataNodeDiscoveryProvider.java | 83 ++++++++++++++++ .../security/IndexerJwtAuthToken.java | 29 ++++++ .../security/IndexerJwtAuthTokenProvider.java | 80 ++++++++++++++++ .../shared/security/SecurityBindings.java | 3 + .../storage/versionprobe/VersionProbe.java | 39 ++++++-- .../ContainerizedGraylogBackend.java | 44 +++++---- .../graylognode/NodeContainerFactory.java | 2 +- pom.xml | 16 ++++ 35 files changed, 558 insertions(+), 259 deletions(-) create mode 100644 changelog/unreleased/pr-16334.toml create mode 100644 graylog2-server/src/main/java/org/graylog2/configuration/RunsWithDataNode.java create mode 100644 graylog2-server/src/main/java/org/graylog2/configuration/RunsWithDataNodeDiscoveryProvider.java create mode 100644 graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthToken.java create mode 100644 graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthTokenProvider.java diff --git a/UPGRADING.md b/UPGRADING.md index 1049ffea31..dc8b1290a1 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,7 @@ Upgrading to Graylog 5.2.x - New pipeline rule functions for manipulating maps: `map_set` and `map_remove`. ## Breaking Changes +- If you use the DataNode, the system clocks of the nodes have to be synchronized with an external source for JWT usage (within a margin of a couple of seconds). ### Migrating from legacy index templates to composable index templates diff --git a/changelog/unreleased/pr-16334.toml b/changelog/unreleased/pr-16334.toml new file mode 100644 index 0000000000..d757102df5 --- /dev/null +++ b/changelog/unreleased/pr-16334.toml @@ -0,0 +1,9 @@ +type = "added" +message = "DataNode: if you use the DataNode, the nodes system clocks have to be synchronized for JWT usage." + +pulls = ["16334"] + +details.user = """ +JWT usage over several systems/nodes requires clock synchronization to prevent errors with expiration time and +clock skew between client/server. +""" diff --git a/data-node/pom.xml b/data-node/pom.xml index 4b2de58e4a..5eb8847266 100644 --- a/data-node/pom.xml +++ b/data-node/pom.xml @@ -195,6 +195,19 @@ netty-tcnative-boringssl-static linux-x86_64 + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + + + io.jsonwebtoken + jjwt-jackson + runtime + org.glassfish.jersey.inject diff --git a/data-node/src/main/java/org/graylog/datanode/Configuration.java b/data-node/src/main/java/org/graylog/datanode/Configuration.java index 91fe38f123..21347106a3 100644 --- a/data-node/src/main/java/org/graylog/datanode/Configuration.java +++ b/data-node/src/main/java/org/graylog/datanode/Configuration.java @@ -23,6 +23,7 @@ import com.github.joschi.jadconfig.Validator; import com.github.joschi.jadconfig.ValidatorMethod; import com.github.joschi.jadconfig.converters.IntegerConverter; import com.github.joschi.jadconfig.converters.StringListConverter; +import com.github.joschi.jadconfig.util.Duration; import com.github.joschi.jadconfig.validators.PositiveIntegerValidator; import com.github.joschi.jadconfig.validators.StringNotBlankValidator; import com.github.joschi.jadconfig.validators.URIAbsoluteValidator; @@ -190,12 +191,27 @@ public class Configuration extends BaseConfiguration { return opensearchProcessLogsBufferSize; } - @Parameter(value = "rest_api_username") - private String restApiUsername; - @Parameter(value = "password_secret", required = true, validators = StringNotBlankValidator.class) private String passwordSecret; + public String getPasswordSecret() { + return passwordSecret; + } + + @Parameter(value = "indexer_jwt_auth_token_caching_duration") + Duration indexerJwtAuthTokenCachingDuration = Duration.seconds(60); + + public Duration getIndexerJwtAuthTokenCachingDuration() { + return indexerJwtAuthTokenCachingDuration; + } + + @Parameter(value = "indexer_jwt_auth_token_expiration_duration") + Duration indexerJwtAuthTokenExpirationDuration = Duration.seconds(180); + + public Duration getIndexerJwtAuthTokenExpirationDuration() { + return indexerJwtAuthTokenExpirationDuration; + } + @ValidatorMethod @SuppressWarnings("unused") public void validatePasswordSecret() throws ValidationException { @@ -204,9 +220,6 @@ public class Configuration extends BaseConfiguration { } } - @Parameter(value = "rest_api_password") - private String restApiPassword; - @Parameter(value = "node_id_file", validators = NodeIdFileValidator.class) private String nodeIdFile = "data/node-id"; @@ -274,16 +287,6 @@ public class Configuration extends BaseConfiguration { return datanodeHttpCertificatePassword; } - - public String getRestApiUsername() { - return restApiUsername; - } - - public String getRestApiPassword() { - return restApiPassword; - } - - public Optional getOpensearchNetworkHostHost() { return Optional.ofNullable(opensearchNetworkHostHost); } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeConfiguration.java b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeConfiguration.java index b570005be6..2744fe5d5c 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeConfiguration.java @@ -17,6 +17,7 @@ package org.graylog.datanode.configuration; import org.graylog.datanode.OpensearchDistribution; +import org.graylog2.security.IndexerJwtAuthTokenProvider; /** * DatanodeConfiguration holds the static configuration as parsed during the datanode startup, either from the @@ -25,6 +26,7 @@ import org.graylog.datanode.OpensearchDistribution; public record DatanodeConfiguration( OpensearchDistribution opensearchDistribution, String nodeName, - int processLogsBufferSize + int processLogsBufferSize, + IndexerJwtAuthTokenProvider indexerJwtAuthTokenProvider ) { } diff --git a/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeConfigurationProvider.java b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeConfigurationProvider.java index 7964735919..1d98e1090d 100644 --- a/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeConfigurationProvider.java +++ b/data-node/src/main/java/org/graylog/datanode/configuration/DatanodeConfigurationProvider.java @@ -18,6 +18,7 @@ package org.graylog.datanode.configuration; import org.graylog.datanode.Configuration; import org.graylog.datanode.OpensearchDistribution; +import org.graylog2.security.IndexerJwtAuthTokenProvider; import javax.inject.Inject; import javax.inject.Provider; @@ -31,12 +32,13 @@ public class DatanodeConfigurationProvider implements Provider { - private final Configuration localConfiguration; private final UploadedCertFilesSecureConfiguration uploadedCertFilesSecureConfiguration; private final MongoCertSecureConfiguration mongoCertSecureConfiguration; private final InSecureConfiguration inSecureConfiguration; private final DatanodeConfiguration datanodeConfiguration; + private final byte[] signingKey; @Inject public OpensearchConfigurationProvider(Configuration localConfiguration, DatanodeConfiguration datanodeConfiguration, UploadedCertFilesSecureConfiguration uploadedCertFilesSecureConfiguration, MongoCertSecureConfiguration mongoCertSecureConfiguration, - InSecureConfiguration inSecureConfiguration) { + InSecureConfiguration inSecureConfiguration, + final @Named("password_secret") String passwordSecret) { this.localConfiguration = localConfiguration; this.datanodeConfiguration = datanodeConfiguration; this.uploadedCertFilesSecureConfiguration = uploadedCertFilesSecureConfiguration; this.mongoCertSecureConfiguration = mongoCertSecureConfiguration; this.inSecureConfiguration = inSecureConfiguration; + this.signingKey = passwordSecret.getBytes(StandardCharsets.UTF_8); } @Override @@ -82,7 +86,7 @@ public class OpensearchConfigurationProvider implements Provider filterConfigurationMap(final Map map, final String... keys) { + Map result = map; + for(final String key: List.of(keys)) { + result = (Map)result.get(key); + } + return result; + } + + private void enableJwtAuthenticationInConfig(final Path opensearchConfigDir, final byte[] signingKey) throws IOException { + final ObjectMapper objectMapper = new YAMLMapper(); + final File file = opensearchConfigDir.resolve(Path.of("opensearch-security", "config.yml")).toFile(); + Map contents = objectMapper.readValue(file, new TypeReference<>() {}); + + /* TODO: the following two lines should be active, once #16334 is merged and there are nightly builds of master available to build a compatible 5.2-dev DataNode Docker image + Map config = filterConfigurationMap(contents, "config", "dynamic", "authc", "jwt_auth_domain", "http_authenticator", "config"); + config.put("signing_key", Base64.getEncoder().encodeToString(signingKey)); + */ + + // TODO: remove the following block, once #16334 is merged and there are nightly builds of master available to build a compatible 5.2-dev DataNode Docker image + Map jwt_auth_domain = filterConfigurationMap(contents, "config", "dynamic", "authc", "jwt_auth_domain"); + jwt_auth_domain.put("http_enabled", true); + jwt_auth_domain.put("transport_enabled", true); + + Map config = filterConfigurationMap(jwt_auth_domain, "http_authenticator", "config"); + config.put("signing_key", Base64.getEncoder().encodeToString(signingKey)); + config.put("roles_key", "os_roles"); + // TODO: remove up until here + + objectMapper.writeValue(file, contents); + } + public boolean securityEnabled() { return !Objects.isNull(httpCertificate) && !Objects.isNull(transportCertificate); } @@ -169,37 +200,6 @@ public class OpensearchSecurityConfiguration { return config.build(); } - protected void configureInitialAdmin(final Configuration localConfiguration, final Path opensearchConfigDir, - final String adminUsername, - final String adminPassword) throws IOException { - final Path internalUsersFile = opensearchConfigDir.resolve("opensearch-security").resolve("internal_users.yml"); - - Objects.requireNonNull(localConfiguration.getRestApiUsername(), - "rest_api_username has to be configured the usage of secured Opensearch REST api" - ); - - Objects.requireNonNull(localConfiguration.getRestApiPassword(), - "rest_api_password has to be configured the usage of secured Opensearch REST api" - ); - - final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - final Map map = mapper.readValue(new FileInputStream(internalUsersFile.toFile()), TypeReferences.MAP_STRING_OBJECT); - final Map adminUserConfig = (Map) map.get("admin"); - - map.remove("admin"); - map.put(adminUsername, adminUserConfig); - - final BCryptPasswordAlgorithm passwordAlgorithm = new BCryptPasswordAlgorithm(12); - final String hashWithPrefix = passwordAlgorithm.hash(adminPassword); - - // remove the prefix and suffix, we need just the hash itself - final String hash = hashWithPrefix.substring("{bcrypt}".length(), hashWithPrefix.indexOf("{salt}")); - adminUserConfig.put("hash", hash); - - final FileOutputStream fos = new FileOutputStream(internalUsersFile.toFile()); - mapper.writeValue(fos, map); - } - private void logCertificateInformation(String certificateType, KeystoreInformation keystore) throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { final KeyStore instance = KeyStore.getInstance(KEYSTORE_FORMAT); try (final FileInputStream is = new FileInputStream(keystore.location().toFile())) { diff --git a/data-node/src/main/java/org/graylog/datanode/management/OpensearchProcessImpl.java b/data-node/src/main/java/org/graylog/datanode/management/OpensearchProcessImpl.java index f36d9d19d4..342069a2ae 100644 --- a/data-node/src/main/java/org/graylog/datanode/management/OpensearchProcessImpl.java +++ b/data-node/src/main/java/org/graylog/datanode/management/OpensearchProcessImpl.java @@ -71,7 +71,7 @@ class OpensearchProcessImpl implements OpensearchProcess, ProcessListener { } private RestHighLevelClient createRestClient(OpensearchConfiguration configuration) { - return OpensearchRestClient.build(configuration, trustManager); + return OpensearchRestClient.build(configuration, datanodeConfiguration, trustManager); } @Override @@ -98,8 +98,7 @@ class OpensearchProcessImpl implements OpensearchProcess, ProcessListener { .map(httpHost -> new URIBuilder() .setHost(httpHost.getHostName()) .setPort(httpHost.getPort()) - .setScheme(httpHost.getSchemeName()) - .setUserInfo(configuration.get().authUsername(), configuration.get().authPassword()).toString()) + .setScheme(httpHost.getSchemeName()).toString()) .orElse(""); // TODO: will this cause problems in the nodeservice? return URI.create(baseUrl); } diff --git a/data-node/src/main/java/org/graylog/datanode/management/OpensearchRestClient.java b/data-node/src/main/java/org/graylog/datanode/management/OpensearchRestClient.java index 5014b3ea2a..ba75092d25 100644 --- a/data-node/src/main/java/org/graylog/datanode/management/OpensearchRestClient.java +++ b/data-node/src/main/java/org/graylog/datanode/management/OpensearchRestClient.java @@ -16,19 +16,15 @@ */ package org.graylog.datanode.management; +import org.graylog.datanode.configuration.DatanodeConfiguration; import org.graylog.datanode.process.OpensearchConfiguration; -import org.graylog.security.certutil.CaService; import org.graylog.shaded.opensearch2.org.apache.http.HttpHost; -import org.graylog.shaded.opensearch2.org.apache.http.auth.AuthScope; -import org.graylog.shaded.opensearch2.org.apache.http.auth.UsernamePasswordCredentials; -import org.graylog.shaded.opensearch2.org.apache.http.client.CredentialsProvider; -import org.graylog.shaded.opensearch2.org.apache.http.impl.client.BasicCredentialsProvider; +import org.graylog.shaded.opensearch2.org.apache.http.HttpRequestInterceptor; import org.graylog.shaded.opensearch2.org.opensearch.client.RestHighLevelClient; import org.graylog2.security.CustomCAX509TrustManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import java.security.KeyManagementException; @@ -38,23 +34,21 @@ import java.security.SecureRandom; public class OpensearchRestClient { private static final Logger LOG = LoggerFactory.getLogger(OpensearchRestClient.class); - public static RestHighLevelClient build(final OpensearchConfiguration configuration, final CustomCAX509TrustManager tm) { - final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - + public static RestHighLevelClient build(final OpensearchConfiguration configuration, final DatanodeConfiguration datanodeConfiguration, final CustomCAX509TrustManager tm) { final HttpHost host = configuration.getRestBaseUrl(); org.graylog.shaded.opensearch2.org.opensearch.client.RestClientBuilder builder = org.graylog.shaded.opensearch2.org.opensearch.client.RestClient.builder(host); if ("https".equals(host.getSchemeName())) { - if (configuration.authUsername() != null && configuration.authPassword() != null) { - credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(configuration.authUsername(), configuration.authPassword())); - } try { final var sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[]{tm}, new SecureRandom()); builder.setHttpClientConfigCallback(httpClientBuilder -> { - httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + httpClientBuilder.addInterceptorLast((HttpRequestInterceptor) (request, context) -> { + final String jwtToken = datanodeConfiguration.indexerJwtAuthTokenProvider().get(); + request.addHeader("Authorization", jwtToken); + }); httpClientBuilder.setSSLContext(sslContext); return httpClientBuilder; }); diff --git a/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java b/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java index d99dbe2df3..a1510832fc 100644 --- a/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java +++ b/data-node/src/main/java/org/graylog/datanode/process/OpensearchConfiguration.java @@ -33,8 +33,6 @@ public record OpensearchConfiguration( String hostname, int httpPort, int transportPort, - String authUsername, - String authPassword, String clusterName, String nodeName, List nodeRoles, List discoverySeedHosts, OpensearchSecurityConfiguration opensearchSecurityConfiguration, diff --git a/data-node/src/main/resources/opensearch/config/opensearch-security/config.yml b/data-node/src/main/resources/opensearch/config/opensearch-security/config.yml index 59f5f05352..b9958375af 100644 --- a/data-node/src/main/resources/opensearch/config/opensearch-security/config.yml +++ b/data-node/src/main/resources/opensearch/config/opensearch-security/config.yml @@ -121,8 +121,8 @@ config: type: noop jwt_auth_domain: description: "Authenticate via Json Web Token" - http_enabled: false - transport_enabled: false + http_enabled: true + transport_enabled: true order: 0 http_authenticator: type: jwt @@ -131,7 +131,7 @@ config: signing_key: "base64 encoded HMAC key or public RSA/ECDSA pem key" jwt_header: "Authorization" jwt_url_parameter: null - roles_key: null + roles_key: "os_roles" subject_key: null authentication_backend: type: noop diff --git a/data-node/src/main/resources/opensearch/config/opensearch-security/internal_users.yml b/data-node/src/main/resources/opensearch/config/opensearch-security/internal_users.yml index f4d31e52c6..68838f482f 100644 --- a/data-node/src/main/resources/opensearch/config/opensearch-security/internal_users.yml +++ b/data-node/src/main/resources/opensearch/config/opensearch-security/internal_users.yml @@ -10,13 +10,6 @@ _meta: ## Demo users -admin: - hash: "$2a$12$VcCDgh2NDk07JGN0rjGbM.Ad41qVR/YFJcgHp0UGns5JDymv..TOG" - reserved: true - backend_roles: - - "admin" - description: "Demo admin user" - anomalyadmin: hash: "$2y$12$TRwAAJgnNo67w3rVUz4FIeLx9Dy/llB79zf9I15CKJ9vkM4ZzAd3." reserved: false diff --git a/data-node/src/test/java/org/graylog/datanode/DatanodeOpensearchWait.java b/data-node/src/test/java/org/graylog/datanode/DatanodeOpensearchWait.java index a8a37f7462..8eb233f25c 100644 --- a/data-node/src/test/java/org/graylog/datanode/DatanodeOpensearchWait.java +++ b/data-node/src/test/java/org/graylog/datanode/DatanodeOpensearchWait.java @@ -38,6 +38,8 @@ import org.slf4j.LoggerFactory; import javax.net.ssl.SSLHandshakeException; import java.net.SocketException; import java.security.KeyStore; +import java.util.LinkedList; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -46,13 +48,12 @@ import java.util.function.Predicate; public class DatanodeOpensearchWait { - private static final Logger LOG = LoggerFactory.getLogger(DatanodeSecuritySetupIT.class); + private static final Logger LOG = LoggerFactory.getLogger(DatanodeOpensearchWait.class); private static final int ATTEMPTS_COUNT = 160; private final int opensearchPort; private KeyStore truststore; - private String username; - private String password; + private String jwtToken; private String lastRecordedResponse; private DatanodeOpensearchWait(int opensearchPort) { @@ -68,9 +69,8 @@ public class DatanodeOpensearchWait { return this; } - public DatanodeOpensearchWait withBasicAuth(String username, String password) { - this.username = username; - this.password = password; + public DatanodeOpensearchWait withJwtAuth(String jwtToken) { + this.jwtToken = jwtToken; return this; } @@ -90,6 +90,11 @@ public class DatanodeOpensearchWait { @SafeVarargs private ValidatableResponse waitForOpensearch(Predicate... predicates) throws ExecutionException, RetryException { + + if(LOG.isInfoEnabled()) { + LOG.info("cURL request: " + formatCurlConnectionInfo()); + } + //noinspection UnstableApiUsage final RetryerBuilder builder = RetryerBuilder.newBuilder() .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS)) @@ -122,8 +127,8 @@ public class DatanodeOpensearchWait { Optional.ofNullable(truststore).ifPresent(ts -> req.trustStore(truststore)); - if (username != null && password != null) { - req.auth().basic(username, password); + if (jwtToken != null) { + req.header( "Authorization", "Bearer " + jwtToken); } return req.get(formatClusterHeathUrl()) @@ -138,6 +143,16 @@ public class DatanodeOpensearchWait { } } + private String formatCurlConnectionInfo() { + List builder = new LinkedList<>(); + builder.add("curl -k"); // trust self-signed certs + if(jwtToken != null) { + builder.add("-H \"Authorization: Bearer " + jwtToken + "\""); + } + builder.add(formatClusterHeathUrl()); + return String.join(" ", builder); + } + private void printShardsInfo() { try { final RequestSpecification req = RestAssured.given() @@ -145,8 +160,8 @@ public class DatanodeOpensearchWait { Optional.ofNullable(truststore).ifPresent(ts -> req.trustStore(truststore)); - if (username != null && password != null) { - req.auth().basic(username, password); + if (jwtToken != null) { + req.header( "Authorization", "Bearer " + jwtToken); } final String shardsResponse = req.get("https://localhost:" + opensearchPort + "/_cat/shards?v=true&h=index,shard,prirep,state,node,unassigned.reason&s=state") diff --git a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeClusterIT.java b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeClusterIT.java index 1768282f46..5a704bf4e9 100644 --- a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeClusterIT.java +++ b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeClusterIT.java @@ -16,11 +16,14 @@ */ package org.graylog.datanode.integration; +import com.github.joschi.jadconfig.util.Duration; import com.github.rholder.retry.RetryException; import org.apache.commons.lang3.RandomStringUtils; import org.graylog.datanode.DatanodeOpensearchWait; import org.graylog.datanode.configuration.variants.KeystoreInformation; import org.graylog.datanode.testinfra.DatanodeContainerizedBackend; +import org.graylog2.security.IndexerJwtAuthTokenProvider; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,8 +33,11 @@ import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; +import javax.net.ssl.SSLHandshakeException; import javax.validation.constraints.NotNull; import java.io.IOException; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; @@ -144,6 +150,7 @@ public class DatanodeClusterIT { mongodb, hostname, datanodeContainer -> { + datanodeContainer.withEnv("GRAYLOG_DATANODE_PASSWORD_SECRET", DatanodeContainerizedBackend.SIGNING_SECRET); datanodeContainer.withEnv("GRAYLOG_DATANODE_CLUSTER_INITIAL_MANAGER_NODES", hostnameNodeA); datanodeContainer.withEnv("GRAYLOG_DATANODE_OPENSEARCH_DISCOVERY_SEED_HOSTS", hostnameNodeA + ":9300"); @@ -174,11 +181,13 @@ public class DatanodeClusterIT { }); } - private void waitForNodesCount(int countOfNodes) throws ExecutionException, RetryException { + private void waitForNodesCount(final int countOfNodes) throws ExecutionException, RetryException { + final String jwtToken = IndexerJwtAuthTokenProvider.createToken(DatanodeContainerizedBackend.SIGNING_SECRET.getBytes(StandardCharsets.UTF_8), Duration.seconds(120)); + try { DatanodeOpensearchWait.onPort(nodeA.getOpensearchRestPort()) .withTruststore(trustStore) - .withBasicAuth(initialAdminUsername, initialAdminPassword) + .withJwtAuth(jwtToken) .waitForNodesCount(countOfNodes); } catch (Exception retryException) { LOG.error("DataNode Container logs from nodeA follow:\n" + nodeA.getLogs()); diff --git a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java index f698124e9b..769a8de27d 100644 --- a/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java +++ b/data-node/src/test/java/org/graylog/datanode/integration/DatanodeSecuritySetupIT.java @@ -16,6 +16,7 @@ */ package org.graylog.datanode.integration; +import com.github.joschi.jadconfig.util.Duration; import com.github.rholder.retry.RetryException; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; @@ -28,6 +29,7 @@ import org.apache.http.NoHttpResponseException; import org.graylog.datanode.configuration.variants.KeystoreInformation; import org.graylog.datanode.testinfra.DatanodeContainerizedBackend; import org.graylog2.plugin.Tools; +import org.graylog2.security.IndexerJwtAuthTokenProvider; import org.graylog2.shared.utilities.StringUtils; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; @@ -40,6 +42,7 @@ import org.slf4j.LoggerFactory; import javax.net.ssl.SSLHandshakeException; import java.io.IOException; import java.net.SocketException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; @@ -86,6 +89,7 @@ public class DatanodeSecuritySetupIT { datanodeContainer.withEnv("GRAYLOG_DATANODE_TRANSPORT_CERTIFICATE", "datanode-transport-certificates.p12"); datanodeContainer.withEnv("GRAYLOG_DATANODE_TRANSPORT_CERTIFICATE_PASSWORD", transportCert.passwordAsString()); datanodeContainer.withEnv("GRAYLOG_DATANODE_INSECURE_STARTUP", "false"); + datanodeContainer.withEnv("GRAYLOG_DATANODE_PASSWORD_SECRET", DatanodeContainerizedBackend.SIGNING_SECRET); // configure http security datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_CERTIFICATE", "datanode-https-certificates.p12"); @@ -115,24 +119,24 @@ public class DatanodeSecuritySetupIT { @Test void testSecuredSetup() throws ExecutionException, RetryException { + final String jwtAuthToken = IndexerJwtAuthTokenProvider.createToken(DatanodeContainerizedBackend.SIGNING_SECRET.getBytes(StandardCharsets.UTF_8), Duration.seconds(120)); - waitForOpensearchAvailableStatus(backend.getDatanodeRestPort(), trustStore); + waitForOpensearchAvailableStatus(backend.getDatanodeRestPort(), backend.getOpensearchRestPort(), trustStore); try { - given() - .auth().basic(httpUsername, httpPassword) - .trustStore(trustStore) - .get("https://localhost:" + backend.getOpensearchRestPort()) - .then().assertThat() - .body("name", Matchers.equalTo(containerHostname)) - .body("cluster_name", Matchers.equalTo("datanode-cluster")); + given().header( "Authorization", "Bearer " + jwtAuthToken) + .trustStore(trustStore) + .get("https://localhost:" + backend.getOpensearchRestPort()) + .then().assertThat() + .body("name", Matchers.equalTo(containerHostname)) + .body("cluster_name", Matchers.equalTo("datanode-cluster")); } catch (Exception ex) { LOG.error("Error connecting to OpenSearch in the DataNode, showing logs:\n{}", backend.getLogs()); throw ex; } } - private void waitForOpensearchAvailableStatus(Integer datanodeRestPort, KeyStore trustStore) throws ExecutionException, RetryException { + private void waitForOpensearchAvailableStatus(final Integer datanodeRestPort, final Integer opensearchPort, final KeyStore trustStore) throws ExecutionException, RetryException { final Retryer retryer = RetryerBuilder.newBuilder() .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt(120)) @@ -145,11 +149,13 @@ public class DatanodeSecuritySetupIT { try { var hostname = Tools.getLocalCanonicalHostname(); - var url = StringUtils.f("https://%s:%d", hostname, datanodeRestPort); - LOG.info("Trying to connect to: {}", url); + var rest = StringUtils.f("https://%s:%d", hostname, datanodeRestPort); + var os = StringUtils.f("https://%s:%d", hostname, opensearchPort); + LOG.info("Trying to connect to REST API at: {}, OpenSearch is at: {}", rest, os); retryer.call(() -> RestAssured.given() + .auth().basic(httpUsername, httpPassword) .trustStore(trustStore) - .get(url) + .get(rest) .then()); } catch (Exception ex) { LOG.error("Error starting the DataNode, showing logs:\n" + backend.getLogs()); diff --git a/data-node/src/test/java/org/graylog/datanode/testinfra/DatanodeContainerizedBackend.java b/data-node/src/test/java/org/graylog/datanode/testinfra/DatanodeContainerizedBackend.java index 45212642f6..ffad624175 100644 --- a/data-node/src/test/java/org/graylog/datanode/testinfra/DatanodeContainerizedBackend.java +++ b/data-node/src/test/java/org/graylog/datanode/testinfra/DatanodeContainerizedBackend.java @@ -38,6 +38,7 @@ import java.util.Properties; import java.util.function.Supplier; public class DatanodeContainerizedBackend { + static public final String SIGNING_SECRET = "0123456789012345678901234567890123456789012345678901234567890987654321098765432109876543211"; public static final int DATANODE_REST_PORT = 8999; public static final int DATANODE_OPENSEARCH_PORT = 9200; @@ -106,7 +107,7 @@ public class DatanodeContainerizedBackend { .withEnv("GRAYLOG_DATANODE_REST_API_USERNAME", "admin") .withEnv("GRAYLOG_DATANODE_REST_API_PASSWORD", "admin") - .withEnv("GRAYLOG_DATANODE_PASSWORD_SECRET", "this_is_not_used_but_required") + .withEnv("GRAYLOG_DATANODE_PASSWORD_SECRET", SIGNING_SECRET) .withEnv("GRAYLOG_DATANODE_NODE_ID_FILE", "./node-id") .withEnv("GRAYLOG_DATANODE_HTTP_BIND_ADDRESS", "0.0.0.0:" + DATANODE_REST_PORT) diff --git a/graylog-storage-opensearch2/pom.xml b/graylog-storage-opensearch2/pom.xml index 66e14b8886..c610845658 100644 --- a/graylog-storage-opensearch2/pom.xml +++ b/graylog-storage-opensearch2/pom.xml @@ -73,6 +73,19 @@ + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + + + io.jsonwebtoken + jjwt-jackson + runtime + org.opensearch opensearch-testcontainers diff --git a/graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/RestHighLevelClientProvider.java b/graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/RestHighLevelClientProvider.java index ff68a99912..41880c307d 100644 --- a/graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/RestHighLevelClientProvider.java +++ b/graylog-storage-opensearch2/src/main/java/org/graylog/storage/opensearch2/RestHighLevelClientProvider.java @@ -19,12 +19,15 @@ package org.graylog.storage.opensearch2; import com.github.joschi.jadconfig.util.Duration; import com.google.common.base.Suppliers; import org.graylog.shaded.opensearch2.org.apache.http.HttpHost; +import org.graylog.shaded.opensearch2.org.apache.http.HttpRequestInterceptor; import org.graylog.shaded.opensearch2.org.apache.http.client.CredentialsProvider; import org.graylog.shaded.opensearch2.org.opensearch.client.RestClient; import org.graylog.shaded.opensearch2.org.opensearch.client.RestClientBuilder; import org.graylog.shaded.opensearch2.org.opensearch.client.RestHighLevelClient; import org.graylog.shaded.opensearch2.org.opensearch.client.sniff.OpenSearchNodesSniffer; import org.graylog2.configuration.IndexerHosts; +import org.graylog2.configuration.RunsWithDataNode; +import org.graylog2.security.IndexerJwtAuthToken; import org.graylog2.security.TrustManagerAndSocketFactoryProvider; import org.graylog2.system.shutdown.GracefulShutdownService; import org.slf4j.Logger; @@ -66,7 +69,10 @@ public class RestHighLevelClientProvider implements Provider new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme())).toArray(HttpHost[]::new); final RestClientBuilder restClientBuilder = RestClient.builder(esHosts) - .setRequestConfigCallback(requestConfig -> requestConfig - .setConnectTimeout(Math.toIntExact(connectTimeout.toMilliseconds())) - .setSocketTimeout(Math.toIntExact(socketTimeout.toMilliseconds())) - .setExpectContinueEnabled(useExpectContinue) - .setAuthenticationEnabled(true) + .setRequestConfigCallback(requestConfig -> { + requestConfig + .setConnectTimeout(Math.toIntExact(connectTimeout.toMilliseconds())) + .setSocketTimeout(Math.toIntExact(socketTimeout.toMilliseconds())) + .setExpectContinueEnabled(useExpectContinue); + // manually handle Auth if we use JWT + if (!isJwtAuthentication) { + requestConfig.setAuthenticationEnabled(true); + } + return requestConfig; + } ) .setHttpClientConfigCallback(httpClientConfig -> { httpClientConfig .setMaxConnTotal(maxTotalConnections) - .setMaxConnPerRoute(maxTotalConnectionsPerRoute) - .setDefaultCredentialsProvider(credentialsProvider); + .setMaxConnPerRoute(maxTotalConnectionsPerRoute); + + if(isJwtAuthentication) { + httpClientConfig.addInterceptorLast((HttpRequestInterceptor) (request, context) -> request.addHeader("Authorization", jwtBearerToken)); + } else { + httpClientConfig.setDefaultCredentialsProvider(credentialsProvider); + } if(muteElasticsearchDeprecationWarnings) { httpClientConfig.addInterceptorFirst(new OpenSearchFilterDeprecationWarningsInterceptor()); diff --git a/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/DatanodeInstance.java b/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/DatanodeInstance.java index 3558cbfcd0..57c4f7a5fd 100644 --- a/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/DatanodeInstance.java +++ b/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/DatanodeInstance.java @@ -16,54 +16,32 @@ */ package org.graylog.storage.opensearch2.testing; -import com.github.joschi.jadconfig.util.Duration; -import com.google.common.collect.ImmutableList; -import org.graylog.shaded.opensearch2.org.apache.http.impl.client.BasicCredentialsProvider; -import org.graylog.shaded.opensearch2.org.opensearch.client.RestHighLevelClient; -import org.graylog.storage.opensearch2.OpenSearchClient; -import org.graylog.storage.opensearch2.RestHighLevelClientProvider; import org.graylog.testing.containermatrix.SearchServer; -import org.graylog.testing.elasticsearch.Adapters; -import org.graylog.testing.elasticsearch.Client; -import org.graylog.testing.elasticsearch.FixtureImporter; -import org.graylog.testing.elasticsearch.TestableSearchServerInstance; -import org.graylog2.shared.bindings.providers.ObjectMapperProvider; import org.graylog2.storage.SearchVersion; -import org.graylog2.system.shutdown.GracefulShutdownService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.PullPolicy; import org.testcontainers.utility.DockerImageName; -import java.net.URI; import java.util.List; import java.util.Locale; import static java.util.Objects.isNull; -public class DatanodeInstance extends TestableSearchServerInstance { +public class DatanodeInstance extends OpenSearchInstance { + private static final Logger LOG = LoggerFactory.getLogger(DatanodeInstance.class); public static final SearchServer DATANODE_VERSION = SearchServer.DATANODE_DEV; - private final OpenSearchClient openSearchClient; - private final Client client; - private final FixtureImporter fixtureImporter; - private final Adapters adapters; - public DatanodeInstance(final SearchVersion version, final Network network, final String heapSize, final List featureFlags) { - super(version, network, heapSize); - - RestHighLevelClient restHighLevelClient = buildRestClient(); - this.openSearchClient = new OpenSearchClient(restHighLevelClient, false, new ObjectMapperProvider().get()); - this.client = new ClientOS2(this.openSearchClient, featureFlags); - this.fixtureImporter = new FixtureImporterOS2(this.openSearchClient); - adapters = new AdaptersOS2(openSearchClient); - Runtime.getRuntime().addShutdownHook(new Thread(this::close)); + super(version, network, heapSize, featureFlags); } @Override protected String imageName() { - return String.format(Locale.ROOT, "graylog/graylog-datanode:%s", "dev"); + return String.format(Locale.ROOT, "graylog/graylog-datanode:%s", "5.2-dev"); } @Override @@ -71,42 +49,6 @@ public class DatanodeInstance extends TestableSearchServerInstance { return DATANODE_VERSION; } - private RestHighLevelClient buildRestClient() { - return new RestHighLevelClientProvider( - new GracefulShutdownService(), - ImmutableList.of(URI.create("http://" + this.getHttpHostAddress())), - Duration.seconds(60), - Duration.seconds(60), - Duration.seconds(60), - 1, - 1, - 1, - false, - false, - null, - Duration.seconds(60), - "http", - false, - false, - new BasicCredentialsProvider(), - null) - .get(); - } - - @Override - public Client client() { - return this.client; - } - - @Override - public FixtureImporter fixtureImporter() { - return this.fixtureImporter; - } - - public OpenSearchClient openSearchClient() { - return this.openSearchClient; - } - @Override public GenericContainer buildContainer(String image, Network network) { return new GenericContainer<>(DockerImageName.parse(image)) @@ -118,19 +60,20 @@ public class DatanodeInstance extends TestableSearchServerInstance { .withEnv("GRAYLOG_DATANODE_ROOT_PASSWORD_SHA2", "") .withEnv("GRAYLOG_DATANODE_MONGODB_URI", "mongodb://mongodb:27017/graylog") .withEnv("GRAYLOG_DATANODE_SINGLE_NODE_ONLY", "true") - .withExposedPorts(9200, 9300) + .withEnv("GRAYLOG_DATANODE_INSECURE_STARTUP", "true") + .withExposedPorts(8999, 9200, 9300) .withNetwork(network) .withNetworkAliases(NETWORK_ALIAS) - .waitingFor(Wait.forHttp("/_cluster/health").forPort(OPENSEARCH_PORT).forStatusCode(200).forResponsePredicate(s -> s.contains("\"status\":\"green\""))); - } - - @Override - public Adapters adapters() { - return this.adapters; - } - - @Override - public String getLogs() { - return this.container.getLogs(); + .waitingFor( + Wait.forHttp("/_cluster/health") + .forPort(OPENSEARCH_PORT) + .forStatusCode(200) + .forResponsePredicate(s -> { + LOG.info("Response while waiting: {}", s); + // allow yellow for fixing indices later + return s.contains("\"status\":\"green\"") || s.contains("\"status\":\"yellow\""); + }) + .withStartupTimeout(java.time.Duration.ofSeconds(180)) + ); } } diff --git a/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/OpenSearchInstance.java b/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/OpenSearchInstance.java index 61d437b5b7..3bad829693 100644 --- a/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/OpenSearchInstance.java +++ b/graylog-storage-opensearch2/src/test/java/org/graylog/storage/opensearch2/testing/OpenSearchInstance.java @@ -155,7 +155,10 @@ public class OpenSearchInstance extends TestableSearchServerInstance { false, false, new BasicCredentialsProvider(), - null) + null, + false, + false, + "maybe_want_jwt_here") .get(); } diff --git a/graylog2-server/pom.xml b/graylog2-server/pom.xml index 593897674a..5a41d70c1d 100644 --- a/graylog2-server/pom.xml +++ b/graylog2-server/pom.xml @@ -104,6 +104,19 @@ org.apache.directory.api api-all + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + + + io.jsonwebtoken + jjwt-jackson + runtime + com.google.inject diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogCertificateProvisioningPeriodical.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogCertificateProvisioningPeriodical.java index 8a877d717e..5e318dcd51 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogCertificateProvisioningPeriodical.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/GraylogCertificateProvisioningPeriodical.java @@ -16,9 +16,7 @@ */ package org.graylog2.bootstrap.preflight; -import com.google.common.base.Splitter; import okhttp3.Call; -import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -39,6 +37,7 @@ import org.graylog2.plugin.certificates.RenewalPolicy; import org.graylog2.plugin.cluster.ClusterConfigService; import org.graylog2.plugin.periodical.Periodical; import org.graylog2.security.CustomCAX509TrustManager; +import org.graylog2.security.IndexerJwtAuthToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,8 +47,6 @@ import javax.inject.Singleton; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -60,8 +57,6 @@ import java.security.cert.X509Certificate; import java.util.List; import java.util.Optional; -import static com.google.common.base.Strings.isNullOrEmpty; - @Singleton public class GraylogCertificateProvisioningPeriodical extends Periodical { private static final Logger LOG = LoggerFactory.getLogger(GraylogCertificateProvisioningPeriodical.class); @@ -78,6 +73,7 @@ public class GraylogCertificateProvisioningPeriodical extends Periodical { private final String passwordSecret; private final ClusterEventBus clusterEventBus; private Optional okHttpClient = Optional.empty(); + private final String jwtBearerToken; @Inject public GraylogCertificateProvisioningPeriodical(final DataNodeProvisioningService dataNodeProvisioningService, @@ -88,7 +84,9 @@ public class GraylogCertificateProvisioningPeriodical extends Periodical { final NodeService nodeService, final CsrSigner csrSigner, final ClusterConfigService clusterConfigService, - final @Named("password_secret") String passwordSecret, ClusterEventBus clusterEventBus) { + final @Named("password_secret") String passwordSecret, + @IndexerJwtAuthToken String jwtBearerToken, + ClusterEventBus clusterEventBus) { this.dataNodeProvisioningService = dataNodeProvisioningService; this.csrStorage = csrStorage; this.certMongoStorage = certMongoStorage; @@ -99,10 +97,11 @@ public class GraylogCertificateProvisioningPeriodical extends Periodical { this.csrSigner = csrSigner; this.clusterConfigService = clusterConfigService; this.clusterEventBus = clusterEventBus; + this.jwtBearerToken = jwtBearerToken; } // building a httpclient to check the connectivity to OpenSearch - TODO: maybe replace it with a VersionProbe already? - private Optional buildTempHttpClient() { + private Optional buildConnectivityCheckOkHttpClient() { try { OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); try { @@ -147,7 +146,7 @@ public class GraylogCertificateProvisioningPeriodical extends Periodical { } if (okHttpClient.isEmpty()) { - okHttpClient = buildTempHttpClient(); + okHttpClient = buildConnectivityCheckOkHttpClient(); } KeyStore caKeystore = optKey.get(); @@ -203,18 +202,7 @@ public class GraylogCertificateProvisioningPeriodical extends Periodical { Request request = new Request.Builder().url(node.getTransportAddress()).build(); if(okHttpClient.isPresent()) { OkHttpClient.Builder builder = okHttpClient.get().newBuilder(); - try { - URI uri = new URI(node.getTransportAddress()); - if (!isNullOrEmpty(uri.getUserInfo())) { - var list = Splitter.on(":").limit(2).splitToList(uri.getUserInfo()); - builder.authenticator((route, response) -> { - String credential = Credentials.basic(list.get(0), list.get(1)); - return response.request().newBuilder().header("Authorization", credential).build(); - }); - } - } catch (URISyntaxException ex) { - return false; - } + builder.authenticator((route, response) -> response.request().newBuilder().header("Authorization", jwtBearerToken).build()); Call call = builder.build().newCall(request); try(Response response = call.execute()) { return response.isSuccessful(); diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/ServerPreflightChecksModule.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/ServerPreflightChecksModule.java index 2e174ceaa4..0b0c83dbd0 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/ServerPreflightChecksModule.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/ServerPreflightChecksModule.java @@ -28,6 +28,8 @@ import org.graylog2.plugin.inject.Graylog2Module; import org.graylog2.plugin.system.FilePersistedNodeIdProvider; import org.graylog2.plugin.system.NodeId; import org.graylog2.security.CustomCAX509TrustManager; +import org.graylog2.security.IndexerJwtAuthToken; +import org.graylog2.security.IndexerJwtAuthTokenProvider; import org.graylog2.security.TrustManagerProvider; import org.graylog2.shared.bindings.providers.OkHttpClientProvider; import org.graylog2.storage.providers.ElasticsearchVersionProvider; @@ -44,8 +46,7 @@ public class ServerPreflightChecksModule extends Graylog2Module { .implement(TrustManager.class, CustomCAX509TrustManager.class) .build(TrustManagerProvider.class)); - bind(CustomCAX509TrustManager.class).asEagerSingleton(); - + bind(CustomCAX509TrustManager.class).asEagerSingleton(); bind(KeystoreContentMover.class).to(SinglePasswordKeystoreContentMover.class).asEagerSingleton(); bind(OkHttpClient.class).toProvider(OkHttpClientProvider.class).asEagerSingleton(); @@ -53,6 +54,7 @@ public class ServerPreflightChecksModule extends Graylog2Module { bind(NodeId.class).toProvider(FilePersistedNodeIdProvider.class).asEagerSingleton(); bind(AuditEventSender.class).to(NullAuditEventSender.class); + bind(String.class).annotatedWith(IndexerJwtAuthToken.class).toProvider(IndexerJwtAuthTokenProvider.class).asEagerSingleton(); // The MongoDBPreflightCheck is not registered here, because it is called separately from ServerBootstrap addPreflightCheck(SearchDbPreflightCheck.class); addPreflightCheck(DiskJournalPreflightCheck.class); diff --git a/graylog2-server/src/main/java/org/graylog2/configuration/ElasticsearchClientConfiguration.java b/graylog2-server/src/main/java/org/graylog2/configuration/ElasticsearchClientConfiguration.java index 2988a37038..fa7b7a5ca6 100644 --- a/graylog2-server/src/main/java/org/graylog2/configuration/ElasticsearchClientConfiguration.java +++ b/graylog2-server/src/main/java/org/graylog2/configuration/ElasticsearchClientConfiguration.java @@ -25,12 +25,10 @@ import org.graylog2.configuration.converters.URIListConverter; import org.graylog2.configuration.validators.ElasticsearchVersionValidator; import org.graylog2.configuration.validators.HttpOrHttpsSchemeValidator; import org.graylog2.configuration.validators.ListOfURIsWithHostAndSchemeValidator; -import org.graylog2.configuration.validators.NonEmptyListValidator; import org.graylog2.storage.SearchVersion; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public class ElasticsearchClientConfiguration { @@ -93,4 +91,13 @@ public class ElasticsearchClientConfiguration { @Parameter(value = "elasticsearch_mute_deprecation_warnings") private boolean muteDeprecationWarnings = false; + + @Parameter(value = "indexer_use_jwt_authentication") + boolean indexerUseJwtAuthentication = false; + + @Parameter(value = "indexer_jwt_auth_token_caching_duration") + Duration indexerJwtAuthTokenCachingDuration = Duration.seconds(60); + + @Parameter(value = "indexer_jwt_auth_token_expiration_duration") + Duration indexerJwtAuthTokenExpirationDuration = Duration.seconds(180); } diff --git a/graylog2-server/src/main/java/org/graylog2/configuration/IndexerDiscoveryModule.java b/graylog2-server/src/main/java/org/graylog2/configuration/IndexerDiscoveryModule.java index 8635e1bc2a..aff100da3b 100644 --- a/graylog2-server/src/main/java/org/graylog2/configuration/IndexerDiscoveryModule.java +++ b/graylog2-server/src/main/java/org/graylog2/configuration/IndexerDiscoveryModule.java @@ -32,6 +32,7 @@ public class IndexerDiscoveryModule extends AbstractModule { @Override protected void configure() { bind(new TypeLiteral>() {}).annotatedWith(IndexerHosts.class).toProvider(IndexerDiscoveryProvider.class).asEagerSingleton(); + bind(Boolean.class).annotatedWith(RunsWithDataNode.class).toProvider(RunsWithDataNodeDiscoveryProvider.class).asEagerSingleton(); bind(NodeService.class).to(NodeServiceImpl.class); bind(PreflightConfigService.class).to(PreflightConfigServiceImpl.class); bind(MongoConnection.class).toProvider(MongoConnectionProvider.class); diff --git a/graylog2-server/src/main/java/org/graylog2/configuration/RunsWithDataNode.java b/graylog2-server/src/main/java/org/graylog2/configuration/RunsWithDataNode.java new file mode 100644 index 0000000000..40447cd829 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog2/configuration/RunsWithDataNode.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.configuration; + +import javax.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.PARAMETER, ElementType.TYPE }) +@Qualifier +public @interface RunsWithDataNode { +} diff --git a/graylog2-server/src/main/java/org/graylog2/configuration/RunsWithDataNodeDiscoveryProvider.java b/graylog2-server/src/main/java/org/graylog2/configuration/RunsWithDataNodeDiscoveryProvider.java new file mode 100644 index 0000000000..b354a678d9 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog2/configuration/RunsWithDataNodeDiscoveryProvider.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.configuration; + +import com.google.common.base.Suppliers; +import org.graylog2.bootstrap.preflight.PreflightConfig; +import org.graylog2.bootstrap.preflight.PreflightConfigResult; +import org.graylog2.bootstrap.preflight.PreflightConfigService; +import org.graylog2.cluster.Node; +import org.graylog2.cluster.NodeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + + +public class RunsWithDataNodeDiscoveryProvider implements Provider { + + private static final Logger LOG = LoggerFactory.getLogger(RunsWithDataNodeDiscoveryProvider.class); + private final PreflightConfigService preflightConfigService; + private final NodeService nodeService; + + private final Supplier resultsCachingSupplier; + + @Inject + public RunsWithDataNodeDiscoveryProvider( + PreflightConfigService preflightConfigService, + NodeService nodeService) { + this.preflightConfigService = preflightConfigService; + this.nodeService = nodeService; + this.resultsCachingSupplier = Suppliers.memoize(this::doGet); + } + + @Override + public Boolean get() { + return resultsCachingSupplier.get(); + } + + private Boolean doGet() { + final PreflightConfigResult preflightResult = preflightConfigService.getPersistedConfig() + .map(PreflightConfig::result) + .orElse(PreflightConfigResult.UNKNOWN); + + // if preflight is finished, we assume that there will be some datanode registered via node-service. + if (preflightResult == PreflightConfigResult.FINISHED) { + final List discovered = discover(); + if (!discovered.isEmpty()) { + LOG.debug("Running with DataNode(s)."); + return Boolean.TRUE; + } + } + LOG.debug("Not running with DataNode(s)."); + return Boolean.FALSE; + } + + private List discover() { + return nodeService.allActive(Node.Type.DATANODE).values().stream() + .map(Node::getTransportAddress) + .map(URI::create) + .collect(Collectors.toList()); + } +} diff --git a/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthToken.java b/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthToken.java new file mode 100644 index 0000000000..1361868f10 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthToken.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.security; + +import javax.inject.Qualifier; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.PARAMETER, ElementType.TYPE }) +@Qualifier +public @interface IndexerJwtAuthToken { +} diff --git a/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthTokenProvider.java b/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthTokenProvider.java new file mode 100644 index 0000000000..ee54b8705d --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog2/security/IndexerJwtAuthTokenProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.security; + +import com.google.common.base.Suppliers; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import javax.crypto.spec.SecretKeySpec; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import com.github.joschi.jadconfig.util.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@Singleton +public class IndexerJwtAuthTokenProvider implements Provider { + private final Supplier authHeaderBearerString; + + private static final Logger LOG = LoggerFactory.getLogger(IndexerJwtAuthTokenProvider.class); + + @Inject + public IndexerJwtAuthTokenProvider(@Named("password_secret") String signingKey, + @Named("indexer_jwt_auth_token_expiration_duration") final Duration tokenExpirationDuration, + @Named("indexer_jwt_auth_token_caching_duration") final Duration cachingDuration) { + authHeaderBearerString = Suppliers.memoizeWithExpiration(() -> { + LOG.debug("Creating new JWT token, expiration set to {}", tokenExpirationDuration); + return "Bearer " + createToken(signingKey.getBytes(StandardCharsets.UTF_8), tokenExpirationDuration); + }, cachingDuration.toSeconds(), TimeUnit.SECONDS); + } + + public static String createToken(final byte[] apiKeySecretBytes, final Duration tokenExpirationDuration) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + long nowMillis = System.currentTimeMillis(); + Date now = new Date(nowMillis); + + Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); + + JwtBuilder builder = Jwts.builder().setId("graylog datanode connect " + nowMillis) + .addClaims(Map.of("os_roles", "admin")) + .setIssuedAt(now) + .setSubject("admin") + .setIssuer("graylog") + .setNotBefore(now) + .setExpiration(new Date(nowMillis + tokenExpirationDuration.toMilliseconds())) + .signWith(signingKey, signatureAlgorithm); + + return builder.compact(); + } + + @Override + public String get() { + return authHeaderBearerString.get(); + } +} diff --git a/graylog2-server/src/main/java/org/graylog2/shared/security/SecurityBindings.java b/graylog2-server/src/main/java/org/graylog2/shared/security/SecurityBindings.java index 8fa1c85a81..d76d41ebfb 100644 --- a/graylog2-server/src/main/java/org/graylog2/shared/security/SecurityBindings.java +++ b/graylog2-server/src/main/java/org/graylog2/shared/security/SecurityBindings.java @@ -26,6 +26,8 @@ import org.graylog2.rest.models.system.sessions.responses.DefaultSessionResponse import org.graylog2.rest.models.system.sessions.responses.SessionResponseFactory; import org.graylog2.security.CustomCAX509TrustManager; import org.graylog2.security.DefaultX509TrustManager; +import org.graylog2.security.IndexerJwtAuthToken; +import org.graylog2.security.IndexerJwtAuthTokenProvider; import org.graylog2.security.TrustManagerProvider; import org.graylog2.security.UserSessionTerminationService; import org.graylog2.security.encryption.EncryptedValueService; @@ -46,6 +48,7 @@ public class SecurityBindings extends PluginModule { .implement(TrustManager.class, DefaultX509TrustManager.class) .build(TrustManagerProvider.class)); bind(CustomCAX509TrustManager.class).asEagerSingleton(); + bind(String.class).annotatedWith(IndexerJwtAuthToken.class).toProvider(IndexerJwtAuthTokenProvider.class).asEagerSingleton(); OptionalBinder.newOptionalBinder(binder(), ActorAwareAuthenticationTokenFactory.class) .setDefault().to(ActorAwareUsernamePasswordTokenFactory.class); diff --git a/graylog2-server/src/main/java/org/graylog2/storage/versionprobe/VersionProbe.java b/graylog2-server/src/main/java/org/graylog2/storage/versionprobe/VersionProbe.java index 2b0ab1e95e..2bc85ea694 100644 --- a/graylog2-server/src/main/java/org/graylog2/storage/versionprobe/VersionProbe.java +++ b/graylog2-server/src/main/java/org/graylog2/storage/versionprobe/VersionProbe.java @@ -29,6 +29,8 @@ import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.ResponseBody; +import org.graylog2.configuration.RunsWithDataNode; +import org.graylog2.security.IndexerJwtAuthToken; import org.graylog2.shared.utilities.ExceptionUtils; import org.graylog2.storage.SearchVersion; import org.slf4j.Logger; @@ -55,16 +57,23 @@ public class VersionProbe { private final OkHttpClient okHttpClient; private final int connectionAttempts; private final Duration delayBetweenAttempts; + private final boolean isJwtAuthentication; + private final String jwtBearerToken; @Inject public VersionProbe(ObjectMapper objectMapper, OkHttpClient okHttpClient, @Named("elasticsearch_version_probe_attempts") int elasticsearchVersionProbeAttempts, - @Named("elasticsearch_version_probe_delay") Duration elasticsearchVersionProbeDelay) { + @Named("elasticsearch_version_probe_delay") Duration elasticsearchVersionProbeDelay, + @RunsWithDataNode Boolean runsWithDataNode, + @Named("indexer_use_jwt_authentication") boolean opensearchUseJwtAuthentication, + @IndexerJwtAuthToken String jwtBearerToken) { this.objectMapper = objectMapper; this.okHttpClient = okHttpClient; this.connectionAttempts = elasticsearchVersionProbeAttempts; this.delayBetweenAttempts = elasticsearchVersionProbeDelay; + this.isJwtAuthentication = runsWithDataNode || opensearchUseJwtAuthentication; + this.jwtBearerToken = jwtBearerToken; } public Optional probe(final Collection hosts) { @@ -83,9 +92,9 @@ public class VersionProbe { } } if (connectionAttempts == 0) { - LOG.info("Elasticsearch is not available. Retry #{}", attempt.getAttemptNumber()); + LOG.info("OpenSearch/Elasticsearch is not available. Retry #{}", attempt.getAttemptNumber()); } else { - LOG.info("Elasticsearch is not available. Retry #{}/{}", attempt.getAttemptNumber(), connectionAttempts); + LOG.info("OpenSearch/Elasticsearch is not available. Retry #{}/{}", attempt.getAttemptNumber(), connectionAttempts); } } }) @@ -93,7 +102,7 @@ public class VersionProbe { .withStopStrategy((connectionAttempts == 0) ? StopStrategies.neverStop() : StopStrategies.stopAfterAttempt(connectionAttempts)) .build().call(() -> this.probeAllHosts(hosts)); } catch (ExecutionException | RetryException e) { - LOG.error("Unable to retrieve version from Elasticsearch node: ", e); + LOG.error("Unable to retrieve version from OpenSearch/Elasticsearch node: ", e); } return Optional.empty(); } @@ -126,9 +135,9 @@ public class VersionProbe { final Consumer errorLogger = (responseBody) -> { try { final ErrorResponse errorResponse = errorResponseConverter.convert(responseBody); - LOG.error("Unable to retrieve version from Elasticsearch node {}:{}: {}", host.getHost(), host.getPort(), errorResponse); + LOG.error("Unable to retrieve version from OpenSearch/Elasticsearch node {}:{}: {}", host.getHost(), host.getPort(), errorResponse); } catch (IOException e) { - LOG.error("Unable to retrieve version from Elasticsearch node {}:{}: unknown error - an exception occurred while deserializing error response: {}", host.getHost(), host.getPort(), e); + LOG.error("Unable to retrieve version from OpenSearch/Elasticsearch node {}:{}: unknown error - an exception occurred while deserializing error response: {}", host.getHost(), host.getPort(), e); } }; @@ -138,17 +147,27 @@ public class VersionProbe { .flatMap(this::parseVersion); } - private OkHttpClient addAuthenticationIfPresent(URI host, OkHttpClient okHttpClient) { - if (Strings.emptyToNull(host.getUserInfo()) != null) { + private Optional getAuthToken(final URI host, final boolean useJWT) { + if(useJWT) { + return Optional.of(jwtBearerToken); + } else if (Strings.emptyToNull(host.getUserInfo()) != null) { final String[] credentials = host.getUserInfo().split(":"); final String username = credentials[0]; final String password = credentials[1]; - final String authToken = Credentials.basic(username, password); + return Optional.of(Credentials.basic(username, password)); + } + return Optional.empty(); + } + + private OkHttpClient addAuthenticationIfPresent(URI host, OkHttpClient okHttpClient) { + final Optional authToken = getAuthToken(host, isJwtAuthentication); + + if(authToken.isPresent()) { return okHttpClient.newBuilder() .addInterceptor(chain -> { final Request originalRequest = chain.request(); - final Request.Builder builder = originalRequest.newBuilder().header("Authorization", authToken); + final Request.Builder builder = originalRequest.newBuilder().header("Authorization", authToken.get()); final Request newRequest = builder.build(); return chain.proceed(newRequest); }) diff --git a/graylog2-server/src/test/java/org/graylog/testing/completebackend/ContainerizedGraylogBackend.java b/graylog2-server/src/test/java/org/graylog/testing/completebackend/ContainerizedGraylogBackend.java index 6df1b05a81..c50b13dc7b 100644 --- a/graylog2-server/src/test/java/org/graylog/testing/completebackend/ContainerizedGraylogBackend.java +++ b/graylog2-server/src/test/java/org/graylog/testing/completebackend/ContainerizedGraylogBackend.java @@ -16,11 +16,9 @@ */ package org.graylog.testing.completebackend; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.apache.commons.lang3.StringUtils; import org.graylog.testing.containermatrix.MongodbServer; import org.graylog.testing.elasticsearch.SearchServerInstance; -import org.graylog.testing.graylognode.ExecutableNotFoundException; import org.graylog.testing.graylognode.NodeInstance; import org.graylog.testing.mongodb.MongoDBInstance; import org.graylog2.storage.SearchVersion; @@ -33,10 +31,6 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.ServiceLoader; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.stream.Collectors; public class ContainerizedGraylogBackend implements GraylogBackend, AutoCloseable { @@ -47,23 +41,30 @@ public class ContainerizedGraylogBackend implements GraylogBackend, AutoCloseabl private MailServerContainer emailServerInstance; private NodeInstance node; - private ContainerizedGraylogBackend() { + private ContainerizedGraylogBackend() {} + + public synchronized static ContainerizedGraylogBackend createStarted(final SearchVersion version, + final MongodbServer mongodbVersion, + final int[] extraPorts, + final List mongoDBFixtures, + final PluginJarsProvider pluginJarsProvider, + final MavenProjectDirProvider mavenProjectDirProvider, + final List enabledFeatureFlags, + final boolean preImportLicense, + final boolean withMailServerEnabled) { + + return new ContainerizedGraylogBackend().create(version, mongodbVersion, extraPorts, mongoDBFixtures, pluginJarsProvider, mavenProjectDirProvider, enabledFeatureFlags, preImportLicense, withMailServerEnabled); } - public static ContainerizedGraylogBackend createStarted(SearchVersion version, MongodbServer mongodbVersion, - int[] extraPorts, List mongoDBFixtures, - PluginJarsProvider pluginJarsProvider, MavenProjectDirProvider mavenProjectDirProvider, - final List enabledFeatureFlags, boolean preImportLicense, boolean withMailServerEnabled) { - - final ContainerizedGraylogBackend backend = new ContainerizedGraylogBackend(); - backend.create(version, mongodbVersion, extraPorts, mongoDBFixtures, pluginJarsProvider, mavenProjectDirProvider, enabledFeatureFlags, preImportLicense, withMailServerEnabled); - return backend; - } - - private void create(final SearchVersion version, MongodbServer mongodbVersion, - int[] extraPorts, List mongoDBFixtures, - PluginJarsProvider pluginJarsProvider, MavenProjectDirProvider mavenProjectDirProvider, - List enabledFeatureFlags, boolean preImportLicense, boolean withMailServerEnabled) { + private ContainerizedGraylogBackend create(final SearchVersion version, + final MongodbServer mongodbVersion, + final int[] extraPorts, + final List mongoDBFixtures, + final PluginJarsProvider pluginJarsProvider, + final MavenProjectDirProvider mavenProjectDirProvider, + final List enabledFeatureFlags, + final boolean preImportLicense, + final boolean withMailServerEnabled) { final var network = Network.newNetwork(); final var builder = SearchServerInstanceProvider.getBuilderFor(version).orElseThrow(() -> new UnsupportedOperationException("Search version " + version + " not supported.")); @@ -103,6 +104,7 @@ public class ContainerizedGraylogBackend implements GraylogBackend, AutoCloseabl LOG.error("------------------------------ Search Server logs: --------------------------------------\n{}", searchServer.getLogs()); throw ex; } + return this; } private void createLicenses(final MongoDBInstance mongoDBInstance, final String... licenseStrs) { diff --git a/graylog2-server/src/test/java/org/graylog/testing/graylognode/NodeContainerFactory.java b/graylog2-server/src/test/java/org/graylog/testing/graylognode/NodeContainerFactory.java index e6be7d8de7..64eb22a19e 100644 --- a/graylog2-server/src/test/java/org/graylog/testing/graylognode/NodeContainerFactory.java +++ b/graylog2-server/src/test/java/org/graylog/testing/graylognode/NodeContainerFactory.java @@ -127,7 +127,7 @@ public class NodeContainerFactory { .withEnv("GRAYLOG_ELASTICSEARCH_HOSTS", config.elasticsearchUri) // TODO: should we set this override search version or let graylog server to detect it from the search server itself? .withEnv("GRAYLOG_ELASTICSEARCH_VERSION", config.elasticsearchVersion.encode()) - .withEnv("GRAYLOG_PASSWORD_SECRET", "M4lteserKreuzHerrStrack?") + .withEnv("GRAYLOG_PASSWORD_SECRET", "M4lteserKreuzHerrStrack?-warZuKurzDeshalbMussdaNochWasdran") .withEnv("GRAYLOG_NODE_ID_FILE", "data/config/node-id") .withEnv("GRAYLOG_HTTP_BIND_ADDRESS", "0.0.0.0:" + API_PORT) .withEnv("GRAYLOG_ROOT_PASSWORD_SHA2", ADMIN_PW_SHA2) diff --git a/pom.xml b/pom.xml index 5240c75421..5fd12a6751 100644 --- a/pom.xml +++ b/pom.xml @@ -180,6 +180,7 @@ 9.1.6 0.3.2 3.6 + 0.11.5 2.0.0-M24 @@ -316,6 +317,21 @@ joda-time ${joda-time.version} + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} +