This is an automated email from the ASF dual-hosted git repository. epugh pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/solr.git
commit a8388778421b3239476049f7295eb4ce2520c432 Author: Jan Høydahl <[email protected]> AuthorDate: Mon Feb 7 22:26:33 2022 +0100 SOLR-15907: Establish jwt-auth module --- gradle/maven/defaults-maven.gradle | 1 + gradle/validation/check-broken-links.gradle | 2 +- settings.gradle | 1 + solr/CHANGES.txt | 2 + solr/benchmark/build.gradle | 1 + solr/core/build.gradle | 18 +---- .../org/apache/solr/core/SolrResourceLoader.java | 3 +- .../apache/solr/security/AuthenticationPlugin.java | 4 +- .../netty-codec-http-4.1.68.Final.jar.sha1 | 1 + solr/modules/jwt-auth/README.md | 25 +++++++ solr/modules/jwt-auth/build.gradle | 56 +++++++++++++++ .../apache/solr/security/jwt}/JWTAuthPlugin.java | 13 ++-- .../apache/solr/security/jwt}/JWTIssuerConfig.java | 16 ++--- .../apache/solr/security/jwt}/JWTPrincipal.java | 15 ++-- .../security/jwt}/JWTPrincipalWithUserRoles.java | 22 +++--- .../security/jwt}/JWTVerificationkeyResolver.java | 36 +++++----- .../apache/solr/security/jwt/package-info.java} | 21 +----- solr/modules/jwt-auth/src/java/overview.html | 21 ++++++ .../solr/configsets/cloud-minimal/conf/schema.xml | 29 ++++++++ .../configsets/cloud-minimal/conf/solrconfig.xml | 51 ++++++++++++++ .../solr/security/jwt_plugin_idp_cert.pem | 0 .../solr/security/jwt_plugin_idp_certs.p12 | Bin .../solr/security/jwt_plugin_idp_invalidcert.pem | 0 .../solr/security/jwt_plugin_idp_wrongcert.pem | 0 .../solr/security/jwt_plugin_jwk_security.json | 0 .../jwt_plugin_jwk_security_blockUnknownFalse.json | 0 .../solr/security/jwt_plugin_jwk_url_security.json | 0 .../solr/security/jwt_well-known-config.json | 0 .../jwt}/JWTAuthPluginIntegrationTest.java | 43 ++++++++---- .../solr/security/jwt}/JWTAuthPluginTest.java | 76 +++++++++------------ .../solr/security/jwt}/JWTIssuerConfigTest.java | 10 +-- .../jwt}/JWTVerificationkeyResolverTest.java | 9 +-- solr/modules/langid/build.gradle | 3 +- solr/packaging/build.gradle | 1 + solr/prometheus-exporter/build.gradle | 2 +- .../src/jwt-authentication-plugin.adoc | 5 ++ .../src/major-changes-in-solr-9.adoc | 3 + .../apache/solr/cloud/MiniSolrCloudCluster.java | 13 +++- .../apache/solr/cloud/SolrCloudAuthTestCase.java | 16 +---- versions.lock | 11 +-- 40 files changed, 345 insertions(+), 185 deletions(-) diff --git a/gradle/maven/defaults-maven.gradle b/gradle/maven/defaults-maven.gradle index 278ad68..509b707 100644 --- a/gradle/maven/defaults-maven.gradle +++ b/gradle/maven/defaults-maven.gradle @@ -33,6 +33,7 @@ configure(rootProject) { ":solr:modules:extraction", ":solr:modules:gcs-repository", ":solr:modules:jaegertracer-configurator", + ":solr:modules:jwt-auth", ":solr:modules:langid", ":solr:modules:ltr", ":solr:modules:s3-repository", diff --git a/gradle/validation/check-broken-links.gradle b/gradle/validation/check-broken-links.gradle index d032f41..885e1a7 100644 --- a/gradle/validation/check-broken-links.gradle +++ b/gradle/validation/check-broken-links.gradle @@ -64,7 +64,7 @@ class CheckBrokenLinksTask extends DefaultTask { } if (result.getExitValue() != 0) { - throw new GradleException("Broken links check failed. Command output at: ${outputFile}") + throw new GradleException("Broken links check failed. Command output at: ${outputFile.get()}") } } } diff --git a/settings.gradle b/settings.gradle index 6a78dd4..9a2dcd7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -35,6 +35,7 @@ include "solr:modules:clustering" include "solr:modules:extraction" include "solr:modules:langid" include "solr:modules:jaegertracer-configurator" +include "solr:modules:jwt-auth" include "solr:modules:s3-repository" include "solr:modules:scripting" include "solr:modules:ltr" diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index b9f96dc..08b37cb 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -535,6 +535,8 @@ Other Changes * SOLR-15807: New LogListener class for tests to use to make assertions about what Log messages should or should not be produced by a test (hossman) +* SOLR-15907: Move JWT Authenticaion plugin to module 'jwt-auth' (janhoy) + * SOLR-15845: Add a new SolrVersion class to manage Solr's version independently from the Lucene version we consume (janhoy) * SOLR-14858: Add the server WEB-INF/lib directory to the classpath for the solr-exporter script. Will allow the script diff --git a/solr/benchmark/build.gradle b/solr/benchmark/build.gradle index cbfb9d1..8575d36 100644 --- a/solr/benchmark/build.gradle +++ b/solr/benchmark/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation 'org.jctools:jctools-core' implementation 'org.quicktheories:quicktheories' implementation 'org.openjdk.jmh:jmh-core' + implementation 'org.slf4j:slf4j-api' annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess' implementation 'org.slf4j:slf4j-api' } diff --git a/solr/core/build.gradle b/solr/core/build.gradle index ed43b04..a709f83 100644 --- a/solr/core/build.gradle +++ b/solr/core/build.gradle @@ -39,6 +39,9 @@ dependencies { api "org.apache.lucene:lucene-analysis-common" api "org.apache.lucene:lucene-queries" + // We export logging api with dependencies, which is useful for all modules + api 'org.slf4j:slf4j-api' + api project(':solr:solrj') api project(':solr:server') @@ -70,8 +73,6 @@ dependencies { implementation('com.github.ben-manes.caffeine:caffeine') { transitive = false } - implementation 'org.slf4j:slf4j-api' - implementation 'commons-codec:commons-codec' implementation 'commons-cli:commons-cli' @@ -148,9 +149,6 @@ dependencies { testImplementation 'org.slf4j:jcl-over-slf4j' - // JWT Auth plugin - api 'org.bitbucket.b_c:jose4j' - // For faster XML processing than the JDK implementation 'org.codehaus.woodstox:stax2-api' implementation 'com.fasterxml.woodstox:woodstox-core' @@ -204,16 +202,6 @@ dependencies { testImplementation('org.mockito:mockito-core', { exclude group: "net.bytebuddy", module: "byte-buddy-agent" }) - - testImplementation('no.nav.security:mock-oauth2-server', { - exclude group: "ch.qos.logback", module: "logback-core" - exclude group: "io.netty", module: "netty-all" - exclude group: "ch.qos.logback", module: "logback-classic" - }) - - testImplementation 'com.nimbusds:nimbus-jose-jwt' - testImplementation 'com.squareup.okhttp3:mockwebserver' - testImplementation 'com.squareup.okhttp3:okhttp' } diff --git a/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java b/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java index 661b061..2109694 100644 --- a/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java +++ b/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java @@ -87,7 +87,8 @@ public class SolrResourceLoader implements ResourceLoader, Closeable, SolrClassL private static final String[] packages = { "", "analysis.", "schema.", "handler.", "handler.tagger.", "search.", "update.", "core.", "response.", "request.", "update.processor.", "util.", "spelling.", "handler.component.", - "spelling.suggest.", "spelling.suggest.fst.", "rest.schema.analysis.", "security.", "handler.admin." + "spelling.suggest.", "spelling.suggest.fst.", "rest.schema.analysis.", "security.", "handler.admin.", + "security.jwt." }; private static final Charset UTF_8 = StandardCharsets.UTF_8; public static final String SOLR_ALLOW_UNSAFE_RESOURCELOADING_PARAM = "solr.allow.unsafe.resourceloading"; diff --git a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java index 0f4442c..b39bc41 100644 --- a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java @@ -95,11 +95,11 @@ public abstract class AuthenticationPlugin implements SolrInfoBean { } } - HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal principal) { + protected HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal principal) { return wrapWithPrincipal(request, principal, principal.getName()); } - HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal principal, String username) { + protected HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal principal, String username) { return new HttpServletRequestWrapper(request) { @Override public Principal getUserPrincipal() { diff --git a/solr/licenses/netty-codec-http-4.1.68.Final.jar.sha1 b/solr/licenses/netty-codec-http-4.1.68.Final.jar.sha1 new file mode 100644 index 0000000..a05d228 --- /dev/null +++ b/solr/licenses/netty-codec-http-4.1.68.Final.jar.sha1 @@ -0,0 +1 @@ +fc2e0526ceba7fe1d0ca1adfedc301afcc47bc51 diff --git a/solr/modules/jwt-auth/README.md b/solr/modules/jwt-auth/README.md new file mode 100644 index 0000000..eb2a275 --- /dev/null +++ b/solr/modules/jwt-auth/README.md @@ -0,0 +1,25 @@ +Apache Solr JWT Authentication Plugin +===================================== + +Introduction +------------ +Solr can support [JSON Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWT) based +Bearer authentication with the use of the JWTAuthPlugin. + +This allows Solr to assert that a user is already authenticated with an external +[Identity Provider (IdP)](https://en.wikipedia.org/wiki/Identity_provider) by validating +that the JWT formatted [access token](https://en.wikipedia.org/wiki/Access_token) +is digitally signed by the Identity Provider. + +The typical use case is to integrate Solr with an [OpenID Connect](https://en.wikipedia.org/wiki/OpenID_Connect) +enabled Identity Provider. + + +Getting Started +--------------- +Please refer to the Solr Ref Guide at https://solr.apache.org/guide/jwt-authentication-plugin.html +for more information. + +User interface +-------------- +A User interface for this module is part of Solr Core. \ No newline at end of file diff --git a/solr/modules/jwt-auth/build.gradle b/solr/modules/jwt-auth/build.gradle new file mode 100644 index 0000000..e747491 --- /dev/null +++ b/solr/modules/jwt-auth/build.gradle @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'java-library' + +description = 'JWT / OpenID Connect / OAuth2 authentication plugin' + +dependencies { + implementation project(':solr:core') + implementation project(':solr:solrj') + + implementation 'org.bitbucket.b_c:jose4j' + + implementation 'commons-io:commons-io' + implementation 'io.dropwizard.metrics:metrics-core' + implementation 'javax.servlet:javax.servlet-api' + implementation 'org.apache.commons:commons-lang3' + implementation 'org.apache.httpcomponents:httpclient' + implementation 'org.apache.httpcomponents:httpcore' + implementation 'org.eclipse.jetty:jetty-client' + implementation ('com.google.guava:guava') { transitive = false } + implementation 'org.slf4j:slf4j-api' + + implementation 'com.fasterxml.jackson.core:jackson-databind' + + testImplementation project(':solr:test-framework') + testImplementation 'org.apache.lucene:lucene-test-framework' + testImplementation 'junit:junit' + + testImplementation('org.mockito:mockito-core', { + exclude group: "net.bytebuddy", module: "byte-buddy-agent" + }) + testImplementation('no.nav.security:mock-oauth2-server', { + exclude group: "ch.qos.logback", module: "logback-core" + exclude group: "io.netty", module: "netty-all" + exclude group: "ch.qos.logback", module: "logback-classic" + }) + testImplementation 'com.nimbusds:nimbus-jose-jwt' + testImplementation 'com.squareup.okhttp3:mockwebserver' + testImplementation 'com.squareup.okhttp3:okhttp' + testImplementation 'io.netty:netty-codec-http' +} diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java similarity index 99% rename from solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java rename to solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java index ab040bc..83fcb48 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java +++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java @@ -14,9 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; -import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.InputStream; import java.lang.invoke.MethodHandles; @@ -58,7 +57,9 @@ import org.apache.solr.common.util.CommandOperation; import org.apache.solr.common.util.Utils; import org.apache.solr.common.util.ValidatingJsonMap; import org.apache.solr.core.CoreContainer; -import org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode; +import org.apache.solr.security.AuthenticationPlugin; +import org.apache.solr.security.ConfigEditablePlugin; +import org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode; import org.apache.solr.util.CryptoKeys; import org.eclipse.jetty.client.api.Request; import org.jose4j.jwa.AlgorithmConstraints; @@ -103,7 +104,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin private static final String PARAM_ALG_WHITELIST = "algWhitelist"; private static final Set<String> PROPS = - ImmutableSet.of( + Set.of( PARAM_BLOCK_UNKNOWN, PARAM_PRINCIPAL_CLAIM, PARAM_REQUIRE_EXPIRATIONTIME, @@ -693,7 +694,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin } @Override - public void close() throws IOException { + public void close() { jwtConsumer = null; } @@ -781,7 +782,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin } /** Response for authentication attempt */ - static class JWTAuthenticationResponse { + protected static class JWTAuthenticationResponse { private final Principal principal; private String errorMessage; private final AuthCode authCode; diff --git a/solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java similarity index 97% rename from solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java rename to solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java index 11b4115..9b3d06a 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java +++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java @@ -15,13 +15,12 @@ * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; import com.google.common.annotations.VisibleForTesting; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.lang.invoke.MethodHandles; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; @@ -43,12 +42,9 @@ import org.jose4j.jwk.HttpsJwks; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKeySet; import org.jose4j.lang.JoseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** Holds information about an IdP (issuer), such as issuer ID, JWK url(s), keys etc */ public class JWTIssuerConfig { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); static final String PARAM_ISS_NAME = "name"; static final String PARAM_JWKS_URL = "jwksUrl"; static final String PARAM_JWK = "jwk"; @@ -378,8 +374,7 @@ public class JWTIssuerConfig { return this.trustedCerts; } - /** */ - static class HttpsJwksFactory { + public static class HttpsJwksFactory { private final long jwkCacheDuration; private final long refreshReprieveThreshold; private Collection<X509Certificate> trustedCerts; @@ -398,7 +393,7 @@ public class JWTIssuerConfig { this.trustedCerts = trustedCerts; } - /** + /* * While the class name is HttpsJwks, it actually works with plain http formatted url as well. * * @param url the Url to connect to for JWK details. @@ -433,8 +428,9 @@ public class JWTIssuerConfig { } /** - * Config object for a OpenId Connect well-known config Typically exposed through - * /.well-known/openid-configuration endpoint + * Config object for a OpenId Connect well-known config. + * + * <p>Typically exposed through <code>/.well-known/openid-configuration endpoint</code>. */ public static class WellKnownDiscoveryConfig { private final Map<String, Object> securityConf; diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipal.java similarity index 91% rename from solr/core/src/java/org/apache/solr/security/JWTPrincipal.java rename to solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipal.java index a779fad..ec70bb2 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java +++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipal.java @@ -15,9 +15,10 @@ * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; import java.security.Principal; +import java.util.Locale; import java.util.Map; import java.util.Objects; import org.apache.http.util.Args; @@ -75,15 +76,7 @@ public class JWTPrincipal implements Principal { @Override public String toString() { - return "JWTPrincipal{" - + "username='" - + username - + '\'' - + ", token='" - + "*****" - + '\'' - + ", claims=" - + claims - + '}'; + return String.format( + Locale.ROOT, "JWTPrincipal{username='%s', token='*****', claims=%s}", username, claims); } } diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipalWithUserRoles.java similarity index 87% rename from solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java rename to solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipalWithUserRoles.java index 856e6c2..1f298a1 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java +++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipalWithUserRoles.java @@ -15,12 +15,14 @@ * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import org.apache.http.util.Args; +import org.apache.solr.security.VerifiedUserRoles; /** * JWT principal that contains username, token, claims and a list of roles the user has, so one can @@ -58,17 +60,11 @@ public class JWTPrincipalWithUserRoles extends JWTPrincipal implements VerifiedU @Override public String toString() { - return "JWTPrincipalWithUserRoles{" - + "username='" - + username - + '\'' - + ", token='" - + "*****" - + '\'' - + ", claims=" - + claims - + ", roles=" - + roles - + '}'; + return String.format( + Locale.ROOT, + "JWTPrincipalWithUserRoles{username='%s', token='*****', claims=%s, roles=%s}", + username, + claims, + roles); } } diff --git a/solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTVerificationkeyResolver.java similarity index 88% rename from solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java rename to solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTVerificationkeyResolver.java index 08e5cb5..dd7a54b 100644 --- a/solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java +++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTVerificationkeyResolver.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; import java.io.IOException; import java.lang.invoke.MethodHandles; @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import javax.net.ssl.SSLHandshakeException; @@ -154,25 +155,26 @@ public class JWTVerificationkeyResolver implements VerificationKeyResolver { theChosenOne = verificationJwkSelector.select(jws, jsonWebKeys); } } catch (JoseException | IOException | InvalidJwtException | MalformedClaimException e) { - StringBuilder sb = new StringBuilder(); - sb.append("Unable to find a suitable verification key for JWS w/ header ") - .append(jws.getHeaders().getFullHeaderAsJsonString()); - sb.append(" due to an unexpected exception (") - .append(e) - .append(") while obtaining or using keys from source "); - sb.append(keysSource); - throw new UnresolvableKeyException(sb.toString(), e); + String msg = + String.format( + Locale.ROOT, + "Unable to find a suitable verification key for JWS w/ header %s due to an unexpected exception (%s) " + + "while obtaining or using keys from source %s", + jws.getHeaders().getFullHeaderAsJsonString(), + e, + keysSource); + throw new UnresolvableKeyException(msg, e); } if (theChosenOne == null) { - StringBuilder sb = new StringBuilder(); - sb.append("Unable to find a suitable verification key for JWS w/ header ") - .append(jws.getHeaders().getFullHeaderAsJsonString()); - sb.append(" from ") - .append(jsonWebKeys.size()) - .append(" keys from source ") - .append(keysSource); - throw new UnresolvableKeyException(sb.toString()); + String msg = + String.format( + Locale.ROOT, + "Unable to find a suitable verification key for JWS w/ header %s from %d keys from source %s", + jws.getHeaders().getFullHeaderAsJsonString(), + jsonWebKeys.size(), + keysSource); + throw new UnresolvableKeyException(msg); } return theChosenOne.getKey(); diff --git a/solr/modules/langid/build.gradle b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/package-info.java similarity index 54% copy from solr/modules/langid/build.gradle copy to solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/package-info.java index 5576d9d..1cf2ccd 100644 --- a/solr/modules/langid/build.gradle +++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/package-info.java @@ -15,22 +15,5 @@ * limitations under the License. */ -apply plugin: 'java-library' - -description = 'Language Identifier module for extracting language from a document being indexed' - -dependencies { - implementation project(':solr:core') - implementation project(':solr:solrj') - - implementation ('org.apache.tika:tika-core') { transitive = false } - implementation 'commons-io:commons-io' - implementation 'com.cybozu.labs:langdetect' - implementation 'net.arnx:jsonic' - implementation 'org.apache.opennlp:opennlp-tools' - implementation 'org.slf4j:slf4j-api' - - testImplementation project(':solr:test-framework') - testImplementation 'com.carrotsearch.randomizedtesting:randomizedtesting-runner' - testImplementation 'junit:junit' -} +/** JWT authentication plugin */ +package org.apache.solr.security.jwt; diff --git a/solr/modules/jwt-auth/src/java/overview.html b/solr/modules/jwt-auth/src/java/overview.html new file mode 100644 index 0000000..69fcc17 --- /dev/null +++ b/solr/modules/jwt-auth/src/java/overview.html @@ -0,0 +1,21 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<html> +<body> +Apache Solr Search Server: Solr Oauth JWT Authentication plugin +</body> +</html> diff --git a/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/schema.xml b/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/schema.xml new file mode 100644 index 0000000..4124fea --- /dev/null +++ b/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/schema.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<schema name="minimal" version="1.1"> + <fieldType name="string" class="solr.StrField"/> + <fieldType name="int" class="${solr.tests.IntegerFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/> + <fieldType name="long" class="${solr.tests.LongFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/> + <dynamicField name="*" type="string" indexed="true" stored="true"/> + <!-- for versioning --> + <field name="_version_" type="long" indexed="true" stored="true"/> + <field name="_root_" type="string" indexed="true" stored="true" multiValued="false" required="false"/> + <field name="id" type="string" indexed="true" stored="true"/> + <dynamicField name="*_s" type="string" indexed="true" stored="true" /> + <uniqueKey>id</uniqueKey> +</schema> diff --git a/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml b/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml new file mode 100644 index 0000000..853ba65 --- /dev/null +++ b/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" ?> + +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- Minimal solrconfig.xml with /select, /admin and /update only --> + +<config> + + <dataDir>${solr.data.dir:}</dataDir> + + <directoryFactory name="DirectoryFactory" + class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/> + <schemaFactory class="ClassicIndexSchemaFactory"/> + + <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion> + + <updateHandler class="solr.DirectUpdateHandler2"> + <commitWithin> + <softCommit>${solr.commitwithin.softcommit:true}</softCommit> + </commitWithin> + <updateLog class="${solr.ulog:solr.UpdateLog}"></updateLog> + </updateHandler> + + <requestHandler name="/select" class="solr.SearchHandler"> + <lst name="defaults"> + <str name="echoParams">explicit</str> + <str name="indent">true</str> + <str name="df">text</str> + </lst> + + </requestHandler> + <indexConfig> + <mergeScheduler class="${solr.mscheduler:org.apache.lucene.index.ConcurrentMergeScheduler}"/> +: </indexConfig> +</config> + diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_idp_cert.pem b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_cert.pem similarity index 100% rename from solr/core/src/test-files/solr/security/jwt_plugin_idp_cert.pem rename to solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_cert.pem diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_idp_certs.p12 b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_certs.p12 similarity index 100% rename from solr/core/src/test-files/solr/security/jwt_plugin_idp_certs.p12 rename to solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_certs.p12 diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_idp_invalidcert.pem b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_invalidcert.pem similarity index 100% rename from solr/core/src/test-files/solr/security/jwt_plugin_idp_invalidcert.pem rename to solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_invalidcert.pem diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_idp_wrongcert.pem b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_wrongcert.pem similarity index 100% rename from solr/core/src/test-files/solr/security/jwt_plugin_idp_wrongcert.pem rename to solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_wrongcert.pem diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security.json similarity index 100% rename from solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json rename to solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security.json diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json similarity index 100% rename from solr/core/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json rename to solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_url_security.json similarity index 100% rename from solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json rename to solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_url_security.json diff --git a/solr/core/src/test-files/solr/security/jwt_well-known-config.json b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_well-known-config.json similarity index 100% rename from solr/core/src/test-files/solr/security/jwt_well-known-config.json rename to solr/modules/jwt-auth/src/test-files/solr/security/jwt_well-known-config.json diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java similarity index 94% rename from solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java rename to solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java index 434469b..e079d3a 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java +++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java @@ -14,9 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.solr.security.jwt.JWTAuthPluginTest.JWT_TEST_PATH; import java.io.BufferedReader; import java.io.IOException; @@ -72,6 +73,7 @@ import org.jose4j.jwk.RsaJwkGenerator; import org.jose4j.jws.AlgorithmIdentifiers; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; +import org.jose4j.lang.JoseException; import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -100,11 +102,9 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { @BeforeClass public static void beforeClass() throws Exception { // Setup an OAuth2 mock server with SSL - Path p12Cert = - SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_certs.p12"); - pemFilePath = SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"); - wrongPemFilePath = - SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_wrongcert.pem"); + Path p12Cert = JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_certs.p12"); + pemFilePath = JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"); + wrongPemFilePath = JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_wrongcert.pem"); mockOAuth2Server = createMockOAuthServer(p12Cert, "secret"); mockOAuth2Server.start(); @@ -215,13 +215,24 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { assertAuthMetricsMinimums(2, 1, 0, 0, 1, 0); executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: false}}", jws); verifySecurityStatus( - cl, baseUrl + authcPrefix, "authentication/blockUnknown", "false", 20, jws); + cl, + baseUrl + authcPrefix, + "authentication/blockUnknown", + "false", + 20, + getBearerAuthHeader(jws)); // Pass through verifySecurityStatus(cl, baseUrl + "/admin/info/key", "key", NOT_NULL_PREDICATE, 20); // Now succeeds since blockUnknown=false get(baseUrl + "/" + COLLECTION + "/query?q=*:*", null); executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: true}}", null); - verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/blockUnknown", "true", 20, jws); + verifySecurityStatus( + cl, + baseUrl + authcPrefix, + "authentication/blockUnknown", + "true", + 20, + getBearerAuthHeader(jws)); assertAuthMetricsMinimums(9, 4, 4, 0, 1, 0); @@ -268,6 +279,10 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { HttpClientUtil.close(cl); } + static String getBearerAuthHeader(JsonWebSignature jws) throws JoseException { + return "Bearer " + jws.getCompactSerialization(); + } + /** * Configure solr cluster with a security.json talking to MockOAuth2 server * @@ -282,13 +297,12 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { MiniSolrCloudCluster myCluster = configureCluster(numNodes) // nodes .addConfig( - "conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) + "conf1", + JWT_TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) .withDefaultClusterProperty("useLegacyReplicaAssignment", "false") .build(); String securityJson = createMockOAuthSecurityJson(pemFilePath); - myCluster - .getZkClient() - .setData("/security.json", securityJson.getBytes(Charset.defaultCharset()), true); + myCluster.zkSetData("/security.json", securityJson.getBytes(Charset.defaultCharset()), true); RTimer timer = new RTimer(); do { // Wait timeoutMs time for the security.json change to take effect Thread.sleep(200); @@ -311,9 +325,10 @@ public class JWTAuthPluginIntegrationTest extends SolrCloudAuthTestCase { throws Exception { MiniSolrCloudCluster myCluster = configureCluster(2) // nodes - .withSecurityJson(TEST_PATH().resolve("security").resolve(securityJsonFilename)) + .withSecurityJson(JWT_TEST_PATH().resolve("security").resolve(securityJsonFilename)) .addConfig( - "conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) + "conf1", + JWT_TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) .withDefaultClusterProperty("useLegacyReplicaAssignment", "false") .build(); diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java similarity index 89% rename from solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java rename to solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java index 2e1c241..e25d2fb 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java +++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java @@ -14,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; -import static org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.*; +import static org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.*; import java.io.IOException; import java.io.InputStream; @@ -38,6 +38,7 @@ import org.apache.commons.io.IOUtils; import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.common.SolrException; import org.apache.solr.common.util.Utils; +import org.apache.solr.security.VerifiedUserRoles; import org.apache.solr.util.CryptoKeys; import org.jose4j.jwk.RsaJsonWebKey; import org.jose4j.jwk.RsaJwkGenerator; @@ -63,6 +64,10 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { // Shared with other tests static HashMap<String, Object> testJwk; + public static Path JWT_TEST_PATH() { + return getFile("solr/security").getParentFile().toPath(); + } + static { // Generate an RSA key pair, which will be used for signing and verification of the JWT, wrapped // in a JWK @@ -82,7 +87,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { "n", BigEndianBigInteger.toBase64Url(rsaJsonWebKey.getRsaPublicKey().getModulus())); trustedPemCert = - Files.readString(TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem")); + Files.readString(JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem")); } catch (JoseException | IOException e) { fail("Failed static initialization: " + e.getMessage()); } @@ -142,13 +147,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { plugin = new JWTAuthPlugin(); testConfig = new HashMap<>(); - testConfig.put("class", "org.apache.solr.security.JWTAuthPlugin"); + testConfig.put("class", "org.apache.solr.security.jwt.JWTAuthPlugin"); testConfig.put("principalClaim", "customPrincipal"); testConfig.put("jwk", testJwk); plugin.init(testConfig); minimalConfig = new HashMap<>(); - minimalConfig.put("class", "org.apache.solr.security.JWTAuthPlugin"); + minimalConfig.put("class", "org.apache.solr.security.jwt.JWTAuthPlugin"); } @Override @@ -169,7 +174,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void initFromSecurityJSONLocalJWK() throws Exception { - Path securityJson = TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_security.json"); + Path securityJson = JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_security.json"); InputStream is = Files.newInputStream(securityJson); Map<String, Object> securityConf = (Map<String, Object>) Utils.fromJSON(is); Map<String, Object> authConf = (Map<String, Object>) securityConf.get("authentication"); @@ -178,16 +183,15 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void initFromSecurityJSONUrlJwk() throws Exception { - Path securityJson = TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_url_security.json"); + Path securityJson = + JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_url_security.json"); InputStream is = Files.newInputStream(securityJson); Map<String, Object> securityConf = (Map<String, Object>) Utils.fromJSON(is); Map<String, Object> authConf = (Map<String, Object>) securityConf.get("authentication"); plugin.init(authConf); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, - resp.getAuthCode()); + assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); assertTrue(resp.getJwtException().getMessage().contains("Connection refused")); } @@ -236,9 +240,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { plugin.init(minimalConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); assertFalse(resp.isAuthenticated()); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, - resp.getAuthCode()); + assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); testConfig.put("principalClaim", "sub"); // testConfig has subject = solruser plugin.init(testConfig); @@ -252,9 +254,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { plugin.init(testConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); assertFalse(resp.isAuthenticated()); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, - resp.getAuthCode()); + assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); testConfig.put("iss", "IDServer"); plugin.init(testConfig); @@ -268,9 +268,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { plugin.init(testConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); assertFalse(resp.isAuthenticated()); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, - resp.getAuthCode()); + assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); testConfig.put("aud", "Solr"); plugin.init(testConfig); @@ -289,8 +287,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { plugin.init(testConfig); resp = plugin.authenticate(testHeader); assertFalse(resp.isAuthenticated()); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PRINCIPAL_MISSING, resp.getAuthCode()); + assertEquals(PRINCIPAL_MISSING, resp.getAuthCode()); } @Test @@ -310,15 +307,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { shouldMatch.put("claim9", "NA"); plugin.init(testConfig); resp = plugin.authenticate(testHeader); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode()); + assertEquals(CLAIM_MISMATCH, resp.getAuthCode()); // Required claim does not match regex shouldMatch.clear(); shouldMatch.put("claim1", "NA"); resp = plugin.authenticate(testHeader); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, resp.getAuthCode()); + assertEquals(CLAIM_MISMATCH, resp.getAuthCode()); } @Test @@ -333,18 +328,14 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { testConfig.put("requireExp", true); plugin.init(testConfig); resp = plugin.authenticate(slimHeader); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, - resp.getAuthCode()); + assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); testConfig.put("requireExp", false); // Missing issuer claim testConfig.put("requireIss", true); plugin.init(testConfig); resp = plugin.authenticate(slimHeader); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, - resp.getAuthCode()); + assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); } @Test @@ -352,9 +343,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { testConfig.put("algAllowlist", Arrays.asList("PS384", "PS512")); plugin.init(testConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(testHeader); - assertEquals( - JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION, - resp.getAuthCode()); + assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode()); assertTrue(resp.getErrorMessage().contains("not a permitted algorithm")); } @@ -413,7 +402,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { testConfig.put("blockUnknown", false); plugin.init(testConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode()); + assertEquals(PASS_THROUGH, resp.getAuthCode()); } @Test @@ -421,13 +410,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { minimalConfig.put("blockUnknown", false); plugin.init(minimalConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode()); + assertEquals(PASS_THROUGH, resp.getAuthCode()); } @Test public void wellKnownConfigNoHeaderPassThrough() { String wellKnownUrl = - TEST_PATH() + JWT_TEST_PATH() .resolve("security") .resolve("jwt_well-known-config.json") .toAbsolutePath() @@ -437,13 +426,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { testConfig.remove("jwk"); plugin.init(testConfig); JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null); - assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, resp.getAuthCode()); + assertEquals(PASS_THROUGH, resp.getAuthCode()); } @Test public void defaultRealm() { String wellKnownUrl = - TEST_PATH() + JWT_TEST_PATH() .resolve("security") .resolve("jwt_well-known-config.json") .toAbsolutePath() @@ -458,7 +447,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void configureRealm() { String wellKnownUrl = - TEST_PATH() + JWT_TEST_PATH() .resolve("security") .resolve("jwt_well-known-config.json") .toAbsolutePath() @@ -560,7 +549,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { authConf.put("jwksUrl", "https://127.0.0.1:9999/foo.jwk"); authConf.put( "trustedCertsFile", - TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem").toString()); + JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem").toString()); plugin = new JWTAuthPlugin(); plugin.init(authConf); assertEquals(2, plugin.getIssuerConfigs().get(0).getTrustedCerts().size()); @@ -581,7 +570,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { authConf.put("jwksUrl", "https://127.0.0.1:9999/foo.jwk"); authConf.put( "trustedCertsFile", - TEST_PATH().resolve("security").resolve("jwt_plugin_idp_invalidcert.pem").toString()); + JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_invalidcert.pem").toString()); plugin = new JWTAuthPlugin(); expectThrows(SolrException.class, () -> plugin.init(authConf)); } @@ -622,8 +611,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 { @Test public void extractCertificate() throws IOException { - Path pemFilePath = - SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"); + Path pemFilePath = JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"); String cert = CryptoKeys.extractCertificateFromPem(Files.readString(pemFilePath)); assertEquals( 2, CryptoKeys.parseX509Certs(IOUtils.toInputStream(cert, StandardCharsets.UTF_8)).size()); diff --git a/solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java similarity index 95% rename from solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java rename to solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java index f15f4e4..d51ae82 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java +++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java @@ -15,10 +15,10 @@ * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; -import static org.apache.solr.SolrTestCaseJ4.TEST_PATH; -import static org.apache.solr.security.JWTAuthPluginTest.testJwk; +import static org.apache.solr.security.jwt.JWTAuthPluginTest.JWT_TEST_PATH; +import static org.apache.solr.security.jwt.JWTAuthPluginTest.testJwk; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -157,7 +157,7 @@ public class JWTIssuerConfigTest extends SolrTestCase { @Test public void wellKnownConfigFromInputstream() throws IOException { - Path configJson = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json"); + Path configJson = JWT_TEST_PATH().resolve("security").resolve("jwt_well-known-config.json"); JWTIssuerConfig.WellKnownDiscoveryConfig config = JWTIssuerConfig.WellKnownDiscoveryConfig.parse(Files.newInputStream(configJson)); assertEquals("https://acmepaymentscorp/oauth/jwks", config.getJwksUrl()); @@ -165,7 +165,7 @@ public class JWTIssuerConfigTest extends SolrTestCase { @Test public void wellKnownConfigFromString() throws IOException { - Path configJson = TEST_PATH().resolve("security").resolve("jwt_well-known-config.json"); + Path configJson = JWT_TEST_PATH().resolve("security").resolve("jwt_well-known-config.json"); String configString = StringUtils.join(Files.readAllLines(configJson), "\n"); JWTIssuerConfig.WellKnownDiscoveryConfig config = JWTIssuerConfig.WellKnownDiscoveryConfig.parse(configString, StandardCharsets.UTF_8); diff --git a/solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java similarity index 95% rename from solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java rename to solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java index c24b9d8..3de70e6 100644 --- a/solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java +++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java @@ -15,10 +15,9 @@ * limitations under the License. */ -package org.apache.solr.security; +package org.apache.solr.security.jwt; import static java.util.Arrays.asList; -import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; @@ -26,7 +25,7 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.apache.solr.SolrTestCaseJ4; -import org.apache.solr.security.JWTIssuerConfig.HttpsJwksFactory; +import org.apache.solr.security.jwt.JWTIssuerConfig.HttpsJwksFactory; import org.jose4j.jwk.HttpsJwks; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.RsaJsonWebKey; @@ -38,6 +37,7 @@ import org.jose4j.lang.UnresolvableKeyException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -85,7 +85,8 @@ public class JWTVerificationkeyResolverTest extends SolrTestCaseJ4 { keysToReturnFromSecondJwk = refreshSequenceForSecondJwk.next(); return keysToReturnFromSecondJwk; }); - when(httpsJwksFactory.createList(anyList())).thenReturn(asList(firstJwkList, secondJwkList)); + when(httpsJwksFactory.createList(ArgumentMatchers.anyList())) + .thenReturn(asList(firstJwkList, secondJwkList)); JWTIssuerConfig issuerConfig = new JWTIssuerConfig("primary").setIss("foo").setJwksUrl(asList("url1", "url2")); diff --git a/solr/modules/langid/build.gradle b/solr/modules/langid/build.gradle index 5576d9d..a88a202 100644 --- a/solr/modules/langid/build.gradle +++ b/solr/modules/langid/build.gradle @@ -28,8 +28,7 @@ dependencies { implementation 'com.cybozu.labs:langdetect' implementation 'net.arnx:jsonic' implementation 'org.apache.opennlp:opennlp-tools' - implementation 'org.slf4j:slf4j-api' - +implementation 'org.slf4j:slf4j-api' testImplementation project(':solr:test-framework') testImplementation 'com.carrotsearch.randomizedtesting:randomizedtesting-runner' testImplementation 'junit:junit' diff --git a/solr/packaging/build.gradle b/solr/packaging/build.gradle index 40006f0..da324d8 100644 --- a/solr/packaging/build.gradle +++ b/solr/packaging/build.gradle @@ -53,6 +53,7 @@ dependencies { ":solr:modules:gcs-repository", ":solr:modules:hdfs", ":solr:modules:jaegertracer-configurator", + ":solr:modules:jwt-auth", ":solr:modules:langid", ":solr:modules:ltr", ":solr:modules:s3-repository", diff --git a/solr/prometheus-exporter/build.gradle b/solr/prometheus-exporter/build.gradle index ecd1437..9402fce 100644 --- a/solr/prometheus-exporter/build.gradle +++ b/solr/prometheus-exporter/build.gradle @@ -22,7 +22,6 @@ description = 'Prometheus exporter for exposing metrics from Solr using Metrics dependencies { implementation project(':solr:solrj') - implementation 'org.slf4j:slf4j-api' implementation('org.apache.zookeeper:zookeeper') { transitive = false } // ideally remove ZK dep implementation ('io.prometheus:simpleclient') @@ -36,6 +35,7 @@ dependencies { implementation ('net.sourceforge.argparse4j:argparse4j') implementation ('com.github.ben-manes.caffeine:caffeine') { transitive = false } implementation 'commons-io:commons-io' + implementation 'org.slf4j:slf4j-api' runtimeOnly 'org.apache.logging.log4j:log4j-api' runtimeOnly 'org.apache.logging.log4j:log4j-core' diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc index bcf3d72..c928e60 100644 --- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc +++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc @@ -21,6 +21,11 @@ Solr can support https://en.wikipedia.org/wiki/JSON_Web_Token[JSON Web Token] (J This allows Solr to assert that a user is already authenticated with an external https://en.wikipedia.org/wiki/Identity_provider[Identity Provider] by validating that the JWT formatted https://en.wikipedia.org/wiki/Access_token[access token] is digitally signed by the Identity Provider. The typical use case is to integrate Solr with an https://en.wikipedia.org/wiki/OpenID_Connect[OpenID Connect] enabled IdP. +== Module + +This is provided via a Solr Module that needs to be added to the classpath before use. Since this is a node-level +plugin it must go in `sharedLib`, see <<configuring-solr-xml.adoc#,Configuring solr.xml>> for details. + == Enable JWT Authentication To use JWT Bearer authentication, the `security.json` file must have an `authentication` part which defines the class being used for authentication along with configuration parameters. diff --git a/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc b/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc index 17611da..fbc5f8e 100644 --- a/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc +++ b/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc @@ -110,6 +110,9 @@ This is only applicable for users returning information in JSON format, which is * SOLR-14660: HDFS storage support has been moved to a module. Existing Solr configurations do not need any HDFS-related changes, however the module needs to be installed - see <<solr-on-hdfs.adoc#,Running Solr on HDFS>>. +* SOLR-15097: JWTAuthPlugin has been moved to a module. Users need to add the module to classpath. The plugin has also + changed package name to `org.apache.solr.security.jwt`, but can still be loaded as shortform `class="solr.JWTAuthPlugin"`. + == New Features & Enhancements * Replica placement plugins diff --git a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java index aa9e533..d11d22e 100644 --- a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java +++ b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java @@ -644,7 +644,18 @@ public class MiniSolrCloudCluster { public SolrZkClient getZkClient() { return solrClient.getZkStateReader().getZkClient(); } - + + /** + * Set data in zk without exposing caller to the ZK API, i.e. tests won't need to include Zookeeper dependencies + */ + public void zkSetData(String path, byte[] data, boolean retryOnConnLoss) throws InterruptedException { + try { + getZkClient().setData(path, data, -1, retryOnConnLoss); + } catch (KeeperException e) { + throw new SolrException(ErrorCode.UNKNOWN, "Failed writing to Zookeeper", e); + } + } + protected CloudSolrClient buildSolrClient() { return new CloudSolrClient.Builder(Collections.singletonList(getZkServer().getZkAddress()), Optional.empty()) .withSocketTimeout(90000).withConnectionTimeout(15000).build(); // we choose 90 because we run in some harsh envs diff --git a/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java b/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java index f918562..b8ff1e8 100644 --- a/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java +++ b/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java @@ -44,8 +44,6 @@ import org.apache.solr.client.solrj.embedded.JettySolrRunner; import org.apache.solr.common.util.StrUtils; import org.apache.solr.common.util.Utils; import org.apache.solr.util.TimeOut; -import org.jose4j.jws.JsonWebSignature; -import org.jose4j.lang.JoseException; import org.junit.AfterClass; import org.junit.BeforeClass; import org.slf4j.Logger; @@ -195,15 +193,9 @@ public class SolrCloudAuthTestCase extends SolrCloudTestCase { verifySecurityStatus(cl, url, objPath, expected, count, makeBasicAuthHeader(user, pwd)); } - protected void verifySecurityStatus(HttpClient cl, String url, String objPath, - Object expected, int count, JsonWebSignature jws) throws Exception { - verifySecurityStatus(cl, url, objPath, expected, count, getBearerAuthHeader(jws)); - } - - @SuppressWarnings({"unchecked"}) - private static void verifySecurityStatus(HttpClient cl, String url, String objPath, - Object expected, int count, String authHeader) throws IOException, InterruptedException { + protected static void verifySecurityStatus(HttpClient cl, String url, String objPath, + Object expected, int count, String authHeader) throws IOException, InterruptedException { boolean success = false; String s = null; List<String> hierarchy = StrUtils.splitSmart(objPath, '/'); @@ -242,10 +234,6 @@ public class SolrCloudAuthTestCase extends SolrCloudTestCase { return "Basic " + Base64.getEncoder().encodeToString(userPass.getBytes(UTF_8)); } - static String getBearerAuthHeader(JsonWebSignature jws) throws JoseException { - return "Bearer " + jws.getCompactSerialization(); - } - public static void setAuthorizationHeader(AbstractHttpMessage httpMsg, String headerString) { httpMsg.setHeader(new BasicHeader("Authorization", headerString)); log.info("Added Authorization Header {}", headerString); diff --git a/versions.lock b/versions.lock index b673d06..14d5eea 100644 --- a/versions.lock +++ b/versions.lock @@ -70,12 +70,12 @@ io.dropwizard.metrics:metrics-jvm:4.1.5 (1 constraints: 0c050736) io.grpc:grpc-context:1.36.0 (3 constraints: 5a22231e) io.jaegertracing:jaeger-core:1.6.0 (2 constraints: 6912c420) io.jaegertracing:jaeger-thrift:1.6.0 (1 constraints: 09050236) -io.netty:netty-buffer:4.1.68.Final (5 constraints: df4da774) -io.netty:netty-codec:4.1.68.Final (1 constraints: a60ccb09) -io.netty:netty-common:4.1.68.Final (7 constraints: 59675dcf) -io.netty:netty-handler:4.1.68.Final (1 constraints: db0f158f) +io.netty:netty-buffer:4.1.68.Final (6 constraints: 915bfcb7) +io.netty:netty-codec:4.1.68.Final (2 constraints: 581a4d48) +io.netty:netty-common:4.1.68.Final (8 constraints: 0b75841f) +io.netty:netty-handler:4.1.68.Final (2 constraints: 8d1d363b) io.netty:netty-resolver:4.1.68.Final (2 constraints: 5a1a4732) -io.netty:netty-transport:4.1.68.Final (4 constraints: 2b402b76) +io.netty:netty-transport:4.1.68.Final (5 constraints: dd4d9295) io.netty:netty-transport-native-epoll:4.1.68.Final (1 constraints: db0f158f) io.netty:netty-transport-native-unix-common:4.1.68.Final (1 constraints: b212fc1e) io.opencensus:opencensus-api:0.28.0 (5 constraints: 21426a37) @@ -295,6 +295,7 @@ com.vaadin.external.google:android-json:0.0.20131108.vaadin1 (1 constraints: 340 io.github.microutils:kotlin-logging:2.0.6 (1 constraints: be0e9d62) io.github.microutils:kotlin-logging-jvm:2.0.6 (1 constraints: 810f877c) io.micrometer:micrometer-core:1.5.14 (1 constraints: fc161b19) +io.netty:netty-codec-http:4.1.68.Final (1 constraints: 5d077961) io.opentracing:opentracing-mock:0.33.0 (1 constraints: 3805343b) jakarta.activation:jakarta.activation-api:1.2.1 (2 constraints: b928fbbd) jakarta.annotation:jakarta.annotation-api:1.3.5 (1 constraints: 3a131133)
