diff --git a/solr/ui/src/commonMain/composeResources/values/strings.xml b/solr/ui/src/commonMain/composeResources/values/strings.xml index 6b4a6841c74..42becfb8734 100644 --- a/solr/ui/src/commonMain/composeResources/values/strings.xml +++ b/solr/ui/src/commonMain/composeResources/values/strings.xml @@ -30,6 +30,7 @@ To get started, please provide a Solr host URL: The Solr instance %1$s has enabled authentication. + To authenticate you may use your credentials for Basic authentication. To authenticate you may use your credentials for the realm %1$s. diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/auth/BasicAuthComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/auth/BasicAuthComponent.kt index c18c2e48920..c243ad8ccbb 100644 --- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/auth/BasicAuthComponent.kt +++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/auth/BasicAuthComponent.kt @@ -52,7 +52,7 @@ interface BasicAuthComponent { * does not distinguish input fields. */ data class Model( - val realm: String = "", + val realm: String? = null, val username: String = "", val password: String = "", val hasError: Boolean = false, diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/auth/integration/DefaultAuthenticationComponent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/auth/integration/DefaultAuthenticationComponent.kt index 4fb7493bd00..0aae2090309 100644 --- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/auth/integration/DefaultAuthenticationComponent.kt +++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/auth/integration/DefaultAuthenticationComponent.kt @@ -100,12 +100,11 @@ class DefaultAuthenticationComponent( } init { - lifecycle.doOnCreate { - methods.forEach { method -> - when (method) { - is AuthMethod.BasicAuthMethod -> - basicAuthNavigation.activate(configuration = BasicAuthConfiguration(method)) - } + methods.forEach { method -> + when (method) { + is AuthMethod.BasicAuthMethod -> + basicAuthNavigation.activate(configuration = BasicAuthConfiguration(method)) + is AuthMethod.Unknown -> {} // TODO Handle unknown auth methods } } } @@ -120,9 +119,9 @@ class DefaultAuthenticationComponent( OnAuthenticated( option = AuthOption.BasicAuthOption( url = url, - method = output.method, username = output.username, password = output.password, + realm = output.method.realm, ), ), ) diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/start/integration/HttpStartStoreClient.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/start/integration/HttpStartStoreClient.kt index dfed6e3c921..885fccefe2e 100644 --- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/start/integration/HttpStartStoreClient.kt +++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/components/start/integration/HttpStartStoreClient.kt @@ -64,7 +64,7 @@ class HttpStartStoreClient( methods = methods, message = if (methods.isEmpty()) { - "Unauthorized response received with missing or unsupported auth method." + "Unauthorized response received with missing auth methods." } else { null }, @@ -83,15 +83,38 @@ class HttpStartStoreClient( * @param headers The headers to use for extracting the information. */ private fun getAuthMethodsFromHeader(headers: Headers): List { - val authHeader = headers["Www-Authenticate"] - val parts = authHeader?.split(" ", limit = 2) - val scheme = parts?.firstOrNull() + // Note that on JVM headers.getAll() may return multiple values, whereas in WebAssembly/JS + // it may merge multiple headers and separate them by comma + val authHeaders = headers.getAll("Www-Authenticate") ?: emptyList() - // TODO Get realm from header value + return authHeaders + // Split by comma, as there is the chance that headers will be merged and separated by + // comma (e.g. on web target) + .flatMap { header -> header.split(",") } + .map { authHeader -> + val (scheme, params) = parseWwwAuthenticate(authHeader) - return when (scheme) { - "Basic" -> listOf(AuthMethod.BasicAuthMethod(realm = "solr")) - else -> emptyList() - } + when (scheme.lowercase()) { + "basic", "xbasic" -> AuthMethod.BasicAuthMethod(realm = params["realm"]) + else -> AuthMethod.Unknown + } + } + } + + private fun parseWwwAuthenticate(headerValue: String): Pair> { + val parts = headerValue.split(" ", limit = 2) + val scheme = parts[0] + + // The below mapping is not supporting commas or spaces in the parameter values, nor + // parameters separated by commas (only spaces) + val params = parts.getOrNull(1) + ?.split(" ") + ?.map(String::trim) + ?.associate { + val (k, v) = it.split("=", limit = 2) + k to v.trim('"') + } + ?: emptyMap() + return scheme to params } } diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/domain/AuthMethod.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/domain/AuthMethod.kt index c9b711b2d43..e50d0429290 100644 --- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/domain/AuthMethod.kt +++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/domain/AuthMethod.kt @@ -35,5 +35,7 @@ sealed interface AuthMethod { * @property realm The realm of the basic auth. */ @Serializable - data class BasicAuthMethod(val realm: String = "") : AuthMethod + data class BasicAuthMethod(val realm: String? = null) : AuthMethod + + data object Unknown : AuthMethod } diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/domain/AuthOption.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/domain/AuthOption.kt index 637db9579d9..9665d7aa19c 100644 --- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/domain/AuthOption.kt +++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/domain/AuthOption.kt @@ -38,17 +38,17 @@ sealed interface AuthOption { /** * Authentication option for authenticating with basic auth (credentials). * - * @property method The basic auth method used for authentication (holds metadata). * @property url The URL of the instance that requires basic auth and the credentials of this * instance are for. * @property username The username to use for further authenticated requests. * @property password The password to use for further authenticated requests. + * @property realm The realm defined and used in the Basic auth method. */ @Serializable data class BasicAuthOption( - val method: AuthMethod.BasicAuthMethod, val url: Url, val username: String, val password: String, + val realm: String? = null, ) : AuthOption } diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/utils/HttpClientUtils.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/utils/HttpClientUtils.kt index e8b409a9138..98e6d146ed3 100644 --- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/utils/HttpClientUtils.kt +++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/utils/HttpClientUtils.kt @@ -57,19 +57,25 @@ fun getHttpClientWithAuthOption(option: AuthOption) = when (option) { is AuthOption.None -> getDefaultClient(option.url) is AuthOption.BasicAuthOption -> getHttpClientWithCredentials( url = option.url, + realm = option.realm, username = option.username, password = option.password, ) } fun getHttpClientWithCredentials( - url: Url = Url("http://127.0.0.1:8983/"), username: String, password: String, + url: Url = Url("http://127.0.0.1:8983/"), + realm: String? = null, ) = getDefaultClient(url) { install(Auth) { basic { credentials { BasicAuthCredentials(username, password) } + // Always include the credentials, because we are accessing protected endpoints from a + // not protected asset (web-assembly app) + sendWithoutRequest { true } + this.realm = realm } } } diff --git a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/auth/BasicAuthContent.kt b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/auth/BasicAuthContent.kt index c3a1b1a53c5..3cb87fbba3d 100644 --- a/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/auth/BasicAuthContent.kt +++ b/solr/ui/src/commonMain/kotlin/org/apache/solr/ui/views/auth/BasicAuthContent.kt @@ -35,6 +35,7 @@ import org.apache.solr.ui.components.auth.BasicAuthComponent import org.apache.solr.ui.generated.resources.Res import org.apache.solr.ui.generated.resources.action_sign_in_with_credentials import org.apache.solr.ui.generated.resources.authenticating +import org.apache.solr.ui.generated.resources.desc_sign_in_with_credentials import org.apache.solr.ui.generated.resources.desc_sign_in_with_credentials_to_realm import org.apache.solr.ui.generated.resources.label_password import org.apache.solr.ui.generated.resources.label_username @@ -62,7 +63,9 @@ fun BasicAuthContent( val model by component.model.collectAsState() Text( - text = stringResource(Res.string.desc_sign_in_with_credentials_to_realm, model.realm), + text = model.realm?.let { + stringResource(Res.string.desc_sign_in_with_credentials_to_realm, it) + } ?: stringResource(Res.string.desc_sign_in_with_credentials), style = MaterialTheme.typography.bodyMedium, ) diff --git a/solr/webapp/web/WEB-INF/web.xml b/solr/webapp/web/WEB-INF/web.xml index dfc0c0ce454..37a68b4692b 100644 --- a/solr/webapp/web/WEB-INF/web.xml +++ b/solr/webapp/web/WEB-INF/web.xml @@ -35,7 +35,7 @@ --> excludePatterns - /partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/templates/.+ + /partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/templates/.+,/ui/.*