From dd54f90748cce12b9bce5028ada541b91f41a379 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 27 Nov 2024 13:08:06 +0000 Subject: [PATCH 1/3] Use CertificateGenerator in OIDC Wiremock tests (cherry picked from commit ebb2c0c3aef2a0fc6faccd24a9ef6536374b13bf) --- integration-tests/oidc-wiremock/pom.xml | 5 ++ .../keycloak/CustomTenantConfigResolver.java | 2 +- .../src/main/resources/application.properties | 12 ++-- .../main/resources/truststore-rootcert.p12 | Bin 1862 -> 0 bytes .../src/main/resources/truststore.p12 | Bin 3238 -> 0 bytes .../BearerTokenAuthorizationTest.java | 67 +++++++----------- .../keycloak/CodeFlowAuthorizationTest.java | 2 - .../CustomOidcWiremockTestResource.java | 59 +++++++++++++++ .../io/quarkus/it/keycloak/TestUtils.java | 29 ++++++-- .../src/test/resources/ca.cert.pem | 34 --------- .../src/test/resources/intermediate.cert.pem | 33 --------- .../resources/www.quarkustest.com.cert.pem | 29 -------- .../resources/www.quarkustest.com.key.pem | 28 -------- 13 files changed, 120 insertions(+), 180 deletions(-) delete mode 100644 integration-tests/oidc-wiremock/src/main/resources/truststore-rootcert.p12 delete mode 100644 integration-tests/oidc-wiremock/src/main/resources/truststore.p12 create mode 100644 integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CustomOidcWiremockTestResource.java delete mode 100644 integration-tests/oidc-wiremock/src/test/resources/ca.cert.pem delete mode 100644 integration-tests/oidc-wiremock/src/test/resources/intermediate.cert.pem delete mode 100644 integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.cert.pem delete mode 100644 integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.key.pem diff --git a/integration-tests/oidc-wiremock/pom.xml b/integration-tests/oidc-wiremock/pom.xml index dfddc45a735e1..0f6a6fb5e5fb3 100644 --- a/integration-tests/oidc-wiremock/pom.xml +++ b/integration-tests/oidc-wiremock/pom.xml @@ -43,6 +43,11 @@ rest-assured test + + io.smallrye.certs + smallrye-certificate-generator + test + org.htmlunit htmlunit diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java index 78ff79e9f8d16..9171e9071ba6d 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java @@ -71,7 +71,7 @@ public Uni resolve(RoutingContext context, } else if (path.endsWith("bearer-certificate-full-chain-root-only")) { OidcTenantConfig config = new OidcTenantConfig(); config.setTenantId("bearer-certificate-full-chain-root-only"); - config.getCertificateChain().setTrustStoreFile(Path.of("truststore-rootcert.p12")); + config.getCertificateChain().setTrustStoreFile(Path.of("target/chain/truststore-rootcert.p12")); config.getCertificateChain().setTrustStorePassword("storepassword"); config.getCertificateChain().setLeafCertificateName("www.quarkustest.com"); return Uni.createFrom().item(config); diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index f3d532751976e..ebf73424c3731 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -143,7 +143,7 @@ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.authentication.verify- quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.client-id=quarkus-web-app quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.credentials.client-secret.value=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.credentials.client-secret.method=query -quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.certificate-chain.trust-store-file=target/chain/truststore.p12 quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.certificate-chain.trust-store-password=storepassword @@ -172,7 +172,7 @@ quarkus.oidc.bearer-kid-or-chain.client-id=quarkus-app quarkus.oidc.bearer-kid-or-chain.credentials.secret=secret quarkus.oidc.bearer-kid-or-chain.token.audience=https://service.example.com quarkus.oidc.bearer-kid-or-chain.allow-token-introspection-cache=false -quarkus.oidc.bearer-kid-or-chain.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.bearer-kid-or-chain.certificate-chain.trust-store-file=target/chain/truststore.p12 quarkus.oidc.bearer-kid-or-chain.certificate-chain.trust-store-password=storepassword quarkus.oidc.bearer-id.auth-server-url=${keycloak.url}/realms/quarkus/ @@ -199,7 +199,7 @@ quarkus.oidc.bearer-azure.jwks-path=${keycloak.url}/azure/jwk quarkus.oidc.bearer-azure.jwks.resolve-early=false quarkus.oidc.bearer-azure.token.lifespan-grace=2147483647 quarkus.oidc.bearer-azure.token.customizer-name=azure-access-token-customizer -quarkus.oidc.bearer-azure.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.bearer-azure.certificate-chain.trust-store-file=target/chain/truststore.p12 quarkus.oidc.bearer-azure.certificate-chain.trust-store-password=storepassword quarkus.oidc.bearer-role-claim-path.auth-server-url=${keycloak.url}/realms/quarkus/ @@ -215,14 +215,14 @@ quarkus.oidc.bearer-no-introspection.credentials.secret=secret quarkus.oidc.bearer-no-introspection.token.audience=https://service.example.com quarkus.oidc.bearer-no-introspection.token.allow-jwt-introspection=false -quarkus.oidc.bearer-certificate-full-chain.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.bearer-certificate-full-chain.certificate-chain.trust-store-file=target/chain/truststore.p12 quarkus.oidc.bearer-certificate-full-chain.certificate-chain.trust-store-password=storepassword -quarkus.oidc.bearer-chain-custom-validator.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.bearer-chain-custom-validator.certificate-chain.trust-store-file=target/chain/truststore.p12 quarkus.oidc.bearer-chain-custom-validator.certificate-chain.trust-store-password=storepassword quarkus.oidc.bearer-chain-custom-validator.token.audience=https://service.example.com -quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.trust-store-file=truststore-rootcert.p12 +quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.trust-store-file=target/chain/truststore-rootcert.p12 quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.trust-store-password=storepassword quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.leaf-certificate-name=www.quarkusio.com diff --git a/integration-tests/oidc-wiremock/src/main/resources/truststore-rootcert.p12 b/integration-tests/oidc-wiremock/src/main/resources/truststore-rootcert.p12 deleted file mode 100644 index e6a5a80173a456b27d3b92838280ea0e24f0e5fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1862 zcmV-M2f6q#f(JqZ0Ru3C2J8k2Duzgg_YDCD0ic2g-2{RL*)W0z)i8nv(FO@BhDe6@ z4FLxRpn?X%FoFiY0s#Opf(Ew+2`Yw2hW8Bt2LUi<1_>&LNQU+thDZTr0|Wso1Q3gq`{dQWc@bB20%|E54N$P+QzS#6<4_SJ8X{N z^QIqSz3_PeGJbvEsINt?>p`bfyt(L#mvQGtN1wG*OS1`~6FT z*+=sLB~J!X#%6@SQG$rib9=|MyseFVrIEAJrT*WtNyBd zc+KPna<_%8YdmAO-rQi>?7JBq+1Odgx zg2!gIcZ2(D?;;QlT#`K41vGP**m4K zv&!3S1P6KCzyvThOKnn`kUzM*fknOk=PXaC>OSDWPpX z2tjKugnA#<(L$WkaV@~wDkcfO6I!n2DF#N&>@3xYbok7xZQmZur=mK*%sHLEy%kCS zYChphDX5ZlfNSdYhsp_V@LgObgUwX6*QoyTvzemWxZJO6`57ub_58O6>IByZRJ<*#!@!(E#ok#YX zcNxACg~_DSmO;tAZ2)q|rYT;2`sKKX?Gc)b zoKra#A*gv(nFXoDoS#wCunoTUE(w|r1(Wa+$cFV)QF3Ic%s|KQRzfkBjN=Sir_~;E z>N3y#B7`V1RBPNz*Nhn-FygYiTE1LM0zO#NB+vY;CVo&UIL$t=+B@_sv*HiC=X9_r z)S=UHiOPAlP0^y=XA?K9K9-&dz&Q293PM!+f>=avLlIT2lL{3dXh##?ZXV~wtrC7C@pC4m0+S9_u zc;;>IvwrZ|#ksofds!Z%=MgRxgj)TY@12e zbvw463b;1Ohy-uBV@7D?&O>cao#!aEEH;Y6MbR(Ac`jqfyXW`FXo0o!4iAlp0{ARh zqp5cis&e>c;af#vM}kz;`6KTMgFZIlRPsUqY2(I%$(WL@^IoC*&Wu(%9RZ5$fE$};f)I74cVZ)JwLG?G3Sli|}C0W)3 zU^L(P*`7J~)-;R_Zr}g|30#FNC9O71OfpC00bb}ni)SqkMA-2 zKt1@jFp48R!rV!`zZ3cq6tZ&=4~0hr6gSQPH-h^YW8?LM;J@nkEt7F+?g9cQ5J6;z AH~;_u diff --git a/integration-tests/oidc-wiremock/src/main/resources/truststore.p12 b/integration-tests/oidc-wiremock/src/main/resources/truststore.p12 deleted file mode 100644 index b0c1f8bcb4164b99b5efca0cf4ab719bb66d7210..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3238 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 YeayXOacNY5cYrvK>z>% diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index d6f5bb2efbd48..83b2438ddfc11 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -22,7 +22,6 @@ import org.awaitility.Awaitility; import org.hamcrest.Matchers; import org.jose4j.jwx.HeaderParameterNames; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.WireMockServer; @@ -39,12 +38,10 @@ import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; import io.smallrye.jwt.build.JwtClaimsBuilder; -import io.smallrye.jwt.util.KeyUtils; -import io.smallrye.jwt.util.ResourceUtils; import io.vertx.core.json.JsonObject; @QuarkusTest -@QuarkusTestResource(OidcWiremockTestResource.class) +@QuarkusTestResource(CustomOidcWiremockTestResource.class) public class BearerTokenAuthorizationTest { @OidcWireMock @@ -62,7 +59,6 @@ public void testSecureAccessSuccessPreferredUsername() { } @Test - @Disabled public void testAccessResourceAzure() throws Exception { String azureToken = readFile("token.txt"); String azureJwk = readFile("jwks.json"); @@ -192,16 +188,13 @@ public void testAccessAdminResourceWithWrongCertS256Thumbprint() { } @Test - @Disabled public void testCertChainWithCustomValidator() throws Exception { - X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); - X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); - X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); - PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); + List chain = TestUtils.loadCertificateChain(); + PrivateKey subjectPrivateKey = TestUtils.loadLeafCertificatePrivateKey(); // Send the token with the valid certificate chain and bind it to the token claim String accessToken = getAccessTokenForCustomValidator( - List.of(subjectCert, intermediateCert, rootCert), + chain, subjectPrivateKey, "https://service.example.com", true, false); RestAssured.given().auth().oauth2(accessToken) @@ -212,7 +205,7 @@ public void testCertChainWithCustomValidator() throws Exception { // Send the token with the valid certificate chain but do not bind it to the token claim accessToken = getAccessTokenForCustomValidator( - List.of(subjectCert, intermediateCert, rootCert), + chain, subjectPrivateKey, "https://service.example.com", false, false); RestAssured.given().auth().oauth2(accessToken) @@ -222,7 +215,7 @@ public void testCertChainWithCustomValidator() throws Exception { // Send the token with the valid certificate chain bound to the token claim, but expired accessToken = getAccessTokenForCustomValidator( - List.of(subjectCert, intermediateCert, rootCert), + chain, subjectPrivateKey, "https://service.example.com", true, true); RestAssured.given().auth().oauth2(accessToken) .when().get("/api/admin/bearer-chain-custom-validator") @@ -231,7 +224,7 @@ public void testCertChainWithCustomValidator() throws Exception { // Send the token with the valid certificate chain but with the wrong audience accessToken = getAccessTokenForCustomValidator( - List.of(subjectCert, intermediateCert, rootCert), + chain, subjectPrivateKey, "https://server.example.com", true, false); RestAssured.given().auth().oauth2(accessToken) @@ -242,16 +235,14 @@ public void testCertChainWithCustomValidator() throws Exception { } @Test - @Disabled public void testAccessAdminResourceWithFullCertChain() throws Exception { - X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); - X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); - X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); - PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); + // index 2 - root, index 1 - intermediate, index 0 - leaf + List chain = TestUtils.loadCertificateChain(); + PrivateKey subjectPrivateKey = TestUtils.loadLeafCertificatePrivateKey(); // Send the token with the valid certificate chain and bind it to the token claim String accessToken = getAccessTokenWithCertChain( - List.of(subjectCert, intermediateCert, rootCert), + chain, subjectPrivateKey); RestAssured.given().auth().oauth2(accessToken) @@ -268,7 +259,7 @@ public void testAccessAdminResourceWithFullCertChain() throws Exception { // Send the token with the valid certificate chain, but with the token signed by a non-matching private key accessToken = getAccessTokenWithCertChain( - List.of(subjectCert, intermediateCert, rootCert), + chain, KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate()); RestAssured.given().auth().oauth2(accessToken) .when().get("/api/admin/bearer-certificate-full-chain") @@ -277,7 +268,7 @@ public void testAccessAdminResourceWithFullCertChain() throws Exception { // Send the token with the valid certificates but which are in the wrong order in the chain accessToken = getAccessTokenWithCertChain( - List.of(intermediateCert, subjectCert, rootCert), + List.of(chain.get(1), chain.get(0), chain.get(2)), subjectPrivateKey); RestAssured.given().auth().oauth2(accessToken) .when().get("/api/admin/bearer-certificate-full-chain") @@ -286,7 +277,7 @@ public void testAccessAdminResourceWithFullCertChain() throws Exception { // Send the token with the valid certificates but with the intermediate one omitted from the chain accessToken = getAccessTokenWithCertChain( - List.of(subjectCert, rootCert), + List.of(chain.get(0), chain.get(2)), subjectPrivateKey); RestAssured.given().auth().oauth2(accessToken) .when().get("/api/admin/bearer-certificate-full-chain") @@ -295,7 +286,7 @@ public void testAccessAdminResourceWithFullCertChain() throws Exception { // Send the token with the only the last valid certificate accessToken = getAccessTokenWithCertChain( - List.of(subjectCert), + List.of(chain.get(0)), subjectPrivateKey); RestAssured.given().auth().oauth2(accessToken) .when().get("/api/admin/bearer-certificate-full-chain") @@ -305,16 +296,13 @@ public void testAccessAdminResourceWithFullCertChain() throws Exception { } @Test - @Disabled public void testFullCertChainWithOnlyRootInTruststore() throws Exception { - X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); - X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); - X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); - PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); + List chain = TestUtils.loadCertificateChain(); + PrivateKey subjectPrivateKey = TestUtils.loadLeafCertificatePrivateKey(); // Send the token with the valid certificate chain String accessToken = getAccessTokenWithCertChain( - List.of(subjectCert, intermediateCert, rootCert), + chain, subjectPrivateKey); RestAssured.given().auth().oauth2(accessToken) @@ -331,7 +319,7 @@ public void testFullCertChainWithOnlyRootInTruststore() throws Exception { // Send the token with the valid certificates but which are in the wrong order in the chain accessToken = getAccessTokenWithCertChain( - List.of(intermediateCert, subjectCert, rootCert), + List.of(chain.get(1), chain.get(0), chain.get(2)), subjectPrivateKey); RestAssured.given().auth().oauth2(accessToken) .when().get("/api/admin/bearer-certificate-full-chain-root-only") @@ -340,7 +328,7 @@ public void testFullCertChainWithOnlyRootInTruststore() throws Exception { // Send the token with the valid certificates but with the intermediate one omitted from the chain accessToken = getAccessTokenWithCertChain( - List.of(subjectCert, rootCert), + List.of(chain.get(0), chain.get(2)), subjectPrivateKey); RestAssured.given().auth().oauth2(accessToken) .when().get("/api/admin/bearer-certificate-full-chain-root-only") @@ -349,7 +337,7 @@ public void testFullCertChainWithOnlyRootInTruststore() throws Exception { // Send the token with the only the last valid certificate accessToken = getAccessTokenWithCertChain( - List.of(subjectCert), + List.of(chain.get(0)), subjectPrivateKey); RestAssured.given().auth().oauth2(accessToken) .when().get("/api/admin/bearer-certificate-full-chain-root-only") @@ -358,7 +346,6 @@ public void testFullCertChainWithOnlyRootInTruststore() throws Exception { } @Test - @Disabled public void testAccessAdminResourceWithKidOrChain() throws Exception { // token with a matching kid, not x5c String token = Jwt.preferredUserName("admin") @@ -403,14 +390,12 @@ public void testAccessAdminResourceWithKidOrChain() throws Exception { .then() .statusCode(401); - X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); - X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); - X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); - PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); + List chain = TestUtils.loadCertificateChain(); + PrivateKey subjectPrivateKey = TestUtils.loadLeafCertificatePrivateKey(); // Send the token with the valid certificate chain token = getAccessTokenWithCertChain( - List.of(subjectCert, intermediateCert, rootCert), + chain, subjectPrivateKey); TestUtils.assertX5cOnlyIsPresent(token); @@ -429,7 +414,7 @@ public void testAccessAdminResourceWithKidOrChain() throws Exception { // Send the token with the valid certificate chain with certificates in the wrong order token = getAccessTokenWithCertChain( - List.of(intermediateCert, subjectCert, rootCert), + List.of(chain.get(1), chain.get(0), chain.get(2)), subjectPrivateKey); TestUtils.assertX5cOnlyIsPresent(token); @@ -445,7 +430,7 @@ public void testAccessAdminResourceWithKidOrChain() throws Exception { .groups(Set.of("admin")) .issuer("https://server.example.com") .audience("https://service.example.com") - .jws().keyId("1").chain(List.of(intermediateCert, subjectCert, rootCert)) + .jws().keyId("1").chain(List.of(chain.get(1), chain.get(0), chain.get(2))) .sign(subjectPrivateKey); assertBothKidAndX5cArePresent(token, "1"); diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index d5845341e9648..0741e298c5236 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -50,7 +50,6 @@ import org.htmlunit.util.Cookie; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.WireMockServer; @@ -337,7 +336,6 @@ public void testCodeFlowUserInfo() throws Exception { } @Test - @Disabled public void testCodeFlowUserInfoCachedInIdToken() throws Exception { // Internal ID token, allow in memory cache = false, cacheUserInfoInIdtoken = true final String refreshJwtToken = generateAlreadyExpiredRefreshToken(); diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CustomOidcWiremockTestResource.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CustomOidcWiremockTestResource.java new file mode 100644 index 0000000000000..3cd5564e7b7d6 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CustomOidcWiremockTestResource.java @@ -0,0 +1,59 @@ +package io.quarkus.it.keycloak; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Map; + +import io.quarkus.test.oidc.server.OidcWiremockTestResource; +import io.smallrye.certs.chain.CertificateChainGenerator; +import io.smallrye.jwt.util.KeyUtils; + +public class CustomOidcWiremockTestResource extends OidcWiremockTestResource { + @Override + public Map start() { + try { + generateCertificates(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + return super.start(); + } + + private void generateCertificates() throws Exception { + File chainDir = new File("target/chain"); + CertificateChainGenerator chainGenerator = new CertificateChainGenerator(chainDir) + .withCN("www.quarkustest.com"); + chainGenerator.generate(); + + Path rootCertPath = Paths.get("target/chain/root.crt"); + X509Certificate rootCert = KeyUtils.getCertificate(Files.readString(rootCertPath)); + + Path leafCertPath = Paths.get("target/chain/www.quarkustest.com.crt"); + X509Certificate leafCert = KeyUtils.getCertificate(Files.readString(leafCertPath)); + + File trustStore = new File(chainDir, "truststore.p12"); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + keyStore.setCertificateEntry("root", rootCert); + keyStore.setCertificateEntry("leaf", leafCert); + var fos = new FileOutputStream(trustStore); + keyStore.store(fos, "storepassword".toCharArray()); + fos.close(); + + File trustStoreRoot = new File(chainDir, "truststore-rootcert.p12"); + KeyStore keyStoreRootCert = KeyStore.getInstance("PKCS12"); + keyStoreRootCert.load(null, null); + keyStoreRootCert.setCertificateEntry("root", rootCert); + var fosRootCert = new FileOutputStream(trustStoreRoot); + keyStoreRootCert.store(fosRootCert, "storepassword".toCharArray()); + fosRootCert.close(); + + } + +} diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java index 591ca8c360f4d..c875b28822607 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java @@ -3,6 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.List; @@ -11,19 +14,33 @@ import io.quarkus.oidc.runtime.TrustStoreUtils; import io.smallrye.jwt.build.Jwt; import io.smallrye.jwt.util.KeyUtils; -import io.smallrye.jwt.util.ResourceUtils; import io.vertx.core.json.JsonObject; public class TestUtils { + public static List loadCertificateChain() throws Exception { + Path rootCertPath = Paths.get("target/chain/root.crt"); + Path intermediateCertPath = Paths.get("target/chain/intermediate.crt"); + Path leafCertPath = Paths.get("target/chain/www.quarkustest.com.crt"); + + X509Certificate rootCert = KeyUtils.getCertificate(Files.readString(rootCertPath)); + X509Certificate intermediateCert = KeyUtils.getCertificate(Files.readString(intermediateCertPath)); + X509Certificate subjectCert = KeyUtils.getCertificate(Files.readString(leafCertPath)); + + return List.of(subjectCert, intermediateCert, rootCert); + } + + public static PrivateKey loadLeafCertificatePrivateKey() throws Exception { + Path leafKeyPath = Paths.get("target/chain/www.quarkustest.com.key"); + return KeyUtils.decodePrivateKey(Files.readString(leafKeyPath)); + } + public static String createTokenWithInlinedCertChain(String preferredUserName) throws Exception { - X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); - X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); - X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); - PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); + List chain = loadCertificateChain(); + PrivateKey subjectPrivateKey = loadLeafCertificatePrivateKey(); String bearerAccessToken = getAccessTokenWithCertChain( - List.of(subjectCert, intermediateCert, rootCert), + chain, subjectPrivateKey, preferredUserName); diff --git a/integration-tests/oidc-wiremock/src/test/resources/ca.cert.pem b/integration-tests/oidc-wiremock/src/test/resources/ca.cert.pem deleted file mode 100644 index b8ec4ac6c5dd1..0000000000000 --- a/integration-tests/oidc-wiremock/src/test/resources/ca.cert.pem +++ /dev/null @@ -1,34 +0,0 @@ ------BEGIN CERTIFICATE----- -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 -W+0+5Lc= ------END CERTIFICATE----- diff --git a/integration-tests/oidc-wiremock/src/test/resources/intermediate.cert.pem b/integration-tests/oidc-wiremock/src/test/resources/intermediate.cert.pem deleted file mode 100644 index 27dc46ad3ff27..0000000000000 --- a/integration-tests/oidc-wiremock/src/test/resources/intermediate.cert.pem +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFuTCCA6GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCSUUx -DzANBgNVBAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVibGluMRAwDgYDVQQKDAdRdWFy -a3VzMRswGQYDVQQLDBJRdWFya3VzIERlcGFydG1lbnQxEDAOBgNVBAMMB1Jvb3Qg -Q0EwHhcNMjMxMTE3MTIzMDIwWhcNMzMxMTE0MTIzMDIwWjBnMQswCQYDVQQGEwJJ -RTEPMA0GA1UECAwGRHVibGluMRAwDgYDVQQKDAdRdWFya3VzMRswGQYDVQQLDBJR -dWFya3VzIERlcGFydG1lbnQxGDAWBgNVBAMMD0ludGVybWVkaWF0ZSBDQTCCAiIw -DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMFfN7Pi8dF+gQTglUDbnTsunr10 -0Ldeozyg4UK+V59NJ8FSX9k1OZeSIjqEN3kljtjNzKX7eqwIw/8UjboQeusIM9jM -x+cu0ifWlulu/TBLzZfxO3Laq0lSJWuoPRt9tDAdA1PJK62p044zp6i6B9PDldhi -RpZPj7PJcAoEh3NTmzh7icKVmcGfj2Xo+M/TOiGtKsIhH34w/aXi6u/03PsBs0gw -8Lids9WTUFGIvf4jAeCzuxWL7RQVr7qDhEvlKEh1tRknSUf0W2yJCE+aFD3XL/b2 -r6qc+CbsV59n+IcARH7gFDEBkAdk9lBozmF7o7+ADc5CZAjtN2FGWDhLXoCV8fvT -4/sGLsT/MGZPUS4vqw+Gl3+Qx0qk+DgVrwWGFPX168vXBEB/f+AsCX3O9Hn6vNjB -uEoIi6+bZP6P2MHThARzOm705cM9xxvj82qrdpYtxhi4jLAwzZ/Aa+1wTu1Uobub -9LXoyuNHSppuK1gi4DZYUHs9YkQwVTwGu1cyYI9oCy9tZ90YqDHHQCyNqAFEPyjw -C9JT1B3g3gZiiPfpVtzvqxG8qNC6fYyYDSq4aNjlCfv8jYBPylByUyVMG5QnS0ai -5lYhUF6L8v1jWrsMvkBCmua4TvP4ofa8qUNOpS5eKTNiWk8wkuGne8MhSPDAExVO -z82E3S8s4jdpwWgHAgMBAAGjZjBkMB0GA1UdDgQWBBRM+ZXzjUoJLnQsxPsFa6CJ -uIjsyTAfBgNVHSMEGDAWgBSjdDFDHtjprW5hclFqtSK/sz6gojASBgNVHRMBAf8E -CDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAAa8u -aDO6nrADunYa6ePJLmxpiMCpaJR6XcM3UkZkQ+zdHsPLPl9lbN2tbr982CWbr89Q -QmkHMxylQQ4QLwUM6USzDohIiQ516I0LcGUatTymVXKRwSZc1xK587v4iI7LVy5P -pQuBMeA+tteldyaaWTDL3ppa1UmNWksS3MDHOYcJ/GWqsCqD1Au0sj1E3bGW4t6b -Wkan6gUG7z1IpXN8XOVSgbzTkWnH89LJns6YUHMnIXb+qQflLCbuj3TYa5H5JgOC -atVJEHOtqCaDxNDH5t99zFrvLkYy/AJ2QMqMlLS0pWRBmRcaBXEQl7npenZOUgn9 -A0AKs3hoYl0aF0aVMmy7R1Rx+V0G7s3AFZ31QUuWRiy50QJmZg4qZYliZFMcFZrg -H4T1IKcF1IddU7/tUodaaCP6DT9HRufJ8VNw8kFeFYK414TgvyuViIpHHGuUOLDl -Ee6ONp3VkzY3sseXpmR14JRnT2JOL5yt1kaDc8VdyLe+v57NURNUB8s5s6x/oIYI -9BDT1paHb38C/g8E72emgRMs+LwABJJm72hBiKo4eI8uDYKSTKuzxF9JANMMXPG9 -wvCgyFzf1ySsFlFueAMDVZJtqD0SPqtbilfJJ6lUmzyWEb2WYaUDpSiqmNU4Dnw0 -b+x+3T3uJ8oAbV9xBA9aba6M+fkzjtZ0VakcFpM= ------END CERTIFICATE----- diff --git a/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.cert.pem b/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.cert.pem deleted file mode 100644 index cb05e37ef44da..0000000000000 --- a/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.cert.pem +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIE6zCCAtOgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwZzELMAkGA1UEBhMCSUUx -DzANBgNVBAgMBkR1YmxpbjEQMA4GA1UECgwHUXVhcmt1czEbMBkGA1UECwwSUXVh -cmt1cyBEZXBhcnRtZW50MRgwFgYDVQQDDA9JbnRlcm1lZGlhdGUgQ0EwHhcNMjMx -MTE3MTIzNjQ1WhcNMjQxMTI2MTIzNjQ1WjCBgDELMAkGA1UEBhMCSUUxDzANBgNV -BAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVibGluMRAwDgYDVQQKDAdRdWFya3VzMR8w -HQYDVQQLDBZRdWFya3VzSW50ZWdyYXRpb25UZXN0MRwwGgYDVQQDDBN3d3cucXVh -cmt1c3Rlc3QuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqLr -N3x2QK1oc5FeAiImBSq8ouaUJ5s9wZre7A/2RrM1ZTzUK/VyoynWaVxYIkjdGTpT -H6xYtz3+T6z/l0xHO/tugXGHDGEQOgstvh0E8C1DrdvIOqdPtNYUBW6Nw0NVrVwH -ClBDSFN5Xw89YhjydtETy11joKQ2X9SViDfCICOVpx0ml05Txc45CUJsDofEX5HQ -C0eG32cuemvMLouAFH9fMfoVrx9Yhy5vBrzlX22s0ig9bu53qQlNuzj5AUcNKUCM -NttRltptHmRiAnRzUIGiOhXuaz0oEIU80p82sVM78tfY4qXIu9LVWYM1qqpYielx -BkjC7GElG7Log4lf4QIDAQABo4GGMIGDMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEB -BAQDAgZAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNV -HSMEGDAWgBRM+ZXzjUoJLnQsxPsFa6CJuIjsyTAdBgNVHQ4EFgQUEQ404BYvLmtz -vfS6I3iW4vKM/4QwDQYJKoZIhvcNAQELBQADggIBAFRMZozul2oqjOhQG9todI8w -6woJk5b5VDf0c5zPvQntWeS8vv7Bysy7yqmfdZFqZXZstIfmX+USvE1XOuQl+2X7 -BHn958lwyiKNUwNVm27Luf5yKEtjZHqmCvfCjcGAt5vuyyV1JcmP4gzEvEiTByMq -A4VSw7+u8y0/kEJLpgoikQaBYgp5HPkqJ/EmI55QUKlIElX9cgJxz5ihdHw/EUxD -C0AvKxH4SoMGxAlplz+ncJp6Ru6EI51dE1tIUlLwsFF39GjZ2a7AQCzG3umqM5ui -sKI6l5DFU9HVoDbNrSJ0DWbvevC6jA9sGsQyUjwewrhrsOposR2NOS/RyMw7YdWi -XIg50TmkWOyEScF7PQQ43qYL7JZQx9fB5k5Tscb0tV/anmTjbSQZmAeTsiHkDPeq -hGeP5mnvIdETwS9AZyYFDam1xOPcFpnsN2MGGXIUBvI876rno3zZlNnq4ugbYYWw -GlG/C7dseXP2dyvTsalNNUqSjZoFpwrQDPBFjTNxtKjX3E0J9ATL9QHsvO/+UkdG -FFyKVAGsFkI0kYv6gaPoqPkJoLxK3wZJ/QXMLJPk/jz7jBz5YPwvR1huN4ZgE9A9 -UvFxgcHuDjsBaHd+DJeILv/O47ELLnVKjnmvACZt4WbxMzH4ZpcB1oN9zQ7RP/CK -YZrxGk8DGTBADYV4cHl2 ------END CERTIFICATE----- diff --git a/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.key.pem b/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.key.pem deleted file mode 100644 index 38080bc66d484..0000000000000 --- a/integration-tests/oidc-wiremock/src/test/resources/www.quarkustest.com.key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6ous3fHZArWhz -kV4CIiYFKryi5pQnmz3Bmt7sD/ZGszVlPNQr9XKjKdZpXFgiSN0ZOlMfrFi3Pf5P -rP+XTEc7+26BcYcMYRA6Cy2+HQTwLUOt28g6p0+01hQFbo3DQ1WtXAcKUENIU3lf -Dz1iGPJ20RPLXWOgpDZf1JWIN8IgI5WnHSaXTlPFzjkJQmwOh8RfkdALR4bfZy56 -a8wui4AUf18x+hWvH1iHLm8GvOVfbazSKD1u7nepCU27OPkBRw0pQIw221GW2m0e -ZGICdHNQgaI6Fe5rPSgQhTzSnzaxUzvy19jipci70tVZgzWqqliJ6XEGSMLsYSUb -suiDiV/hAgMBAAECggEAA6VZm3agt8A7dWB+WePRYtH0J+mBtOldMjpEhw+Dw9tN -3Hms+mPb1rCjSeEeLqNGQG8pfwmmnQPGw0cxogLBNHyDqt8tIHKH9t5PiTJ3bXqw -4wVTWsP4dGOnNfj0J3+Z/Z452/t36QKKcS8yx4cNu4D3lkYvg0yZ7FLSjfvq3KZU -w8m8/4EPtP2+KxIvFIDU5/5W43wYUv6QctTEuIDRfikdik3oKDiNUGCtfd6OOQtE -mINkDA6nLgOngyxj8jBAgkBIs+FfOoywPZP3cR9MeAdJYPBxEE3NN4wlZlQZCzkd -SA10258coG7bXii8lTrik89v0WhvGOYtoWniddv7MQKBgQDge3fo06q82+QhP2wM -ni99YMiYaW3oaotEAT86C8owDWR3z2+luk3HK34d/2uVhgzftTN3DMM70pZ7arI2 -GJqxNdzK2YGrACNB9bYCLhRj4/ITGannz6cjrHsiRH99BGpyeZQKFNmvrgY8ru+j -GSy17JY0/8Kj7gQYohnvHdh6SQKBgQDU1ylO6ZHZykfXvPgvnFqYUHiOgBwk6Y7+ -ClozmZL48u/42PpsVNuoquzH5V1kIBDwWZjUsOnZX5rbv0YzOJ449PsVw+pM+NAb -Dwtzwgfb9/uBryFKMgiXMToOEAyN+ENRg4PpyHRQu+shVh8/MPdMytA8AaypmZP7 -aPKhw/v42QKBgGG4PcWjxtJ54oA6rJ19iuzIYeo/EvI21zMeW9i34ycx3UdujIqX -ZF5MZ5VFaB7qANateZ7cdmynSoylMLjt0wFLkjbXomO/JpoDDV07k/K7+tgnttfL -hFW6MswDB4BzmKcGl9QfqXeZiOuHt5fHULhNKkIeCCv2Y+AZSLLXyjHJAoGAcmDq -RwkII0UsVIity1A381CTaOj5tvB4spa3oLEwJW7QfSeFdEAqBztLoaTmCk+dKrzL -f8lO8k0JeHwS6qXLiYpFgI3XVOQFWfU8z0l/VbuvQiLuPeQjb7S5oSMIzCaVbrHB -axoZP+Ws1y5j/l5/F5qKSyUPN9lbiCj+8uXSfAkCgYAcZ8i2+4ji9Ntu83G90GWi -hS2JOlZEUOCcE9vRu6HDkWC0qfkGbhjUk5GUHBjFp0shRTR/GnGA0ILAzgoxEK1s -/aDel9XDeuF4DJC+HzvPDoPFYz0UH4CuOYWhAejL81a5/AAHQqm2fpQSNln097rC -KfGyU23XuO7U8BloCy/hCA== ------END PRIVATE KEY----- From b9278d50148097d3c3adb0fc635907cd0aaa6d1f Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 3 Dec 2024 08:24:51 +0100 Subject: [PATCH 2/3] Qute message bundles: fix localization of enums - support constants with underscores - fixes #44866 (cherry picked from commit b322dbf358dd3f7356063f1e37d1ad4913c78044) --- docs/src/main/asciidoc/qute-reference.adoc | 21 ++++++--- .../deployment/MessageBundleProcessor.java | 45 ++++++++++++++----- .../i18n/MessageBundleEnumTest.java | 44 +++++++++++++++++- .../MessageBundleInvalidEnumConstantTest.java | 43 ++++++++++++++++++ .../test/resources/messages/enu.properties | 11 ++++- .../test/resources/messages/enu_cs.properties | 11 ++++- .../resources/messages/enu_invalid.properties | 2 + .../java/io/quarkus/qute/i18n/Message.java | 12 +++-- 8 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleInvalidEnumConstantTest.java create mode 100644 extensions/qute/deployment/src/test/resources/messages/enu_invalid.properties diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 4b62a037bd432..162103b1c31cc 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2994,10 +2994,9 @@ If there is a message bundle method that accepts a single parameter of an enum t @Message <1> String methodName(MyEnum enum); ---- -<1> The value is intentionally not provided. There's also no key for the method in a localized file. - -Then it receives a generated template: +<1> The value is intentionally not provided. There's also no key/value pair for this method in a localized file. +Then it receives a generated template like: [source,html] ---- {#when enumParamName} @@ -3006,7 +3005,8 @@ Then it receives a generated template: {/when} ---- -Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and values for all constant message keys: +Furthermore, a special message method is generated for each enum constant. +Finally, each localized file must contain keys and values for all enum constants: [source,poperties] ---- @@ -3014,7 +3014,18 @@ methodName_CONSTANT1=Value 1 methodName_CONSTANT2=Value 2 ---- -In a template, an enum constant can be localized with a message bundle method like `{msg:methodName(enumConstant)}`. +// We need to escape the first underscore +// See https://docs.asciidoctor.org/asciidoc/latest/subs/prevent/ +[IMPORTANT] +.Message keys for enum constants +==== +By default, the message key consists of the method name followed by the `\_` separator and the constant name. +If any constant name of a particular enum contains the `_` or the `$` character then the `\_$` separator must be used for all message keys for this enum instead. +For example, `methodName_$CONSTANT_1=Value 1` or `methodName_$CONSTANT$1=Value 1`. +A constant of a localized enum may not contain the `_$` separator. +==== + +In a template, the localized message for an enum constant can be obtained with a message bundle method like `{msg:methodName(enumConstant)}`. TIP: There is also <> - a convenient annotation to access enum constants in a template. diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 5842bbd715abc..a9e659b47b068 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -865,16 +865,20 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle * @param key * @param bundleInterface * @return {@code true} if the given key represents an enum constant message key, such as {@code myEnum_CONSTANT1} - * @see #toEnumConstantKey(String, String) */ boolean isEnumConstantMessageKey(String key, IndexView index, ClassInfo bundleInterface) { if (key.isBlank()) { return false; } - int lastIdx = key.lastIndexOf("_"); + return isEnumConstantMessageKey("_$", key, index, bundleInterface) + || isEnumConstantMessageKey("_", key, index, bundleInterface); + } + + private boolean isEnumConstantMessageKey(String separator, String key, IndexView index, ClassInfo bundleInterface) { + int lastIdx = key.lastIndexOf(separator); if (lastIdx != -1 && lastIdx != key.length()) { String methodName = key.substring(0, lastIdx); - String constant = key.substring(lastIdx + 1, key.length()); + String constant = key.substring(lastIdx + separator.length(), key.length()); MethodInfo method = messageBundleMethod(bundleInterface, methodName); if (method != null && method.parametersCount() == 1) { Type paramType = method.parameterType(0); @@ -1021,11 +1025,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d // We need some special handling for enum message bundle methods // A message bundle method that accepts an enum and has no message template receives a generated template: // {#when enumParamName} - // {#is CONSTANT1}{msg:org_acme_MyEnum_CONSTANT1} - // {#is CONSTANT2}{msg:org_acme_MyEnum_CONSTANT2} + // {#is CONSTANT_1}{msg:myEnum_$CONSTANT_1} + // {#is CONSTANT_2}{msg:myEnum_$CONSTANT_2} // ... // {/when} // Furthermore, a special message method is generated for each enum constant + // These methods are used to handle the {msg:myEnum$CONSTANT_1} and {msg:myEnum$CONSTANT_2} if (messageTemplate == null && method.parametersCount() == 1) { Type paramType = method.parameterType(0); if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { @@ -1036,9 +1041,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d .append("}"); Set enumConstants = maybeEnum.fields().stream().filter(FieldInfo::isEnumConstant) .map(FieldInfo::name).collect(Collectors.toSet()); + String separator = enumConstantSeparator(enumConstants); for (String enumConstant : enumConstants) { - // org_acme_MyEnum_CONSTANT1 - String enumConstantKey = toEnumConstantKey(method.name(), enumConstant); + // myEnum_CONSTANT + // myEnum_$CONSTANT_1 + // myEnum_$CONSTANT$NEXT + String enumConstantKey = toEnumConstantKey(method.name(), separator, enumConstant); String enumConstantTemplate = messageTemplates.get(enumConstantKey); if (enumConstantTemplate == null) { throw new TemplateException( @@ -1052,6 +1060,10 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d .append(":") .append(enumConstantKey) .append("}"); + // For each constant we generate a method: + // myEnum_CONSTANT(MyEnum val) + // myEnum_$CONSTANT_1(MyEnum val) + // myEnum_$CONSTANT$NEXT(MyEnum val) generateEnumConstantMessageMethod(bundleCreator, bundleName, locale, bundleInterface, defaultBundleInterface, enumConstantKey, keyMap, enumConstantTemplate, messageTemplateMethods); @@ -1132,8 +1144,21 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d return generatedName.replace('/', '.'); } - private String toEnumConstantKey(String methodName, String enumConstant) { - return methodName + "_" + enumConstant; + private String enumConstantSeparator(Set enumConstants) { + for (String constant : enumConstants) { + if (constant.contains("_$")) { + throw new MessageBundleException("A constant of a localized enum may not contain '_$': " + constant); + } + if (constant.contains("$") || constant.contains("_")) { + // If any of the constants contains "_" or "$" then "_$" is used + return "_$"; + } + } + return "_"; + } + + private String toEnumConstantKey(String methodName, String separator, String enumConstant) { + return methodName + separator + enumConstant; } private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, String bundleName, String locale, @@ -1165,7 +1190,7 @@ private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, Strin // No expression/tag - no need to use qute enumConstantMethod.returnValue(enumConstantMethod.load(messageTemplate)); } else { - // Obtain the template, e.g. msg_org_acme_MyEnum_CONSTANT1 + // Obtain the template, e.g. msg_myEnum$CONSTANT_1 ResultHandle template = enumConstantMethod.invokeStaticMethod( io.quarkus.qute.deployment.Descriptors.BUNDLES_GET_TEMPLATE, enumConstantMethod.load(templateId)); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java index 8ac3a9e739810..c0b4eb2e481e0 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateEnum; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; import io.quarkus.test.QuarkusUnitTest; @@ -18,7 +19,7 @@ public class MessageBundleEnumTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(Messages.class, MyEnum.class) + .addClasses(Messages.class, MyEnum.class, UnderscoredEnum.class, AnotherUnderscoredEnum.class) .addAsResource("messages/enu.properties") .addAsResource("messages/enu_cs.properties") .addAsResource(new StringAsset( @@ -26,16 +27,39 @@ public class MessageBundleEnumTest { + "{enu:shortEnum(MyEnum:ON)}::{enu:shortEnum(MyEnum:OFF)}::{enu:shortEnum(MyEnum:UNDEFINED)}::" + "{enu:foo(MyEnum:ON)}::{enu:foo(MyEnum:OFF)}::{enu:foo(MyEnum:UNDEFINED)}::" + "{enu:locFileOverride(MyEnum:ON)}::{enu:locFileOverride(MyEnum:OFF)}::{enu:locFileOverride(MyEnum:UNDEFINED)}"), - "templates/foo.html")); + "templates/foo.html") + .addAsResource(new StringAsset( + "{enu:underscored(UnderscoredEnum:A_B)}::{enu:underscored(UnderscoredEnum:FOO_BAR_BAZ)}::{enu:underscored_foo(AnotherUnderscoredEnum:NEXT_B)}::{enu:underscored$foo(AnotherUnderscoredEnum:NEXT_B)}::{enu:uncommon(UncommonEnum:NEXT$B)}"), + "templates/bar.html")); @Inject Template foo; + @Inject + Template bar; + @Test public void testMessages() { assertEquals("On::Off::Undefined::1::0::U::+::-::_::on::off::undefined", foo.render()); assertEquals("Zapnuto::Vypnuto::Nedefinováno::1::0::N::+::-::_::zap::vyp::nedef", foo.instance().setLocale("cs").render()); + assertEquals("A/B::Foo/Bar/Baz::NEXT::NEXT::NEXT", bar.render()); + } + + @TemplateEnum + public enum UnderscoredEnum { + A_B, + FOO_BAR_BAZ + } + + @TemplateEnum + public enum AnotherUnderscoredEnum { + NEXT_B + } + + @TemplateEnum + public enum UncommonEnum { + NEXT$B } @MessageBundle(value = "enu", locale = "en") @@ -69,6 +93,22 @@ public interface Messages { @Message String locFileOverride(MyEnum myEnum); + // maps to underscored_$A_B, underscored_$FOO_BAR_BAZ + @Message + String underscored(UnderscoredEnum val); + + // maps to underscored_foo_$NEXT_B + @Message + String underscored_foo(AnotherUnderscoredEnum val); + + // maps to underscored$foo_$NEXT_B + @Message + String underscored$foo(AnotherUnderscoredEnum val); + + // maps to uncommon_$NEXT$B + @Message + String uncommon(UncommonEnum val); + } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleInvalidEnumConstantTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleInvalidEnumConstantTest.java new file mode 100644 index 0000000000000..cd771a4929dfb --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleInvalidEnumConstantTest.java @@ -0,0 +1,43 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateEnum; +import io.quarkus.qute.deployment.MessageBundleException; +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; +import io.quarkus.test.QuarkusUnitTest; + +public class MessageBundleInvalidEnumConstantTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Messages.class, UnderscoredEnum.class) + .addAsResource("messages/enu_invalid.properties")) + .setExpectedException(MessageBundleException.class, true); + + @Test + public void testMessages() { + fail(); + } + + @TemplateEnum + public enum UnderscoredEnum { + + A_B, + + } + + @MessageBundle(value = "enu_invalid") + public interface Messages { + + @Message + String underscored(UnderscoredEnum constants); + + } + +} diff --git a/extensions/qute/deployment/src/test/resources/messages/enu.properties b/extensions/qute/deployment/src/test/resources/messages/enu.properties index 072f933eb0881..ac9aa52409737 100644 --- a/extensions/qute/deployment/src/test/resources/messages/enu.properties +++ b/extensions/qute/deployment/src/test/resources/messages/enu.properties @@ -10,4 +10,13 @@ locFileOverride={#when myEnum}\ {#is ON}on\ {#is OFF}off\ {#else}undefined\ - {/when} \ No newline at end of file + {/when} + +underscored_$A_B=A/B +underscored_$FOO_BAR_BAZ=Foo/Bar/Baz + +underscored_foo_$NEXT_B=NEXT + +underscored$foo_$NEXT_B=NEXT + +uncommon_$NEXT$B=NEXT \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties index e3f5c0a2ae6de..8f15c483934fc 100644 --- a/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties +++ b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties @@ -10,4 +10,13 @@ locFileOverride={#when myEnum}\ {#is ON}zap\ {#is OFF}vyp\ {#else}nedef\ - {/when} \ No newline at end of file + {/when} + +underscored_$A_B=A/B +underscored_$FOO_BAR_BAZ=Foo/Bar/Baz + +underscored_foo_$NEXT_B=NEXT + +underscored$foo_$NEXT_B=NEXT + +uncommon_$NEXT$B=NEXT \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu_invalid.properties b/extensions/qute/deployment/src/test/resources/messages/enu_invalid.properties new file mode 100644 index 0000000000000..79e8a9324eb4d --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu_invalid.properties @@ -0,0 +1,2 @@ +underscored_$A_B=A/B +underscored_$FOO_BAR_BAZ=Foo/Bar/Baz \ No newline at end of file diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java index b8b8a43ae5955..6f858af9e6a91 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java @@ -27,7 +27,7 @@ * There is a convenient way to localize enums. *

* If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined then - * it receives a generated template: + * it receives a generated template like: * *

  * {#when enumParamName}
@@ -37,14 +37,20 @@
  * 
* * Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and - * values for all constant message keys: + * values for all enum constants. * *
  * methodName_CONSTANT1=Value 1
  * methodName_CONSTANT2=Value 2
  * 
* - * In a template, an enum constant can be localized with a message bundle method {@code msg:methodName(enumConstant)}. + * By default, the message key consists of the method name followed by the {@code _} separator and the constant name. If any + * constant name of a particular enum contains the {@code _} or the {@code $} character then the {@code _$} separator must be + * used for all message keys for this enum instead. For example, {@code methodName_$CONSTANT_1=Value 1} or + * {@code methodName_$CONSTANT$1=Value 1}. + *

+ * In a template, the localized message for an enum constant can be obtained with a message bundle method like + * {@code msg:methodName(enumConstant)}. * * @see MessageBundle */ From cd842d3c52c6a6d9bb6f3f92ae751777c9c32ee4 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 3 Dec 2024 06:30:06 -0500 Subject: [PATCH 3/3] Bump smallrye-open-api.version from 4.0.3 to 4.0.4 Signed-off-by: Michael Edgar (cherry picked from commit bdf48fb1b0a75bba154ea6f3269d54f5c54763f6) --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index ca6e24b5fcfc0..00427268f969b 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -51,7 +51,7 @@ 3.10.2 4.1.0 4.0.0 - 4.0.3 + 4.0.4 2.11.0 6.6.3 4.6.1