Skip to content

Commit 85177c0

Browse files
Merge branch '6.2.x'
Closes gh-14408
2 parents 9135cb4 + 4fb6a33 commit 85177c0

File tree

53 files changed

+331
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+331
-6
lines changed

config/spring-security-config.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ dependencies {
112112
testImplementation ('org.apache.maven.resolver:maven-resolver-transport-http') {
113113
exclude group: "org.slf4j", module: "jcl-over-slf4j"
114114
}
115+
testImplementation libs.org.instancio.instancio.junit
115116

116117
testRuntimeOnly 'org.hsqldb:hsqldb'
117118
}
@@ -153,3 +154,9 @@ tasks.withType(KotlinCompile).configureEach {
153154
jvmTarget = "17"
154155
}
155156
}
157+
158+
configure(project.tasks.withType(Test)) {
159+
doFirst {
160+
systemProperties['springSecurityVersion'] = version
161+
}
162+
}
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/*
2+
* Copyright 2002-2024 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;
18+
19+
import java.io.File;
20+
import java.io.FileInputStream;
21+
import java.io.FileOutputStream;
22+
import java.io.IOException;
23+
import java.io.NotSerializableException;
24+
import java.io.ObjectInputStream;
25+
import java.io.ObjectOutputStream;
26+
import java.io.ObjectStreamClass;
27+
import java.io.Serializable;
28+
import java.lang.reflect.Modifier;
29+
import java.nio.file.Files;
30+
import java.nio.file.Path;
31+
import java.nio.file.Paths;
32+
import java.util.ArrayList;
33+
import java.util.Date;
34+
import java.util.HashMap;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.Set;
38+
import java.util.stream.Stream;
39+
40+
import org.apereo.cas.client.validation.AssertionImpl;
41+
import org.instancio.Instancio;
42+
import org.instancio.InstancioApi;
43+
import org.instancio.Select;
44+
import org.instancio.generator.Generator;
45+
import org.junit.jupiter.api.Disabled;
46+
import org.junit.jupiter.params.ParameterizedTest;
47+
import org.junit.jupiter.params.provider.MethodSource;
48+
49+
import org.springframework.beans.factory.config.BeanDefinition;
50+
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
51+
import org.springframework.core.type.filter.AssignableTypeFilter;
52+
import org.springframework.security.access.intercept.RunAsUserToken;
53+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
54+
import org.springframework.security.authentication.RememberMeAuthenticationToken;
55+
import org.springframework.security.authentication.TestAuthentication;
56+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
57+
import org.springframework.security.authentication.jaas.JaasAuthenticationToken;
58+
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
59+
import org.springframework.security.cas.authentication.CasAuthenticationToken;
60+
import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken;
61+
import org.springframework.security.core.SpringSecurityCoreVersion;
62+
import org.springframework.security.core.session.SessionInformation;
63+
import org.springframework.security.core.userdetails.UserDetails;
64+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
65+
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
66+
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
67+
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
68+
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens;
69+
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
70+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
71+
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
72+
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
73+
import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals;
74+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
75+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
76+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
77+
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
78+
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
79+
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
80+
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
81+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
82+
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
83+
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
84+
import org.springframework.security.oauth2.jwt.TestJwts;
85+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
86+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
87+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
88+
import org.springframework.security.web.authentication.WebAuthenticationDetails;
89+
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
90+
91+
import static org.assertj.core.api.Assertions.assertThat;
92+
import static org.assertj.core.api.Assertions.fail;
93+
94+
/**
95+
* Tests that Spring Security classes that implements {@link Serializable} and have the
96+
* same serial version as {@link SpringSecurityCoreVersion#SERIAL_VERSION_UID} can be
97+
* deserialized from a previous minor version.
98+
* <p>
99+
* For example, all classes from version 6.2.x that matches the previous requirement
100+
* should be serialized and saved to a folder, and then later on, in 6.3.x, it is verified
101+
* if they can be deserialized
102+
*
103+
* @author Marcus da Coregio
104+
* @since 6.2.2
105+
* @see <a href="https://github.com/spring-projects/spring-security/issues/3737">GitHub
106+
* Issue #3737</a>
107+
*/
108+
class SpringSecurityCoreVersionSerializableTests {
109+
110+
private static final Map<Class<?>, Generator<?>> generatorByClassName = new HashMap<>();
111+
112+
static final long securitySerialVersionUid = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
113+
114+
static Path currentVersionFolder = Paths.get("src/test/resources/serialized/" + getCurrentVersion());
115+
116+
static Path previousVersionFolder = Paths.get("src/test/resources/serialized/" + getPreviousVersion());
117+
118+
static {
119+
ClientRegistration.Builder clientRegistrationBuilder = TestClientRegistrations.clientRegistration();
120+
ClientRegistration clientRegistration = clientRegistrationBuilder.build();
121+
UserDetails user = TestAuthentication.user();
122+
WebAuthenticationDetails details = new WebAuthenticationDetails("remote", "sessionId");
123+
generatorByClassName.put(DefaultOAuth2User.class, (r) -> TestOAuth2Users.create());
124+
generatorByClassName.put(ClientRegistration.class, (r) -> clientRegistration);
125+
generatorByClassName.put(ClientRegistration.ProviderDetails.class,
126+
(r) -> clientRegistration.getProviderDetails());
127+
generatorByClassName.put(ClientRegistration.ProviderDetails.UserInfoEndpoint.class,
128+
(r) -> clientRegistration.getProviderDetails().getUserInfoEndpoint());
129+
generatorByClassName.put(ClientRegistration.Builder.class, (r) -> clientRegistrationBuilder);
130+
generatorByClassName.put(OAuth2AuthorizationRequest.class,
131+
(r) -> TestOAuth2AuthorizationRequests.request().build());
132+
generatorByClassName.put(OAuth2AuthorizationResponse.class,
133+
(r) -> TestOAuth2AuthorizationResponses.success().build());
134+
generatorByClassName.put(OAuth2AuthorizedClient.class,
135+
(r) -> new OAuth2AuthorizedClient(clientRegistration, "principal", TestOAuth2AccessTokens.noScopes()));
136+
generatorByClassName.put(OAuth2UserAuthority.class, (r) -> new OAuth2UserAuthority(Map.of("username", "user")));
137+
generatorByClassName.put(OAuth2AuthorizationExchange.class, (r) -> TestOAuth2AuthorizationExchanges.success());
138+
generatorByClassName.put(OidcUserInfo.class, (r) -> OidcUserInfo.builder().email("[email protected]").build());
139+
generatorByClassName.put(SessionInformation.class,
140+
(r) -> new SessionInformation(user, r.alphanumeric(4), new Date(1704378933936L)));
141+
generatorByClassName.put(OAuth2LoginAuthenticationToken.class, (r) -> {
142+
var token = new OAuth2LoginAuthenticationToken(clientRegistration,
143+
TestOAuth2AuthorizationExchanges.success());
144+
token.setDetails(details);
145+
return token;
146+
});
147+
generatorByClassName.put(OAuth2AuthorizationCodeAuthenticationToken.class, (r) -> {
148+
var token = TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated();
149+
token.setDetails(details);
150+
return token;
151+
});
152+
generatorByClassName
153+
.put(org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken.class, (r) -> {
154+
var token = new org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken(
155+
"token");
156+
token.setDetails(details);
157+
return token;
158+
});
159+
generatorByClassName.put(BearerTokenAuthenticationToken.class, (r) -> {
160+
var token = new BearerTokenAuthenticationToken("token");
161+
token.setDetails(details);
162+
return token;
163+
});
164+
generatorByClassName.put(BearerTokenAuthentication.class, (r) -> {
165+
var token = new BearerTokenAuthentication(TestOAuth2AuthenticatedPrincipals.active(),
166+
TestOAuth2AccessTokens.noScopes(), user.getAuthorities());
167+
token.setDetails(details);
168+
return token;
169+
});
170+
generatorByClassName.put(OAuth2AuthenticationToken.class, (r) -> {
171+
var token = TestOAuth2AuthenticationTokens.authenticated();
172+
token.setDetails(details);
173+
return token;
174+
});
175+
generatorByClassName.put(JwtAuthenticationToken.class, (r) -> {
176+
var token = new JwtAuthenticationToken(TestJwts.user());
177+
token.setDetails(details);
178+
return token;
179+
});
180+
generatorByClassName.put(RunAsUserToken.class, (r) -> {
181+
RunAsUserToken token = new RunAsUserToken("key", user, "creds", user.getAuthorities(),
182+
AnonymousAuthenticationToken.class);
183+
token.setDetails(details);
184+
return token;
185+
});
186+
generatorByClassName.put(CasServiceTicketAuthenticationToken.class, (r) -> {
187+
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless("creds");
188+
token.setDetails(details);
189+
return token;
190+
});
191+
generatorByClassName.put(CasAuthenticationToken.class, (r) -> {
192+
var token = new CasAuthenticationToken("key", user, "Password", user.getAuthorities(), user,
193+
new AssertionImpl("test"));
194+
token.setDetails(details);
195+
return token;
196+
});
197+
generatorByClassName.put(CasAssertionAuthenticationToken.class, (r) -> {
198+
var token = new CasAssertionAuthenticationToken(new AssertionImpl("test"), "ticket");
199+
token.setDetails(details);
200+
return token;
201+
});
202+
generatorByClassName.put(RememberMeAuthenticationToken.class, (r) -> {
203+
RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("key", user, user.getAuthorities());
204+
token.setDetails(details);
205+
return token;
206+
});
207+
generatorByClassName.put(PreAuthenticatedAuthenticationToken.class, (r) -> {
208+
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(user, "creds",
209+
user.getAuthorities());
210+
token.setDetails(details);
211+
return token;
212+
});
213+
generatorByClassName.put(UsernamePasswordAuthenticationToken.class, (r) -> {
214+
var token = UsernamePasswordAuthenticationToken.unauthenticated(user, "creds");
215+
token.setDetails(details);
216+
return token;
217+
});
218+
generatorByClassName.put(JaasAuthenticationToken.class, (r) -> {
219+
var token = new JaasAuthenticationToken(user, "creds", null);
220+
token.setDetails(details);
221+
return token;
222+
});
223+
}
224+
225+
@ParameterizedTest
226+
@MethodSource("getClassesToSerialize")
227+
@Disabled("This method should only be used to serialize the classes once")
228+
void serializeCurrentVersionClasses(Class<?> clazz) throws Exception {
229+
Files.createDirectories(currentVersionFolder);
230+
Path filePath = Paths.get(currentVersionFolder.toAbsolutePath() + "/" + clazz.getName());
231+
File file = filePath.toFile();
232+
if (file.exists()) {
233+
return;
234+
}
235+
Files.createFile(filePath);
236+
Object instance = instancioWithDefaults(clazz).create();
237+
assertThat(instance).isInstanceOf(clazz);
238+
try (FileOutputStream fileOutputStream = new FileOutputStream(file);
239+
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
240+
objectOutputStream.writeObject(instance);
241+
objectOutputStream.flush();
242+
}
243+
catch (NotSerializableException ex) {
244+
Files.delete(filePath);
245+
fail("Could not serialize " + clazz.getName(), ex);
246+
}
247+
}
248+
249+
@ParameterizedTest
250+
@MethodSource("getFilesToDeserialize")
251+
void shouldBeAbleToDeserializeClassFromPreviousVersion(Path filePath) {
252+
try (FileInputStream fileInputStream = new FileInputStream(filePath.toFile());
253+
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
254+
Object obj = objectInputStream.readObject();
255+
Class<?> clazz = Class.forName(filePath.getFileName().toString());
256+
assertThat(obj).isInstanceOf(clazz);
257+
}
258+
catch (IOException | ClassNotFoundException ex) {
259+
fail("Could not deserialize " + filePath, ex);
260+
}
261+
}
262+
263+
static Stream<Path> getFilesToDeserialize() throws IOException {
264+
assertThat(previousVersionFolder.toFile().exists())
265+
.as("Make sure that the " + previousVersionFolder + " exists and is not empty")
266+
.isTrue();
267+
try (Stream<Path> files = Files.list(previousVersionFolder)) {
268+
if (files.findFirst().isEmpty()) {
269+
fail("Please make sure to run SpringSecurityCoreVersionSerializableTests#serializeCurrentVersionClasses for the "
270+
+ getPreviousVersion() + " version");
271+
}
272+
}
273+
return Files.list(previousVersionFolder);
274+
}
275+
276+
static Stream<Class<?>> getClassesToSerialize() throws Exception {
277+
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
278+
provider.addIncludeFilter(new AssignableTypeFilter(Serializable.class));
279+
List<Class<?>> classes = new ArrayList<>();
280+
281+
Set<BeanDefinition> components = provider.findCandidateComponents("org/springframework/security");
282+
for (BeanDefinition component : components) {
283+
Class<?> clazz = Class.forName(component.getBeanClassName());
284+
boolean isAbstract = Modifier.isAbstract(clazz.getModifiers());
285+
boolean matchesExpectedSerialVersion = ObjectStreamClass.lookup(clazz)
286+
.getSerialVersionUID() == securitySerialVersionUid;
287+
if (!isAbstract && matchesExpectedSerialVersion) {
288+
classes.add(clazz);
289+
}
290+
}
291+
return classes.stream();
292+
}
293+
294+
private static InstancioApi<?> instancioWithDefaults(Class<?> clazz) {
295+
InstancioApi<?> instancio = Instancio.of(clazz);
296+
if (generatorByClassName.containsKey(clazz)) {
297+
instancio.supply(Select.all(clazz), generatorByClassName.get(clazz));
298+
}
299+
return instancio;
300+
}
301+
302+
private static String getCurrentVersion() {
303+
String version = System.getProperty("springSecurityVersion");
304+
String[] parts = version.split("\\.");
305+
parts[2] = "x";
306+
return String.join(".", parts);
307+
}
308+
309+
private static String getPreviousVersion() {
310+
String version = System.getProperty("springSecurityVersion");
311+
String[] parts = version.split("\\.");
312+
parts[1] = String.valueOf(Integer.parseInt(parts[1]) - 1);
313+
parts[2] = "x";
314+
return String.join(".", parts);
315+
}
316+
317+
}

core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,11 +39,8 @@ public final class SpringSecurityCoreVersion {
3939

4040
/**
4141
* Global Serialization value for Spring Security classes.
42-
*
43-
* N.B. Classes are not intended to be serializable between different versions. See
44-
* SEC-1709 for why we still need a serial version.
4542
*/
46-
public static final long SERIAL_VERSION_UID = 630L;
43+
public static final long SERIAL_VERSION_UID = 620L;
4744

4845
static final String MIN_SPRING_VERSION = getSpringVersion();
4946

core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import org.apache.commons.logging.LogFactory;
2424
import org.junit.jupiter.api.AfterEach;
2525
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Disabled;
2627
import org.junit.jupiter.api.Test;
2728
import org.junit.jupiter.api.extension.ExtendWith;
2829
import org.mockito.Answers;
@@ -79,6 +80,7 @@ public void springVersionIsUpToDate() {
7980
}
8081

8182
@Test
83+
@Disabled("Since 6.3. See gh-3737")
8284
public void serialVersionMajorAndMinorVersionMatchBuildVersion() {
8385
String version = System.getProperty("springSecurityVersion");
8486
// Strip patch version

dependencies/spring-security-dependencies.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ dependencies {
8181
api libs.org.apache.maven.resolver.maven.resolver.impl
8282
api libs.org.apache.maven.resolver.maven.resolver.transport.http
8383
api libs.org.apache.maven.maven.resolver.provider
84+
api libs.org.instancio.instancio.junit
8485
}
8586
}
8687

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ com-github-spullara-mustache-java-compiler = "com.github.spullara.mustache.java:
100100
org-hidetake-gradle-ssh-plugin = "org.hidetake:gradle-ssh-plugin:2.10.1"
101101
org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-info-extractor-gradle:4.29.4"
102102
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1"
103+
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
103104

104105
[plugins]
105106

0 commit comments

Comments
 (0)