Skip to content

Commit ad2bea3

Browse files
Fail build if nimbus-jose-jwt version is not aligned
Closes gh-14047
1 parent 5161712 commit ad2bea3

File tree

4 files changed

+216
-0
lines changed

4 files changed

+216
-0
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ apply plugin: 'org.springframework.security.sagan'
2424
apply plugin: 'org.springframework.github.milestone'
2525
apply plugin: 'org.springframework.github.changelog'
2626
apply plugin: 'org.springframework.github.release'
27+
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
2728

2829
group = 'org.springframework.security'
2930
description = 'Spring Security'

buildSrc/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ gradlePlugin {
6464
id = "s101"
6565
implementationClass = "s101.S101Plugin"
6666
}
67+
verifyDependenciesVersions {
68+
id = "org.springframework.security.versions.verify-dependencies-versions"
69+
implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin"
70+
}
6771
}
6872
}
6973

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.convention.versions;
18+
19+
import okhttp3.OkHttpClient;
20+
import okhttp3.Request;
21+
import okhttp3.Response;
22+
import org.w3c.dom.Document;
23+
import org.xml.sax.SAXException;
24+
25+
import javax.xml.XMLConstants;
26+
import javax.xml.parsers.DocumentBuilder;
27+
import javax.xml.parsers.DocumentBuilderFactory;
28+
import javax.xml.parsers.ParserConfigurationException;
29+
import javax.xml.xpath.XPath;
30+
import javax.xml.xpath.XPathExpressionException;
31+
import javax.xml.xpath.XPathFactory;
32+
import java.io.IOException;
33+
import java.io.InputStream;
34+
35+
class TransitiveDependencyLookupUtils {
36+
static String OIDC_SDK_NAME = "oauth2-oidc-sdk";
37+
static String NIMBUS_JOSE_JWT_NAME = "nimbus-jose-jwt";
38+
39+
private static OkHttpClient client = new OkHttpClient();
40+
41+
static String lookupJwtVersion(String oauthSdcVersion) {
42+
Request request = new Request.Builder()
43+
.get()
44+
.url("https://repo.maven.apache.org/maven2/com/nimbusds/" + OIDC_SDK_NAME + "/" + oauthSdcVersion + "/" + OIDC_SDK_NAME + "-" + oauthSdcVersion + ".pom")
45+
.build();
46+
try (Response response = client.newCall(request).execute()) {
47+
if (!response.isSuccessful()) {
48+
throw new IOException("Unexpected code " + response);
49+
}
50+
InputStream inputStream = response.body().byteStream();
51+
return getVersion(inputStream);
52+
53+
} catch (Exception e) {
54+
throw new RuntimeException(e);
55+
}
56+
}
57+
58+
private static String getVersion(InputStream inputStream) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {
59+
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
60+
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
61+
DocumentBuilder db = dbf.newDocumentBuilder();
62+
63+
Document doc = db.parse(inputStream);
64+
65+
doc.getDocumentElement().normalize();
66+
67+
XPath xPath = XPathFactory.newInstance().newXPath();
68+
return xPath.evaluate("/project/dependencies/dependency/version[../artifactId/text() = \"" + NIMBUS_JOSE_JWT_NAME + "\"]", doc);
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.convention.versions;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Set;
24+
import java.util.function.Supplier;
25+
import java.util.stream.Collectors;
26+
27+
import org.gradle.api.Plugin;
28+
import org.gradle.api.Project;
29+
import org.gradle.api.Task;
30+
import org.gradle.api.artifacts.Configuration;
31+
import org.gradle.api.artifacts.ModuleVersionIdentifier;
32+
import org.gradle.api.tasks.TaskProvider;
33+
34+
public class VerifyDependenciesVersionsPlugin implements Plugin<Project> {
35+
36+
@Override
37+
public void apply(Project project) {
38+
TaskProvider<Task> provider = project.getTasks().register("verifyDependenciesVersions", (verifyDependenciesVersionsTask) -> {
39+
verifyDependenciesVersionsTask.setGroup("Verification");
40+
verifyDependenciesVersionsTask.setDescription("Verify that specific dependencies are using the same version");
41+
List<Configuration> allConfigurations = new ArrayList<>();
42+
allConfigurations.addAll(getConfigurations(project));
43+
allConfigurations.addAll(getSubprojectsConfigurations(project.getSubprojects()));
44+
verifyDependenciesVersionsTask.getInputs().property("dependenciesVersions", new DependencySupplier(allConfigurations));
45+
verifyDependenciesVersionsTask.doLast((task) -> {
46+
DependencySupplier dependencies = (DependencySupplier) task.getInputs().getProperties().get("dependenciesVersions");
47+
Map<String, List<Artifact>> artifacts = dependencies.get();
48+
List<Artifact> oauth2OidcSdk = artifacts.get("oauth2-oidc-sdk");
49+
List<Artifact> nimbusJoseJwt = artifacts.get("nimbus-jose-jwt");
50+
if (oauth2OidcSdk.size() > 1) {
51+
throw new IllegalStateException("Found multiple versions of oauth2-oidc-sdk: " + oauth2OidcSdk);
52+
}
53+
Artifact oauth2OidcSdkArtifact = oauth2OidcSdk.get(0);
54+
String nimbusJoseJwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(oauth2OidcSdkArtifact.version());
55+
List<Artifact> differentVersions = nimbusJoseJwt.stream()
56+
.filter((artifact) -> !artifact.version().equals(nimbusJoseJwtVersion))
57+
.filter((artifact -> !artifact.configurationName().contains("spring-security-cas"))) // CAS uses a different version
58+
.collect(Collectors.toList());
59+
if (!differentVersions.isEmpty()) {
60+
String message = "Found transitive nimbus-jose-jwt version [" + nimbusJoseJwtVersion + "] in oauth2-oidc-sdk " + oauth2OidcSdkArtifact
61+
+ ", but the project contains a different version of nimbus-jose-jwt " + differentVersions
62+
+ ". Please align the versions of nimbus-jose-jwt.";
63+
throw new IllegalStateException(message);
64+
}
65+
});
66+
});
67+
project.getTasks().getByName("build").dependsOn(provider);
68+
}
69+
70+
private List<Configuration> getConfigurations(Project project) {
71+
return project.getConfigurations().stream()
72+
.filter(Configuration::isCanBeResolved)
73+
.filter((config) -> config.getName().equals("runtimeClasspath"))
74+
.collect(Collectors.toList());
75+
}
76+
77+
private List<Configuration> getSubprojectsConfigurations(Set<Project> subprojects) {
78+
if (subprojects.isEmpty()) {
79+
return Collections.emptyList();
80+
}
81+
List<Configuration> subprojectConfigurations = new ArrayList<>();
82+
for (Project subproject : subprojects) {
83+
subprojectConfigurations.addAll(getConfigurations(subproject));
84+
subprojectConfigurations.addAll(getSubprojectsConfigurations(subproject.getSubprojects()));
85+
}
86+
return subprojectConfigurations;
87+
}
88+
89+
private static class Artifact {
90+
91+
private final String name;
92+
private final String version;
93+
private final String configurationName;
94+
95+
private Artifact(String name, String version, String configurationName) {
96+
this.name = name;
97+
this.version = version;
98+
this.configurationName = configurationName;
99+
}
100+
101+
public String name() {
102+
return this.name;
103+
}
104+
105+
public String version() {
106+
return this.version;
107+
}
108+
109+
public String configurationName() {
110+
return this.configurationName;
111+
}
112+
113+
}
114+
115+
private static final class DependencySupplier implements Supplier<Map<String, List<Artifact>>> {
116+
117+
private final List<Configuration> configurations;
118+
119+
private DependencySupplier(List<Configuration> configurations) {
120+
this.configurations = configurations;
121+
}
122+
123+
@Override
124+
public Map<String, List<Artifact>> get() {
125+
return getDependencies(this.configurations);
126+
}
127+
128+
private Map<String, List<Artifact>> getDependencies(List<Configuration> configurations) {
129+
return configurations.stream().flatMap((configuration) -> {
130+
return configuration.getResolvedConfiguration().getResolvedArtifacts().stream()
131+
.map((dep) -> {
132+
ModuleVersionIdentifier id = dep.getModuleVersion().getId();
133+
return new Artifact(id.getName(), id.getVersion(), configuration.toString());
134+
});
135+
})
136+
.distinct()
137+
.collect(Collectors.groupingBy(Artifact::name));
138+
}
139+
}
140+
141+
}

0 commit comments

Comments
 (0)