|
@@ -0,0 +1,318 @@
|
|
|
+/*
|
|
|
+ * Copyright 2002-2024 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;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileInputStream;
|
|
|
+import java.io.FileOutputStream;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.NotSerializableException;
|
|
|
+import java.io.ObjectInputStream;
|
|
|
+import java.io.ObjectOutputStream;
|
|
|
+import java.io.ObjectStreamClass;
|
|
|
+import java.io.Serializable;
|
|
|
+import java.lang.reflect.Modifier;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Stream;
|
|
|
+
|
|
|
+import org.apereo.cas.client.validation.AssertionImpl;
|
|
|
+import org.instancio.Instancio;
|
|
|
+import org.instancio.InstancioApi;
|
|
|
+import org.instancio.Select;
|
|
|
+import org.instancio.generator.Generator;
|
|
|
+import org.junit.jupiter.api.Disabled;
|
|
|
+import org.junit.jupiter.params.ParameterizedTest;
|
|
|
+import org.junit.jupiter.params.provider.MethodSource;
|
|
|
+
|
|
|
+import org.springframework.beans.factory.config.BeanDefinition;
|
|
|
+import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
|
|
+import org.springframework.core.type.filter.AssignableTypeFilter;
|
|
|
+import org.springframework.security.access.intercept.RunAsUserToken;
|
|
|
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
|
|
+import org.springframework.security.authentication.RememberMeAuthenticationToken;
|
|
|
+import org.springframework.security.authentication.TestAuthentication;
|
|
|
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
+import org.springframework.security.authentication.jaas.JaasAuthenticationToken;
|
|
|
+import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
|
|
|
+import org.springframework.security.cas.authentication.CasAuthenticationToken;
|
|
|
+import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken;
|
|
|
+import org.springframework.security.core.SpringSecurityCoreVersion;
|
|
|
+import org.springframework.security.core.session.SessionInformation;
|
|
|
+import org.springframework.security.core.userdetails.UserDetails;
|
|
|
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
|
|
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
|
|
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
|
|
+import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
|
|
+import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens;
|
|
|
+import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthorizationCodeAuthenticationTokens;
|
|
|
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
|
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
|
|
+import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
|
|
+import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
|
|
|
+import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses;
|
|
|
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
|
|
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
|
|
+import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
|
|
+import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
|
|
+import org.springframework.security.oauth2.jwt.TestJwts;
|
|
|
+import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
|
|
|
+import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
|
|
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
|
|
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
|
|
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
|
|
+
|
|
|
+import static org.assertj.core.api.Assertions.assertThat;
|
|
|
+import static org.assertj.core.api.Assertions.fail;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Tests that Spring Security classes that implements {@link Serializable} and have the
|
|
|
+ * same serial version as {@link SpringSecurityCoreVersion#SERIAL_VERSION_UID} can be
|
|
|
+ * deserialized from a previous minor version.
|
|
|
+ * <p>
|
|
|
+ * For example, all classes from version 6.2.x that matches the previous requirement
|
|
|
+ * should be serialized and saved to a folder, and then later on, in 6.3.x, it is verified
|
|
|
+ * if they can be deserialized
|
|
|
+ *
|
|
|
+ * @author Marcus da Coregio
|
|
|
+ * @since 6.2.2
|
|
|
+ * @see <a href="https://github.com/spring-projects/spring-security/issues/3737">GitHub
|
|
|
+ * Issue #3737</a>
|
|
|
+ */
|
|
|
+class SpringSecurityCoreVersionSerializableTests {
|
|
|
+
|
|
|
+ private static final Map<Class<?>, Generator<?>> generatorByClassName = new HashMap<>();
|
|
|
+
|
|
|
+ static final long securitySerialVersionUid = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
|
|
+
|
|
|
+ static Path currentVersionFolder = Paths.get("src/test/resources/serialized/" + getCurrentVersion());
|
|
|
+
|
|
|
+ static Path previousVersionFolder = Paths.get("src/test/resources/serialized/" + getPreviousVersion());
|
|
|
+
|
|
|
+ static {
|
|
|
+ ClientRegistration.Builder clientRegistrationBuilder = TestClientRegistrations.clientRegistration();
|
|
|
+ ClientRegistration clientRegistration = clientRegistrationBuilder.build();
|
|
|
+ UserDetails user = TestAuthentication.user();
|
|
|
+ WebAuthenticationDetails details = new WebAuthenticationDetails("remote", "sessionId");
|
|
|
+ generatorByClassName.put(DefaultOAuth2User.class, (r) -> TestOAuth2Users.create());
|
|
|
+ generatorByClassName.put(ClientRegistration.class, (r) -> clientRegistration);
|
|
|
+ generatorByClassName.put(ClientRegistration.ProviderDetails.class,
|
|
|
+ (r) -> clientRegistration.getProviderDetails());
|
|
|
+ generatorByClassName.put(ClientRegistration.ProviderDetails.UserInfoEndpoint.class,
|
|
|
+ (r) -> clientRegistration.getProviderDetails().getUserInfoEndpoint());
|
|
|
+ generatorByClassName.put(ClientRegistration.Builder.class, (r) -> clientRegistrationBuilder);
|
|
|
+ generatorByClassName.put(OAuth2AuthorizationRequest.class,
|
|
|
+ (r) -> TestOAuth2AuthorizationRequests.request().build());
|
|
|
+ generatorByClassName.put(OAuth2AuthorizationResponse.class,
|
|
|
+ (r) -> TestOAuth2AuthorizationResponses.success().build());
|
|
|
+ generatorByClassName.put(OAuth2AuthorizedClient.class,
|
|
|
+ (r) -> new OAuth2AuthorizedClient(clientRegistration, "principal", TestOAuth2AccessTokens.noScopes()));
|
|
|
+ generatorByClassName.put(OAuth2UserAuthority.class, (r) -> new OAuth2UserAuthority(Map.of("username", "user")));
|
|
|
+ generatorByClassName.put(OAuth2AuthorizationExchange.class, (r) -> TestOAuth2AuthorizationExchanges.success());
|
|
|
+ generatorByClassName.put(OidcUserInfo.class, (r) -> OidcUserInfo.builder().email("email@example.com").build());
|
|
|
+ generatorByClassName.put(SessionInformation.class,
|
|
|
+ (r) -> new SessionInformation(user, r.alphanumeric(4), new Date(1704378933936L)));
|
|
|
+ generatorByClassName.put(OAuth2LoginAuthenticationToken.class, (r) -> {
|
|
|
+ var token = new OAuth2LoginAuthenticationToken(clientRegistration,
|
|
|
+ TestOAuth2AuthorizationExchanges.success());
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(OAuth2AuthorizationCodeAuthenticationToken.class, (r) -> {
|
|
|
+ var token = TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated();
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName
|
|
|
+ .put(org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken.class, (r) -> {
|
|
|
+ var token = new org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken(
|
|
|
+ "token");
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(BearerTokenAuthenticationToken.class, (r) -> {
|
|
|
+ var token = new BearerTokenAuthenticationToken("token");
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(BearerTokenAuthentication.class, (r) -> {
|
|
|
+ var token = new BearerTokenAuthentication(TestOAuth2AuthenticatedPrincipals.active(),
|
|
|
+ TestOAuth2AccessTokens.noScopes(), user.getAuthorities());
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(OAuth2AuthenticationToken.class, (r) -> {
|
|
|
+ var token = TestOAuth2AuthenticationTokens.authenticated();
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(JwtAuthenticationToken.class, (r) -> {
|
|
|
+ var token = new JwtAuthenticationToken(TestJwts.user());
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(RunAsUserToken.class, (r) -> {
|
|
|
+ RunAsUserToken token = new RunAsUserToken("key", user, "creds", user.getAuthorities(),
|
|
|
+ AnonymousAuthenticationToken.class);
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(CasServiceTicketAuthenticationToken.class, (r) -> {
|
|
|
+ CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless("creds");
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(CasAuthenticationToken.class, (r) -> {
|
|
|
+ var token = new CasAuthenticationToken("key", user, "Password", user.getAuthorities(), user,
|
|
|
+ new AssertionImpl("test"));
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(CasAssertionAuthenticationToken.class, (r) -> {
|
|
|
+ var token = new CasAssertionAuthenticationToken(new AssertionImpl("test"), "ticket");
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(RememberMeAuthenticationToken.class, (r) -> {
|
|
|
+ RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("key", user, user.getAuthorities());
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(PreAuthenticatedAuthenticationToken.class, (r) -> {
|
|
|
+ PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(user, "creds",
|
|
|
+ user.getAuthorities());
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(UsernamePasswordAuthenticationToken.class, (r) -> {
|
|
|
+ var token = UsernamePasswordAuthenticationToken.unauthenticated(user, "creds");
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ generatorByClassName.put(JaasAuthenticationToken.class, (r) -> {
|
|
|
+ var token = new JaasAuthenticationToken(user, "creds", null);
|
|
|
+ token.setDetails(details);
|
|
|
+ return token;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ @ParameterizedTest
|
|
|
+ @MethodSource("getClassesToSerialize")
|
|
|
+ @Disabled("This method should only be used to serialize the classes once")
|
|
|
+ void serializeCurrentVersionClasses(Class<?> clazz) throws Exception {
|
|
|
+ Files.createDirectories(currentVersionFolder);
|
|
|
+ Path filePath = Paths.get(currentVersionFolder.toAbsolutePath() + "/" + clazz.getName());
|
|
|
+ File file = filePath.toFile();
|
|
|
+ if (file.exists()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Files.createFile(filePath);
|
|
|
+ Object instance = instancioWithDefaults(clazz).create();
|
|
|
+ assertThat(instance).isInstanceOf(clazz);
|
|
|
+ try (FileOutputStream fileOutputStream = new FileOutputStream(file);
|
|
|
+ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
|
|
|
+ objectOutputStream.writeObject(instance);
|
|
|
+ objectOutputStream.flush();
|
|
|
+ }
|
|
|
+ catch (NotSerializableException ex) {
|
|
|
+ Files.delete(filePath);
|
|
|
+ fail("Could not serialize " + clazz.getName(), ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ParameterizedTest
|
|
|
+ @MethodSource("getFilesToDeserialize")
|
|
|
+ @Disabled("The feature is only supported for versions >= 6.3")
|
|
|
+ void shouldBeAbleToDeserializeClassFromPreviousVersion(Path filePath) {
|
|
|
+ try (FileInputStream fileInputStream = new FileInputStream(filePath.toFile());
|
|
|
+ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
|
|
|
+ Object obj = objectInputStream.readObject();
|
|
|
+ Class<?> clazz = Class.forName(filePath.getFileName().toString());
|
|
|
+ assertThat(obj).isInstanceOf(clazz);
|
|
|
+ }
|
|
|
+ catch (IOException | ClassNotFoundException ex) {
|
|
|
+ fail("Could not deserialize " + filePath, ex);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static Stream<Path> getFilesToDeserialize() throws IOException {
|
|
|
+ assertThat(previousVersionFolder.toFile().exists())
|
|
|
+ .as("Make sure that the " + previousVersionFolder + " exists and is not empty")
|
|
|
+ .isTrue();
|
|
|
+ try (Stream<Path> files = Files.list(previousVersionFolder)) {
|
|
|
+ if (files.findFirst().isEmpty()) {
|
|
|
+ fail("Please make sure to run SpringSecurityCoreVersionSerializableTests#serializeCurrentVersionClasses for the "
|
|
|
+ + getPreviousVersion() + " version");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Files.list(previousVersionFolder);
|
|
|
+ }
|
|
|
+
|
|
|
+ static Stream<Class<?>> getClassesToSerialize() throws Exception {
|
|
|
+ ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
|
|
|
+ provider.addIncludeFilter(new AssignableTypeFilter(Serializable.class));
|
|
|
+ List<Class<?>> classes = new ArrayList<>();
|
|
|
+
|
|
|
+ Set<BeanDefinition> components = provider.findCandidateComponents("org/springframework/security");
|
|
|
+ for (BeanDefinition component : components) {
|
|
|
+ Class<?> clazz = Class.forName(component.getBeanClassName());
|
|
|
+ boolean isAbstract = Modifier.isAbstract(clazz.getModifiers());
|
|
|
+ boolean matchesExpectedSerialVersion = ObjectStreamClass.lookup(clazz)
|
|
|
+ .getSerialVersionUID() == securitySerialVersionUid;
|
|
|
+ if (!isAbstract && matchesExpectedSerialVersion) {
|
|
|
+ classes.add(clazz);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return classes.stream();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static InstancioApi<?> instancioWithDefaults(Class<?> clazz) {
|
|
|
+ InstancioApi<?> instancio = Instancio.of(clazz);
|
|
|
+ if (generatorByClassName.containsKey(clazz)) {
|
|
|
+ instancio.supply(Select.all(clazz), generatorByClassName.get(clazz));
|
|
|
+ }
|
|
|
+ return instancio;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String getCurrentVersion() {
|
|
|
+ String version = System.getProperty("springSecurityVersion");
|
|
|
+ String[] parts = version.split("\\.");
|
|
|
+ parts[2] = "x";
|
|
|
+ return String.join(".", parts);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String getPreviousVersion() {
|
|
|
+ String version = System.getProperty("springSecurityVersion");
|
|
|
+ String[] parts = version.split("\\.");
|
|
|
+ parts[1] = String.valueOf(Integer.parseInt(parts[1]) - 1);
|
|
|
+ parts[2] = "x";
|
|
|
+ return String.join(".", parts);
|
|
|
+ }
|
|
|
+
|
|
|
+}
|