From ad2bea33509b1439e629739a039a81b0a939e2c0 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 25 Oct 2023 13:33:43 -0300 Subject: [PATCH] Fail build if nimbus-jose-jwt version is not aligned Closes gh-14047 --- build.gradle | 1 + buildSrc/build.gradle | 4 + .../TransitiveDependencyLookupUtils.java | 70 +++++++++ .../VerifyDependenciesVersionsPlugin.java | 141 ++++++++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/TransitiveDependencyLookupUtils.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/VerifyDependenciesVersionsPlugin.java diff --git a/build.gradle b/build.gradle index 2d3b53c87b..f3737eee35 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ apply plugin: 'org.springframework.security.sagan' apply plugin: 'org.springframework.github.milestone' apply plugin: 'org.springframework.github.changelog' apply plugin: 'org.springframework.github.release' +apply plugin: 'org.springframework.security.versions.verify-dependencies-versions' group = 'org.springframework.security' description = 'Spring Security' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 9d0ec9139a..5a17397667 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -64,6 +64,10 @@ gradlePlugin { id = "s101" implementationClass = "s101.S101Plugin" } + verifyDependenciesVersions { + id = "org.springframework.security.versions.verify-dependencies-versions" + implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin" + } } } diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/TransitiveDependencyLookupUtils.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/TransitiveDependencyLookupUtils.java new file mode 100644 index 0000000000..ae27d62489 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/TransitiveDependencyLookupUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.security.convention.versions; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.IOException; +import java.io.InputStream; + +class TransitiveDependencyLookupUtils { + static String OIDC_SDK_NAME = "oauth2-oidc-sdk"; + static String NIMBUS_JOSE_JWT_NAME = "nimbus-jose-jwt"; + + private static OkHttpClient client = new OkHttpClient(); + + static String lookupJwtVersion(String oauthSdcVersion) { + Request request = new Request.Builder() + .get() + .url("https://repo.maven.apache.org/maven2/com/nimbusds/" + OIDC_SDK_NAME + "/" + oauthSdcVersion + "/" + OIDC_SDK_NAME + "-" + oauthSdcVersion + ".pom") + .build(); + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + InputStream inputStream = response.body().byteStream(); + return getVersion(inputStream); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String getVersion(InputStream inputStream) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + DocumentBuilder db = dbf.newDocumentBuilder(); + + Document doc = db.parse(inputStream); + + doc.getDocumentElement().normalize(); + + XPath xPath = XPathFactory.newInstance().newXPath(); + return xPath.evaluate("/project/dependencies/dependency/version[../artifactId/text() = \"" + NIMBUS_JOSE_JWT_NAME + "\"]", doc); + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/VerifyDependenciesVersionsPlugin.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/VerifyDependenciesVersionsPlugin.java new file mode 100644 index 0000000000..85d17a9f4d --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/VerifyDependenciesVersionsPlugin.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.security.convention.versions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.tasks.TaskProvider; + +public class VerifyDependenciesVersionsPlugin implements Plugin { + + @Override + public void apply(Project project) { + TaskProvider provider = project.getTasks().register("verifyDependenciesVersions", (verifyDependenciesVersionsTask) -> { + verifyDependenciesVersionsTask.setGroup("Verification"); + verifyDependenciesVersionsTask.setDescription("Verify that specific dependencies are using the same version"); + List allConfigurations = new ArrayList<>(); + allConfigurations.addAll(getConfigurations(project)); + allConfigurations.addAll(getSubprojectsConfigurations(project.getSubprojects())); + verifyDependenciesVersionsTask.getInputs().property("dependenciesVersions", new DependencySupplier(allConfigurations)); + verifyDependenciesVersionsTask.doLast((task) -> { + DependencySupplier dependencies = (DependencySupplier) task.getInputs().getProperties().get("dependenciesVersions"); + Map> artifacts = dependencies.get(); + List oauth2OidcSdk = artifacts.get("oauth2-oidc-sdk"); + List nimbusJoseJwt = artifacts.get("nimbus-jose-jwt"); + if (oauth2OidcSdk.size() > 1) { + throw new IllegalStateException("Found multiple versions of oauth2-oidc-sdk: " + oauth2OidcSdk); + } + Artifact oauth2OidcSdkArtifact = oauth2OidcSdk.get(0); + String nimbusJoseJwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(oauth2OidcSdkArtifact.version()); + List differentVersions = nimbusJoseJwt.stream() + .filter((artifact) -> !artifact.version().equals(nimbusJoseJwtVersion)) + .filter((artifact -> !artifact.configurationName().contains("spring-security-cas"))) // CAS uses a different version + .collect(Collectors.toList()); + if (!differentVersions.isEmpty()) { + String message = "Found transitive nimbus-jose-jwt version [" + nimbusJoseJwtVersion + "] in oauth2-oidc-sdk " + oauth2OidcSdkArtifact + + ", but the project contains a different version of nimbus-jose-jwt " + differentVersions + + ". Please align the versions of nimbus-jose-jwt."; + throw new IllegalStateException(message); + } + }); + }); + project.getTasks().getByName("build").dependsOn(provider); + } + + private List getConfigurations(Project project) { + return project.getConfigurations().stream() + .filter(Configuration::isCanBeResolved) + .filter((config) -> config.getName().equals("runtimeClasspath")) + .collect(Collectors.toList()); + } + + private List getSubprojectsConfigurations(Set subprojects) { + if (subprojects.isEmpty()) { + return Collections.emptyList(); + } + List subprojectConfigurations = new ArrayList<>(); + for (Project subproject : subprojects) { + subprojectConfigurations.addAll(getConfigurations(subproject)); + subprojectConfigurations.addAll(getSubprojectsConfigurations(subproject.getSubprojects())); + } + return subprojectConfigurations; + } + + private static class Artifact { + + private final String name; + private final String version; + private final String configurationName; + + private Artifact(String name, String version, String configurationName) { + this.name = name; + this.version = version; + this.configurationName = configurationName; + } + + public String name() { + return this.name; + } + + public String version() { + return this.version; + } + + public String configurationName() { + return this.configurationName; + } + + } + + private static final class DependencySupplier implements Supplier>> { + + private final List configurations; + + private DependencySupplier(List configurations) { + this.configurations = configurations; + } + + @Override + public Map> get() { + return getDependencies(this.configurations); + } + + private Map> getDependencies(List configurations) { + return configurations.stream().flatMap((configuration) -> { + return configuration.getResolvedConfiguration().getResolvedArtifacts().stream() + .map((dep) -> { + ModuleVersionIdentifier id = dep.getModuleVersion().getId(); + return new Artifact(id.getName(), id.getVersion(), configuration.toString()); + }); + }) + .distinct() + .collect(Collectors.groupingBy(Artifact::name)); + } + } + +}