diff --git a/solr/modules/jwt-auth/build.gradle b/solr/modules/jwt-auth/build.gradle index 80cb694fb3ac..b52da5eac298 100644 --- a/solr/modules/jwt-auth/build.gradle +++ b/solr/modules/jwt-auth/build.gradle @@ -32,6 +32,7 @@ dependencies { constraintsOnly platform(libs.fasterxml.jackson.bom) implementation project(':solr:core') + implementation project(':solr:api') implementation project(':solr:solrj') implementation project(':solr:solrj-jetty') @@ -42,6 +43,8 @@ dependencies { implementation libs.google.guava implementation libs.slf4j.api implementation libs.jakarta.servlet.api + implementation libs.jakarta.ws.rsapi + implementation libs.swagger3.annotations.jakarta testImplementation project(':solr:test-framework') testImplementation libs.apache.lucene.testframework diff --git a/solr/modules/jwt-auth/gradle.lockfile b/solr/modules/jwt-auth/gradle.lockfile index 8a9d1765fd54..7e16c37db5f0 100644 --- a/solr/modules/jwt-auth/gradle.lockfile +++ b/solr/modules/jwt-auth/gradle.lockfile @@ -22,6 +22,7 @@ com.google.errorprone:error_prone_annotation:2.41.0=annotationProcessor,errorpro com.google.errorprone:error_prone_annotations:2.41.0=annotationProcessor,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_check_api:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_core:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.errorprone:javac:9+181-r4173-1=errorproneJavac com.google.googlejavaformat:google-java-format:1.27.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.guava:failureaccess:1.0.3=annotationProcessor,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.guava:guava:33.4.8-jre=annotationProcessor,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath @@ -86,7 +87,7 @@ jakarta.annotation:jakarta.annotation-api:2.1.1=jarValidation,runtimeClasspath,r jakarta.inject:jakarta.inject-api:2.0.1=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath jakarta.servlet:jakarta.servlet-api:6.1.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath jakarta.validation:jakarta.validation-api:3.0.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath -jakarta.ws.rs:jakarta.ws.rs-api:3.1.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath +jakarta.ws.rs:jakarta.ws.rs-api:3.1.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.18.3=jarValidation,testCompileClasspath,testRuntimeClasspath diff --git a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java index 4c4c35eaabbd..33ff27228973 100644 --- a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java +++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java @@ -44,20 +44,15 @@ import java.util.StringTokenizer; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.apache.solr.api.AnnotatedApi; -import org.apache.solr.api.Api; import org.apache.solr.client.solrj.jetty.HttpJettySolrClient; import org.apache.solr.common.SolrException; -import org.apache.solr.common.SpecProvider; import org.apache.solr.common.util.CommandOperation; import org.apache.solr.common.util.StrUtils; 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.AuthenticationPlugin; import org.apache.solr.security.ConfigEditablePlugin; import org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode; -import org.apache.solr.security.jwt.api.ModifyJWTAuthPluginConfigAPI; import org.apache.solr.servlet.LoadAdminUiServlet; import org.apache.solr.util.CryptoKeys; import org.eclipse.jetty.client.Request; @@ -75,8 +70,7 @@ import org.slf4j.LoggerFactory; /** Authentication plugin that finds logged in user by validating the signature of a JWT token */ -public class JWTAuthPlugin extends AuthenticationPlugin - implements SpecProvider, ConfigEditablePlugin { +public class JWTAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final String PARAM_BLOCK_UNKNOWN = "blockUnknown"; private static final String PARAM_REQUIRE_ISSUER = "requireIss"; @@ -776,12 +770,6 @@ public void close() { jwtConsumer = null; } - @Override - public ValidatingJsonMap getSpec() { - final List apis = AnnotatedApi.getApis(new ModifyJWTAuthPluginConfigAPI()); - return apis.get(0).getSpec(); - } - /** * Operate the commands on the latest conf and return a new conf object If there are errors in the * commands , throw a SolrException. return a null if no changes are to be made as a result of diff --git a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/api/ModifyJWTAuthPluginConfigAPI.java b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/api/ModifyJWTAuthPluginConfigAPI.java index 7fc216462206..a71d5e7da966 100644 --- a/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/api/ModifyJWTAuthPluginConfigAPI.java +++ b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/api/ModifyJWTAuthPluginConfigAPI.java @@ -17,23 +17,24 @@ package org.apache.solr.security.jwt.api; -import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST; -import static org.apache.solr.security.PermissionNameProvider.Name.SECURITY_EDIT_PERM; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import org.apache.solr.client.api.model.SolrJerseyResponse; +import org.apache.solr.jersey.PermissionName; +import org.apache.solr.security.PermissionNameProvider; -import org.apache.solr.api.Command; -import org.apache.solr.api.EndPoint; -import org.apache.solr.api.PayloadObj; +/** V2 API interface for modifying configuration for Solr's JWTAuthPlugin. */ +@Path("/cluster/security/authentication") +public interface ModifyJWTAuthPluginConfigAPI { -/** V2 API for modifying configuration for Solr's JWTAuthPlugin. */ -@EndPoint( - path = {"/cluster/security/authentication"}, - method = POST, - permission = SECURITY_EDIT_PERM) -public class ModifyJWTAuthPluginConfigAPI { - - @Command(name = "set-property") - public void setProperties(PayloadObj propertyPayload) { - // Method stub used only to produce v2 API spec; implementation/body empty. - throw new IllegalStateException(); - } + @POST + @PermissionName(PermissionNameProvider.Name.SECURITY_EDIT_PERM) + @Operation( + summary = "Update the JWT authentication plugin configuration.", + tags = {"security"}) + SolrJerseyResponse updateJwtAuthPluginConfig( + @RequestBody(description = "JWT plugin configuration to update", required = true) + JWTConfigurationPayload propertyPayload); } diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginConfigApiTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginConfigApiTest.java new file mode 100644 index 000000000000..f918c82d4e17 --- /dev/null +++ b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginConfigApiTest.java @@ -0,0 +1,138 @@ +/* + * 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. + */ +package org.apache.solr.security.jwt; + +import static org.apache.solr.security.jwt.JWTAuthPluginTest.JWT_TEST_PATH; + +import java.util.Map; +import org.apache.http.client.HttpClient; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.apache.HttpClientUtil; +import org.apache.solr.client.solrj.request.V2Request; +import org.apache.solr.cloud.SolrCloudAuthTestCase; +import org.apache.solr.security.jwt.api.JWTConfigurationPayload; +import org.jose4j.jwk.PublicJsonWebKey; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Integration test that calls the JWT authentication plugin configuration API using SolrClient, + * verifying end-to-end that the V2 API endpoint works and that unauthenticated requests are + * rejected. + */ +@SolrTestCaseJ4.SuppressSSL +public class JWTAuthPluginConfigApiTest extends SolrCloudAuthTestCase { + + // RSA key pair matching the public key embedded in jwt_plugin_jwk_security.json + private static final String JWK_JSON = + """ + { + "kty": "RSA", + "d": "i6pyv2z3o-MlYytWsOr3IE1olu2RXZBzjPRBNgWAP1TlLNaphHEvH5aHhe_CtBAastgFFMuP29CFhaL3_tGczkvWJkSveZQN2AHWHgRShKgoSVMspkhOt3Ghha4CvpnZ9BnQzVHnaBnHDTTTfVgXz7P1ZNBhQY4URG61DKIF-JSSClyh1xKuMoJX0lILXDYGGcjVTZL_hci4IXPPTpOJHV51-pxuO7WU5M9252UYoiYyCJ56ai8N49aKIMsqhdGuO4aWUwsGIW4oQpjtce5eEojCprYl-9rDhTwLAFoBtjy6LvkqlR2Ae5dKZYpStljBjK8PJrBvWZjXAEMDdQ8PuQ", + "e": "AQAB", + "use": "sig", + "kid": "test", + "alg": "RS256", + "n": "jeyrvOaZrmKWjyNXt0myAc_pJ1hNt3aRupExJEx1ewPaL9J9HFgSCjMrYxCB1ETO1NDyZ3nSgjZis-jHHDqBxBjRdq_t1E2rkGFaYbxAyKt220Pwgme_SFTB9MXVrFQGkKyjmQeVmOmV6zM3KK8uMdKQJ4aoKmwBcF5Zg7EZdDcKOFgpgva1Jq-FlEsaJ2xrYDYo3KnGcOHIt9_0NQeLsqZbeWYLxYni7uROFncXYV5FhSJCeR4A_rrbwlaCydGxE0ToC_9HNYibUHlkJjqyUhAgORCbNS8JLCJH8NUi5sDdIawK9GTSyvsJXZ-QHqo4cMUuxWV5AJtaRGghuMUfqQ" + }"""; + + private static String jwtTestToken; + + @BeforeClass + public static void initJwtToken() throws Exception { + PublicJsonWebKey jwk = RsaJsonWebKey.Factory.newPublicJwk(JWK_JSON); + JwtClaims claims = JWTAuthPluginTest.generateClaims(); + JsonWebSignature jws = new JsonWebSignature(); + jws.setPayload(claims.toJson()); + jws.setKey(jwk.getPrivateKey()); + jws.setKeyIdHeaderValue(jwk.getKeyId()); + jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); + jwtTestToken = jws.getCompactSerialization(); + } + + @Override + @After + public void tearDown() throws Exception { + shutdownCluster(); + super.tearDown(); + } + + /** + * Verifies that the JWT authentication plugin configuration can be updated via the V2 API using + * SolrClient, and that unauthenticated requests are rejected with a 401. + */ + @Test + public void testUpdateJwtConfigViaSolrClientV2Api() throws Exception { + cluster = + configureCluster(2) + .withSecurityJson( + JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_security.json")) + .addConfig( + "conf1", + JWT_TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf")) + .withDefaultClusterProperty("useLegacyReplicaAssignment", "false") + .build(); + cluster.waitForAllNodes(10); + + String baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString(); + String v2AuthcUrl = baseUrl + "/____v2/cluster/security/authentication"; + HttpClient httpClient = HttpClientUtil.createClient(null); + try { + // Confirm initial state: blockUnknown=true (as configured in the security JSON) + verifySecurityStatus( + httpClient, v2AuthcUrl, "authentication/blockUnknown", "true", 20, "Bearer " + jwtTestToken); + + // An unauthenticated POST is rejected (no JWT token → 401) + JWTConfigurationPayload unauthPayload = new JWTConfigurationPayload(); + unauthPayload.blockUnknown = false; + V2Request unauthRequest = + new V2Request.Builder("/cluster/security/authentication") + .withMethod(SolrRequest.METHOD.POST) + .withPayload(Map.of("set-property", unauthPayload)) + .build(); + try (SolrClient unauthClient = getHttpSolrClient(baseUrl)) { + expectThrows(Exception.class, () -> unauthClient.request(unauthRequest)); + } + + // An authenticated POST with a valid JWT token succeeds and the config is updated + JWTConfigurationPayload updatePayload = new JWTConfigurationPayload(); + updatePayload.blockUnknown = false; + V2Request updateRequest = + new V2Request.Builder("/cluster/security/authentication") + .withMethod(SolrRequest.METHOD.POST) + .withPayload(Map.of("set-property", updatePayload)) + .build(); + updateRequest.addHeader("Authorization", "Bearer " + jwtTestToken); + try (SolrClient solrClient = getHttpSolrClient(baseUrl)) { + solrClient.request(updateRequest); + } + + // Verify blockUnknown was changed to false + verifySecurityStatus( + httpClient, v2AuthcUrl, "authentication/blockUnknown", "false", 20, "Bearer " + jwtTestToken); + } finally { + HttpClientUtil.close(httpClient); + } + } +} diff --git a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/api/V2JWTSecurityApiMappingTest.java b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/api/V2JWTSecurityApiMappingTest.java deleted file mode 100644 index 8c55dbfcb9c6..000000000000 --- a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/api/V2JWTSecurityApiMappingTest.java +++ /dev/null @@ -1,65 +0,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. - */ - -package org.apache.solr.security.jwt.api; - -import java.util.HashMap; -import org.apache.solr.SolrTestCaseJ4; -import org.apache.solr.api.AnnotatedApi; -import org.apache.solr.api.Api; -import org.apache.solr.api.ApiBag; -import org.junit.Before; -import org.junit.Test; - -public class V2JWTSecurityApiMappingTest extends SolrTestCaseJ4 { - - private ApiBag apiBag; - - @Before - public void setupApiBag() { - apiBag = new ApiBag(false); - } - - @Test - public void testJwtConfigApiMapping() { - apiBag.registerObject(new ModifyJWTAuthPluginConfigAPI()); - - // Authc API - final AnnotatedApi updateAuthcConfig = - assertAnnotatedApiExistsFor("POST", "/cluster/security/authentication"); - assertEquals(1, updateAuthcConfig.getCommands().size()); - assertEquals("set-property", updateAuthcConfig.getCommands().keySet().iterator().next()); - } - - private AnnotatedApi assertAnnotatedApiExistsFor(String method, String path) { - final HashMap parts = new HashMap<>(); - final Api api = apiBag.lookup(path, method, parts); - if (api == null) { - fail("Expected to find API for path [" + path + "], but no API mapping found."); - } - if (!(api instanceof AnnotatedApi)) { - fail( - "Expected AnnotatedApi for path [" - + path - + "], but found non-annotated API [" - + api - + "]"); - } - - return (AnnotatedApi) api; - } -} diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc index 9c8551033f1b..2b15c47e88bd 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/jwt-authentication-plugin.adoc @@ -206,9 +206,16 @@ but also allow a small set of service accounts to use `Basic` authentication whe == Editing JWT Authentication Plugin Configuration -All properties mentioned above can be set or changed using the xref:basic-authentication-plugin.adoc#editing-basic-authentication-plugin-configuration[Authentication API]. +All properties mentioned above can be set or changed using the Authentication API. You can thus start with a simple configuration with only `class` and `blockUnknown=false` configured and then configure the rest using the API. +=== API Entry Point + +* v1: `\http://localhost:8983/solr/admin/authentication` +* v2: `\http://localhost:8983/api/cluster/security/authentication` + +This endpoint is not collection-specific, so configuration applies to the entire Solr cluster. + === Set a Configuration Property Set properties for the authentication plugin.