diff --git a/changelog/unreleased/issue-16447.toml b/changelog/unreleased/issue-16447.toml new file mode 100644 index 0000000000..778be16fb8 --- /dev/null +++ b/changelog/unreleased/issue-16447.toml @@ -0,0 +1,5 @@ +type = "a" +message = "Enable basic auth for datanode rest API if security is enabled" + +issues = ["16447"] +pulls = ["16446"] 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 21347106a3..84ea6aa73c 100644 --- a/data-node/src/main/java/org/graylog/datanode/Configuration.java +++ b/data-node/src/main/java/org/graylog/datanode/Configuration.java @@ -133,6 +133,9 @@ public class Configuration extends BaseConfiguration { @Parameter(value = "stale_leader_timeout", validators = PositiveIntegerValidator.class) private Integer staleLeaderTimeout = 2000; + @Parameter(value = "root_password_sha2") + private String rootPasswordSha2; + @Parameter(value = "user_password_default_algorithm") private String userPasswordDefaultAlgorithm = "bcrypt"; @@ -577,4 +580,8 @@ public class Configuration extends BaseConfiguration { public String getHostname() { return hostname; } + + public String getRootPasswordSha2() { + return rootPasswordSha2; + } } diff --git a/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java b/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java index de527ed644..55dbfefcbb 100644 --- a/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java +++ b/data-node/src/main/java/org/graylog/datanode/initializers/JerseyService.java @@ -43,6 +43,7 @@ import org.graylog.datanode.configuration.variants.OpensearchSecurityConfigurati import org.graylog.datanode.management.OpensearchConfigurationChangeEvent; import org.graylog.datanode.process.OpensearchConfiguration; import org.graylog.security.certutil.CertConstants; +import org.graylog2.bootstrap.preflight.web.BasicAuthFilter; import org.graylog2.configuration.TLSProtocolsConfiguration; import org.graylog2.plugin.inject.Graylog2Module; import org.graylog2.rest.MoreMediaTypes; @@ -223,11 +224,17 @@ public class JerseyService extends AbstractIdleService { int maxHeaderSize, boolean enableGzip, Set additionalResources) { + final boolean isSecuredInstance = sslEngineConfigurator != null; final ResourceConfig resourceConfig = buildResourceConfig(additionalResources); + + if(isSecuredInstance) { + resourceConfig.register(new BasicAuthFilter(configuration.getRootUsername(), configuration.getRootPasswordSha2(), "Datanode")); + } + final HttpServer httpServer = GrizzlyHttpServerFactory.createHttpServer( listenUri, resourceConfig, - sslEngineConfigurator != null, + isSecuredInstance, sslEngineConfigurator, false); diff --git a/data-node/src/test/java/org/graylog/datanode/DatanodeRestApiWait.java b/data-node/src/test/java/org/graylog/datanode/DatanodeRestApiWait.java new file mode 100644 index 0000000000..23c63092cc --- /dev/null +++ b/data-node/src/test/java/org/graylog/datanode/DatanodeRestApiWait.java @@ -0,0 +1,160 @@ +/* + * 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.graylog.datanode; + +import com.github.rholder.retry.Attempt; +import com.github.rholder.retry.RetryException; +import com.github.rholder.retry.RetryListener; +import com.github.rholder.retry.Retryer; +import com.github.rholder.retry.RetryerBuilder; +import com.github.rholder.retry.StopStrategies; +import com.github.rholder.retry.WaitStrategies; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import io.restassured.response.ValidatableResponse; +import io.restassured.specification.RequestSpecification; +import org.apache.http.NoHttpResponseException; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +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; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +public class DatanodeRestApiWait { + + private static final Logger LOG = LoggerFactory.getLogger(DatanodeRestApiWait.class); + private static final int ATTEMPTS_COUNT = 160; + + private final int datanodePort; + private KeyStore truststore; + private String restApiUsername; + private String restApiPassword; + private String lastRecordedResponse; + + private DatanodeRestApiWait(int opensearchPort) { + this.datanodePort = opensearchPort; + } + + public static DatanodeRestApiWait onPort(int datanodePort) { + return new DatanodeRestApiWait(datanodePort); + } + + public DatanodeRestApiWait withTruststore(KeyStore truststore) { + this.truststore = truststore; + return this; + } + + public DatanodeRestApiWait withBasicAuth(String username, String password) { + this.restApiUsername = username; + this.restApiPassword = password; + return this; + } + + public ValidatableResponse waitForAvailableStatus() throws ExecutionException, RetryException { + return waitForDatanode( + input -> !input.extract().body().path("opensearch.node.state").equals("AVAILABLE") + ); + } + + + @SafeVarargs + private ValidatableResponse waitForDatanode(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)) + .withStopStrategy(StopStrategies.stopAfterAttempt(ATTEMPTS_COUNT)) + .retryIfException(input -> input instanceof NoHttpResponseException) + .retryIfException(input -> input instanceof SocketException) + .retryIfException(input -> input instanceof SSLHandshakeException) // may happen before SSL is configured properly + .retryIfResult(input -> !input.extract().contentType().startsWith("application/json")) + .withRetryListener(new RetryListener() { + @Override + public void onRetry(Attempt attempt) { + if (attempt.hasResult() && attempt.getResult() instanceof ValidatableResponse response) { + lastRecordedResponse = serializeResponse(response.extract()); + } else if (attempt.getExceptionCause() != null) { + lastRecordedResponse = attempt.getExceptionCause().getMessage(); + } + } + }); + + for (Predicate predicate : predicates) { + builder.retryIfResult(predicate::test); + } + + final Retryer retryer = builder + .build(); + try { + return retryer.call(() -> { + final RequestSpecification req = RestAssured.given() + .accept(ContentType.JSON); + + Optional.ofNullable(truststore).ifPresent(ts -> req.trustStore(truststore)); + + if (restApiUsername != null && restApiPassword != null) { + req.auth().basic(restApiUsername, restApiPassword); + } + + return req.get(formatDatanodeRestUrl()) + .then(); + }); + } catch (Exception e) { + if (lastRecordedResponse != null) { + LOG.warn("Last recorded opensearch response, waiting for {}: {}", formatDatanodeRestUrl(), lastRecordedResponse); + } + throw e; + } + } + + private String formatCurlConnectionInfo() { + List builder = new LinkedList<>(); + builder.add("curl -k"); // trust self-signed certs + if(restApiUsername != null && restApiPassword != null) { + builder.add("-u \"" + restApiUsername + ":" + restApiPassword + "\""); + } + builder.add(formatDatanodeRestUrl()); + return String.join(" ", builder); + } + + @NotNull + private String formatDatanodeRestUrl() { + return "https://localhost:" + datanodePort; + } + + private String serializeResponse(ExtractableResponse extract) { + final String body = extract.body().asString(); + final String contentType = extract.contentType(); + final String headers = extract.headers().toString(); + return String.format(Locale.ROOT, "\nContent-type: %s\nheaders: %s\nbody: %s", contentType, headers, body); + } +} 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 733b6b0a45..209b606056 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 @@ -54,8 +54,6 @@ public class DatanodeClusterIT { static Path tempDir; private String hostnameNodeA; private KeyStore trustStore; - private String initialAdminUsername; - private String initialAdminPassword; private KeystoreInformation ca; private Network network; private MongoDBTestService mongoDBTestService; @@ -71,8 +69,6 @@ public class DatanodeClusterIT { hostnameNodeA = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); final KeystoreInformation transportNodeA = DatanodeSecurityTestUtils.generateTransportCert(tempDir, ca, hostnameNodeA); final KeystoreInformation httpNodeA = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, hostnameNodeA); - initialAdminUsername = RandomStringUtils.randomAlphabetic(10); - initialAdminPassword = RandomStringUtils.randomAlphabetic(10); this.network = Network.newNetwork(); this.mongoDBTestService = MongoDBTestService.create(MongodbServer.MONGO5, network); @@ -83,9 +79,7 @@ public class DatanodeClusterIT { mongoDBTestService, hostnameNodeA, transportNodeA, - httpNodeA, - initialAdminUsername, - initialAdminPassword + httpNodeA ); @@ -98,9 +92,7 @@ public class DatanodeClusterIT { mongoDBTestService, hostnameNodeB, transportNodeB, - httpNodeB, - initialAdminUsername, - initialAdminPassword + httpNodeB ); Stream.of(nodeA, nodeB).parallel().forEach(DatanodeContainerizedBackend::start); @@ -130,9 +122,7 @@ public class DatanodeClusterIT { network, mongoDBTestService, hostnameNodeC, transportNodeC, - httpNodeC, - initialAdminUsername, - initialAdminPassword + httpNodeC ); nodeC.start(); @@ -146,9 +136,7 @@ public class DatanodeClusterIT { MongoDBTestService mongodb, String hostname, KeystoreInformation transportKeystore, - KeystoreInformation httpKeystore, - String restUsername, - String restPassword) { + KeystoreInformation httpKeystore) { return new DatanodeContainerizedBackend( network, mongodb, @@ -171,10 +159,6 @@ public class DatanodeClusterIT { datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_CERTIFICATE", "datanode-https-certificates.p12"); datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_CERTIFICATE_PASSWORD", httpKeystore.passwordAsString()); - // configure initial admin username and password for Opensearch REST - datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_USERNAME", restUsername); - datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_PASSWORD", restPassword); - // this is the interface that we bind opensearch to. It must be 0.0.0.0 if we want // to be able to reach opensearch from outside the container and docker network (true?) datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_BIND_ADDRESS", "0.0.0.0"); 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 8d8da14106..91729ea593 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 @@ -18,19 +18,13 @@ 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; -import com.github.rholder.retry.StopStrategies; -import com.github.rholder.retry.WaitStrategies; -import io.restassured.RestAssured; import io.restassured.response.ValidatableResponse; import org.apache.commons.lang3.RandomStringUtils; -import org.apache.http.NoHttpResponseException; +import org.graylog.datanode.DatanodeRestApiWait; 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; import org.junit.jupiter.api.BeforeEach; @@ -39,18 +33,16 @@ import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; 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; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import static io.restassured.RestAssured.given; import static org.graylog.datanode.testinfra.DatanodeContainerizedBackend.IMAGE_WORKING_DIR; +import static org.graylog.testing.completebackend.ContainerizedGraylogBackend.ROOT_PASSWORD_PLAINTEXT; public class DatanodeSecuritySetupIT { private static final Logger LOG = LoggerFactory.getLogger(DatanodeSecuritySetupIT.class); @@ -58,14 +50,15 @@ public class DatanodeSecuritySetupIT { @TempDir static Path tempDir; private DatanodeContainerizedBackend backend; - private String httpUsername; - private String httpPassword; private KeyStore trustStore; private String containerHostname; + private String restAdminUsername; @BeforeEach void setUp() throws IOException, GeneralSecurityException { + restAdminUsername = RandomStringUtils.randomAlphanumeric(10); + containerHostname = "graylog-datanode-host-" + RandomStringUtils.random(8, "0123456789abcdef"); // first generate a self-signed CA KeystoreInformation ca = DatanodeSecurityTestUtils.generateCa(tempDir); @@ -76,9 +69,6 @@ public class DatanodeSecuritySetupIT { // use the CA to generate HTTP certificate keystore final KeystoreInformation httpCert = DatanodeSecurityTestUtils.generateHttpCert(tempDir, ca, containerHostname, Tools.getLocalCanonicalHostname()); - httpUsername = RandomStringUtils.randomAlphabetic(10); - httpPassword = RandomStringUtils.randomAlphabetic(10); - backend = new DatanodeContainerizedBackend(containerHostname, datanodeContainer -> { // provide the keystore files to the docker container datanodeContainer.withFileSystemBind(transportCert.location().toAbsolutePath().toString(), IMAGE_WORKING_DIR + "/config/datanode-transport-certificates.p12"); @@ -88,16 +78,11 @@ 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"); datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_CERTIFICATE_PASSWORD", httpCert.passwordAsString()); - // configure initial admin username and password for Opensearch REST - datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_USERNAME", httpUsername); - datanodeContainer.withEnv("GRAYLOG_DATANODE_REST_API_PASSWORD", httpPassword); - // this is the interface that we bind opensearch to. It must be 0.0.0.0 if we want // to be able to reach opensearch from outside the container and docker network (true?) datanodeContainer.withEnv("GRAYLOG_DATANODE_HTTP_BIND_ADDRESS", "0.0.0.0"); @@ -108,6 +93,8 @@ public class DatanodeSecuritySetupIT { datanodeContainer.withEnv("GRAYLOG_DATANODE_HOSTNAME", containerHostname); datanodeContainer.withEnv("GRAYLOG_DATANODE_SINGLE_NODE_ONLY", "true"); + + datanodeContainer.withEnv("GRAYLOG_DATANODE_ROOT_USERNAME", restAdminUsername); }).start(); } @@ -120,7 +107,7 @@ public class DatanodeSecuritySetupIT { void testSecuredSetup() throws ExecutionException, RetryException { final String jwtAuthToken = IndexerJwtAuthTokenProvider.createToken(DatanodeContainerizedBackend.SIGNING_SECRET.getBytes(StandardCharsets.UTF_8), Duration.seconds(120)); - waitForOpensearchAvailableStatus(backend.getDatanodeRestPort(), backend.getOpensearchRestPort(), trustStore); + waitForOpensearchAvailableStatus(backend.getDatanodeRestPort(), trustStore, restAdminUsername, ROOT_PASSWORD_PLAINTEXT); try { given().header( "Authorization", "Bearer " + jwtAuthToken) @@ -135,27 +122,13 @@ public class DatanodeSecuritySetupIT { } } - 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)) - .retryIfException(input -> input instanceof NoHttpResponseException) - .retryIfException(input -> input instanceof SocketException) - .retryIfException(input -> input instanceof SSLHandshakeException) // may happen before SSL is configured properly - .retryIfResult(input -> !input.extract().contentType().startsWith("application/json")) - .retryIfResult(input -> !input.extract().body().path("opensearch.node.state").equals("AVAILABLE")) - .build(); + private ValidatableResponse waitForOpensearchAvailableStatus(final Integer datanodeRestPort, final KeyStore trustStore, String authUsername, String authPassword) throws ExecutionException, RetryException { try { - var hostname = Tools.getLocalCanonicalHostname(); - 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(rest) - .then()); + return DatanodeRestApiWait.onPort(datanodeRestPort) + .withTruststore(trustStore) + .withBasicAuth(authUsername, authPassword) + .waitForAvailableStatus(); } catch (Exception ex) { LOG.error("Error starting the DataNode, showing logs:\n" + backend.getLogs()); throw ex; 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 b491251803..97b64bc26c 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 @@ -16,28 +16,14 @@ */ package org.graylog.datanode.testinfra; -import com.google.common.base.Suppliers; -import org.graylog.datanode.OpensearchDistribution; import org.graylog.testing.completebackend.ContainerizedGraylogBackend; import org.graylog.testing.completebackend.DefaultMavenProjectDirProvider; -import org.graylog.testing.completebackend.DefaultPluginJarsProvider; import org.graylog.testing.containermatrix.MongodbServer; import org.graylog.testing.datanode.DatanodeDockerHooks; import org.graylog.testing.graylognode.MavenPackager; -import org.graylog.testing.graylognode.NodeContainerConfig; import org.graylog.testing.mongodb.MongoDBTestService; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; -import org.testcontainers.images.builder.ImageFromDockerfile; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.util.Collections; -import java.util.Properties; -import java.util.function.Supplier; public class DatanodeContainerizedBackend { public static final String IMAGE_WORKING_DIR = "/usr/share/graylog/datanode"; diff --git a/data-node/src/test/java/org/graylog/datanode/testinfra/DatanodeDevContainerBuilder.java b/data-node/src/test/java/org/graylog/datanode/testinfra/DatanodeDevContainerBuilder.java index 1d18798af2..1d57428052 100644 --- a/data-node/src/test/java/org/graylog/datanode/testinfra/DatanodeDevContainerBuilder.java +++ b/data-node/src/test/java/org/graylog/datanode/testinfra/DatanodeDevContainerBuilder.java @@ -155,6 +155,7 @@ public class DatanodeDevContainerBuilder implements org.graylog.testing.datanode .withEnv("GRAYLOG_DATANODE_REST_API_USERNAME", "admin") .withEnv("GRAYLOG_DATANODE_REST_API_PASSWORD", "admin") .withEnv("GRAYLOG_DATANODE_PASSWORD_SECRET", passwordSecret) + .withEnv("GRAYLOG_DATANODE_ROOT_PASSWORD_SHA2", rootPasswordSha2) .withEnv("GRAYLOG_DATANODE_NODE_ID_FILE", "./node-id") .withEnv("GRAYLOG_DATANODE_HTTP_BIND_ADDRESS", "0.0.0.0:" + restPort) diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightJerseyService.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightJerseyService.java index 0d397b64ab..757f764d1a 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightJerseyService.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/PreflightJerseyService.java @@ -34,7 +34,7 @@ import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.server.model.Resource; -import org.graylog2.bootstrap.preflight.web.PreflightAuthFilter; +import org.graylog2.bootstrap.preflight.web.BasicAuthFilter; import org.graylog2.configuration.HttpConfiguration; import org.graylog2.rest.MoreMediaTypes; import org.graylog2.shared.rest.exceptionmappers.JacksonPropertyExceptionMapper; @@ -60,7 +60,7 @@ public class PreflightJerseyService extends AbstractIdleService { private static final Logger LOG = LoggerFactory.getLogger(PreflightJerseyService.class); private final HttpConfiguration configuration; - private final PreflightAuthFilter preflightAuthFilter; + private final BasicAuthFilter basicAuthFilter; private final Set> systemRestResources; private final ObjectMapper objectMapper; @@ -70,12 +70,12 @@ public class PreflightJerseyService extends AbstractIdleService { @Inject public PreflightJerseyService(final HttpConfiguration configuration, - final PreflightAuthFilter preflightAuthFilter, + final BasicAuthFilter basicAuthFilter, @PreflightRestResourcesBinding final Set> systemRestResources, ObjectMapper objectMapper, MetricRegistry metricRegistry) { this.configuration = requireNonNull(configuration, "configuration"); - this.preflightAuthFilter = requireNonNull(preflightAuthFilter, "preflightAuthFilter"); + this.basicAuthFilter = requireNonNull(basicAuthFilter, "preflightAuthFilter"); this.systemRestResources = systemRestResources; this.objectMapper = requireNonNull(objectMapper, "objectMapper"); this.metricRegistry = requireNonNull(metricRegistry, "metricRegistry"); @@ -152,7 +152,7 @@ public class PreflightJerseyService extends AbstractIdleService { .register(MultiPartFeature.class) .registerClasses(systemRestResources) .registerResources(additionalResources) - .register(preflightAuthFilter); + .register(basicAuthFilter); return rc; } diff --git a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/PreflightAuthFilter.java b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/BasicAuthFilter.java similarity index 76% rename from graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/PreflightAuthFilter.java rename to graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/BasicAuthFilter.java index 4294b6419e..eb9589fa69 100644 --- a/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/PreflightAuthFilter.java +++ b/graylog2-server/src/main/java/org/graylog2/bootstrap/preflight/web/BasicAuthFilter.java @@ -23,6 +23,7 @@ import org.graylog2.Configuration; import javax.inject.Inject; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import java.io.IOException; @@ -30,17 +31,25 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.StringTokenizer; -public class PreflightAuthFilter implements ContainerRequestFilter { +public class BasicAuthFilter implements ContainerRequestFilter { private static final String AUTHORIZATION_PROPERTY = "Authorization"; private static final String AUTHENTICATION_SCHEME = "Basic"; private final String adminUsername; private final String adminPasswordHash; + private final String realm; @Inject - public PreflightAuthFilter(Configuration configuration) { + public BasicAuthFilter(Configuration configuration) { this.adminUsername = configuration.getRootUsername(); this.adminPasswordHash = configuration.getRootPasswordSha2(); + this.realm = "preflight-config"; + } + + public BasicAuthFilter(String adminUsername, String adminPasswordHash, String realm) { + this.adminUsername = adminUsername; + this.adminPasswordHash = adminPasswordHash; + this.realm = realm; } @Override @@ -51,8 +60,9 @@ public class PreflightAuthFilter implements ContainerRequestFilter { if (authorization == null || authorization.isEmpty()) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED) - .entity("You cannot access this resource") - .header("WWW-Authenticate", "Basic realm=preflight-config") + .entity("You cannot access this resource, missing authorization header!") + .type(MediaType.TEXT_PLAIN_TYPE) + .header("WWW-Authenticate", "Basic realm=" + this.realm) .build()); return; } @@ -67,8 +77,9 @@ public class PreflightAuthFilter implements ContainerRequestFilter { if (!isUserMatching(username, password)) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED) - .entity("You cannot access this resource") - .header("WWW-Authenticate", "Basic realm=preflight-config") + .entity("You cannot access this resource, invalid username/password combination!") + .type(MediaType.TEXT_PLAIN_TYPE) + .header("WWW-Authenticate", "Basic realm=" + this.realm) .build()); } } 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 e4abe4785c..9a1beda6af 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,6 +16,7 @@ */ package org.graylog.testing.completebackend; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.graylog.testing.containermatrix.MongodbServer; import org.graylog.testing.elasticsearch.SearchServerInstance; @@ -38,7 +39,8 @@ import java.util.stream.Collectors; public class ContainerizedGraylogBackend implements GraylogBackend, AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(GraylogBackend.class); public static final String PASSWORD_SECRET = "M4lteserKreuzHerrStrack?-warZuKurzDeshalbMussdaNochWasdran"; - public static final String ROOT_PASSWORD_SHA_2 = "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"; + public static final String ROOT_PASSWORD_PLAINTEXT = "admin"; + public static final String ROOT_PASSWORD_SHA_2 = DigestUtils.sha256Hex(ROOT_PASSWORD_PLAINTEXT); private Network network; private SearchServerInstance searchServer;