فهرست منبع

Polish LDAP serialization

Closes gh-9263
Eleftheria Stein 3 سال پیش
والد
کامیت
bbeca7cd65

+ 2 - 1
core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2020 the original author or authors.
+ * Copyright 2015-2021 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.
@@ -209,6 +209,7 @@ public final class SecurityJackson2Modules {
 			names.add("java.util.HashMap");
 			names.add("java.util.LinkedHashMap");
 			names.add("org.springframework.security.core.context.SecurityContextImpl");
+			names.add("java.util.Arrays$ArrayList");
 			ALLOWLIST_CLASS_NAMES = Collections.unmodifiableSet(names);
 		}
 

+ 4 - 11
ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2020 the original author or authors.
+ * Copyright 2015-2021 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.
@@ -21,19 +21,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
 import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.ldap.userdetails.InetOrgPerson;
 
 /**
- * This is a Jackson mixin class helps in serialize/deserialize
- * {@link org.springframework.security.ldap.userdetails.InetOrgPerson} class. To use this
- * class you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}.
- *
- * <pre>
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * </pre>
- *
- * <i>Note: This class will save full class name into a property called @class</i>
+ * This Jackson mixin is used to serialize/deserialize {@link InetOrgPerson}.
  *
+ * @since 5.7
  * @see LdapJackson2Module
  * @see SecurityJackson2Modules
  */

+ 4 - 18
ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2020 the original author or authors.
+ * Copyright 2015-2021 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.
@@ -26,19 +26,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
 import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.ldap.userdetails.LdapAuthority;
 
 /**
- * This is a Jackson mixin class helps in serialize/deserialize
- * {@link org.springframework.security.ldap.userdetails.LdapAuthority} class. To use this
- * class you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}.
- *
- * <pre>
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * </pre>
- *
- * <i>Note: This class will save full class name into a property called @class</i>
+ * This Jackson mixin is used to serialize/deserialize {@link LdapAuthority}.
  *
+ * @since 5.7
  * @see LdapJackson2Module
  * @see SecurityJackson2Modules
  */
@@ -47,13 +40,6 @@ import org.springframework.security.jackson2.SecurityJackson2Modules;
 @JsonIgnoreProperties(ignoreUnknown = true)
 abstract class LdapAuthorityMixin {
 
-	/**
-	 * Constructor used by Jackson to create object of
-	 * {@link org.springframework.security.ldap.userdetails.LdapAuthority}.
-	 * @param role
-	 * @param dn
-	 * @param attributes
-	 */
 	@JsonCreator
 	LdapAuthorityMixin(@JsonProperty("role") String role, @JsonProperty("dn") String dn,
 			@JsonProperty("attributes") Map<String, List<String>> attributes) {

+ 8 - 5
ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2020 the original author or authors.
+ * Copyright 2015-2021 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.
@@ -26,11 +26,13 @@ import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
 import org.springframework.security.ldap.userdetails.Person;
 
 /**
- * Jackson module for spring-security-ldap. This module registers
+ * Jackson module for {@code spring-security-ldap}. This module registers
  * {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin},
- * {@link InetOrgPersonMixin}. If no default typing enabled by default then it'll enable
- * it because typing info is needed to properly serialize/deserialize objects. In order to
- * use this module just add this module into your ObjectMapper configuration.
+ * {@link InetOrgPersonMixin}.
+ *
+ * If not already enabled, default typing will be automatically enabled as type info is
+ * required to properly serialize/deserialize objects. In order to use this module just
+ * add it to your {@code ObjectMapper} configuration.
  *
  * <pre>
  *     ObjectMapper mapper = new ObjectMapper();
@@ -40,6 +42,7 @@ import org.springframework.security.ldap.userdetails.Person;
  * <b>Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list of all
  * security modules.</b>
  *
+ * @since 5.7
  * @see SecurityJackson2Modules
  */
 public class LdapJackson2Module extends SimpleModule {

+ 4 - 12
ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2020 the original author or authors.
+ * Copyright 2015-2021 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.
@@ -21,20 +21,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
 import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
 
 /**
- * This is a Jackson mixin class helps in serialize/deserialize
- * {@link org.springframework.security.ldap.userdetails.LdapUserDetailsImpl} class. To use
- * this class you need to register it with
- * {@link com.fasterxml.jackson.databind.ObjectMapper}.
- *
- * <pre>
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * </pre>
- *
- * <i>Note: This class will save full class name into a property called @class</i>
+ * This Jackson mixin is used to serialize/deserialize {@link LdapUserDetailsImpl}.
  *
+ * @since 5.7
  * @see LdapJackson2Module
  * @see SecurityJackson2Modules
  */

+ 4 - 11
ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2020 the original author or authors.
+ * Copyright 2015-2021 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.
@@ -21,19 +21,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
 import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.ldap.userdetails.Person;
 
 /**
- * This is a Jackson mixin class helps in serialize/deserialize
- * {@link org.springframework.security.ldap.userdetails.Person} class. To use this class
- * you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}.
- *
- * <pre>
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * </pre>
- *
- * <i>Note: This class will save full class name into a property called @class</i>
+ * This Jackson mixin is used to serialize/deserialize {@link Person}.
  *
+ * @since 5.7
  * @see LdapJackson2Module
  * @see SecurityJackson2Modules
  */

+ 126 - 25
ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -13,27 +13,74 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.security.ldap.jackson2;
 
-import org.springframework.ldap.core.DirContextAdapter;
-import org.springframework.ldap.core.DistinguishedName;
-import org.springframework.security.jackson2.SecurityJackson2Modules;
-import org.springframework.security.ldap.userdetails.InetOrgPerson;
-import org.springframework.security.ldap.userdetails.Person;
+package org.springframework.security.ldap.jackson2;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.skyscreamer.jsonassert.JSONAssert;
 
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.ldap.userdetails.InetOrgPerson;
+import org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper;
+
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 /**
  * Tests for {@link InetOrgPersonMixin}.
  */
-class InetOrgPersonMixinTests {
+public class InetOrgPersonMixinTests {
+
+	private static final String USER_PASSWORD = "Password1234";
+
+	private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]";
+
+	// @formatter:off
+	private static final String INET_ORG_PERSON_JSON = "{\n"
+			+ "\"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\","
+			+ "\"dn\": \"ignored=ignored\","
+			+ "\"uid\": \"ghengis\","
+			+ "\"username\": \"ghengis\","
+			+ "\"password\": \"" + USER_PASSWORD + "\","
+			+ "\"carLicense\": \"HORS1\","
+			+ "\"givenName\": \"Ghengis\","
+			+ "\"destinationIndicator\": \"West\","
+			+ "\"displayName\": \"Ghengis McCann\","
+			+ "\"givenName\": \"Ghengis\","
+			+ "\"homePhone\": \"+467575436521\","
+			+ "\"initials\": \"G\","
+			+ "\"employeeNumber\": \"00001\","
+			+ "\"homePostalAddress\": \"Steppes\","
+			+ "\"mail\": \"ghengis@mongolia\","
+			+ "\"mobile\": \"always\","
+			+ "\"o\": \"Hordes\","
+			+ "\"ou\": \"Horde1\","
+			+ "\"postalAddress\": \"On the Move\","
+			+ "\"postalCode\": \"Changes Frequently\","
+			+ "\"roomNumber\": \"Yurt 1\","
+			+ "\"sn\": \"Khan\","
+			+ "\"street\": \"Westward Avenue\","
+			+ "\"telephoneNumber\": \"+442075436521\","
+			+ "\"departmentNumber\": \"5679\","
+			+ "\"title\": \"T\","
+			+ "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]],"
+			+ "\"description\": \"Scary\","
+			+ "\"accountNonExpired\": true, "
+			+ "\"accountNonLocked\": true, "
+			+ "\"credentialsNonExpired\": true, "
+			+ "\"enabled\": true, "
+			+ "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + ","
+			+ "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + ","
+			+ "\"timeBeforeExpiration\": " + Integer.MAX_VALUE
+			+ "}";
+	// @formatter:on
 
 	private ObjectMapper mapper;
 
@@ -44,22 +91,83 @@ class InetOrgPersonMixinTests {
 		this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
 	}
 
-	@Disabled
 	@Test
 	public void serializeWhenMixinRegisteredThenSerializes() throws Exception {
-		InetOrgPerson.Essence essence = new InetOrgPerson.Essence(createUserContext());
-		InetOrgPerson p = (InetOrgPerson) essence.createUserDetails();
+		InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper();
+		InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis",
+				AuthorityUtils.NO_AUTHORITIES);
 
-		String expectedJson = asJson(p);
 		String json = this.mapper.writeValueAsString(p);
-		JSONAssert.assertEquals(expectedJson, json, true);
+		JSONAssert.assertEquals(INET_ORG_PERSON_JSON, json, true);
+	}
+
+	@Test
+	public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull()
+			throws JsonProcessingException, JSONException {
+		InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper();
+		InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis",
+				AuthorityUtils.NO_AUTHORITIES);
+		p.eraseCredentials();
+		String actualJson = this.mapper.writeValueAsString(p);
+		JSONAssert.assertEquals(INET_ORG_PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true);
+	}
+
+	@Test
+	public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() {
+		assertThatExceptionOfType(JsonProcessingException.class)
+				.isThrownBy(() -> new ObjectMapper().readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class));
+	}
+
+	@Test
+	public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception {
+		InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper();
+		InetOrgPerson expectedAuthentication = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis",
+				AuthorityUtils.NO_AUTHORITIES);
+
+		InetOrgPerson authentication = this.mapper.readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class);
+		assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities());
+		assertThat(authentication.getCarLicense()).isEqualTo(expectedAuthentication.getCarLicense());
+		assertThat(authentication.getDepartmentNumber()).isEqualTo(expectedAuthentication.getDepartmentNumber());
+		assertThat(authentication.getDestinationIndicator())
+				.isEqualTo(expectedAuthentication.getDestinationIndicator());
+		assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn());
+		assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription());
+		assertThat(authentication.getDisplayName()).isEqualTo(expectedAuthentication.getDisplayName());
+		assertThat(authentication.getUid()).isEqualTo(expectedAuthentication.getUid());
+		assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername());
+		assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword());
+		assertThat(authentication.getHomePhone()).isEqualTo(expectedAuthentication.getHomePhone());
+		assertThat(authentication.getEmployeeNumber()).isEqualTo(expectedAuthentication.getEmployeeNumber());
+		assertThat(authentication.getHomePostalAddress()).isEqualTo(expectedAuthentication.getHomePostalAddress());
+		assertThat(authentication.getInitials()).isEqualTo(expectedAuthentication.getInitials());
+		assertThat(authentication.getMail()).isEqualTo(expectedAuthentication.getMail());
+		assertThat(authentication.getMobile()).isEqualTo(expectedAuthentication.getMobile());
+		assertThat(authentication.getO()).isEqualTo(expectedAuthentication.getO());
+		assertThat(authentication.getOu()).isEqualTo(expectedAuthentication.getOu());
+		assertThat(authentication.getPostalAddress()).isEqualTo(expectedAuthentication.getPostalAddress());
+		assertThat(authentication.getPostalCode()).isEqualTo(expectedAuthentication.getPostalCode());
+		assertThat(authentication.getRoomNumber()).isEqualTo(expectedAuthentication.getRoomNumber());
+		assertThat(authentication.getStreet()).isEqualTo(expectedAuthentication.getStreet());
+		assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn());
+		assertThat(authentication.getTitle()).isEqualTo(expectedAuthentication.getTitle());
+		assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName());
+		assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber());
+		assertThat(authentication.getGraceLoginsRemaining())
+				.isEqualTo(expectedAuthentication.getGraceLoginsRemaining());
+		assertThat(authentication.getTimeBeforeExpiration())
+				.isEqualTo(expectedAuthentication.getTimeBeforeExpiration());
+		assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired());
+		assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked());
+		assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled());
+		assertThat(authentication.isCredentialsNonExpired())
+				.isEqualTo(expectedAuthentication.isCredentialsNonExpired());
 	}
 
 	private DirContextAdapter createUserContext() {
 		DirContextAdapter ctx = new DirContextAdapter();
 		ctx.setDn(new DistinguishedName("ignored=ignored"));
 		ctx.setAttributeValue("uid", "ghengis");
-		ctx.setAttributeValue("userPassword", "pillage");
+		ctx.setAttributeValue("userPassword", USER_PASSWORD);
 		ctx.setAttributeValue("carLicense", "HORS1");
 		ctx.setAttributeValue("cn", "Ghengis Khan");
 		ctx.setAttributeValue("description", "Scary");
@@ -77,19 +185,12 @@ class InetOrgPersonMixinTests {
 		ctx.setAttributeValue("postalAddress", "On the Move");
 		ctx.setAttributeValue("postalCode", "Changes Frequently");
 		ctx.setAttributeValue("roomNumber", "Yurt 1");
-		ctx.setAttributeValue("roomNumber", "Yurt 1");
 		ctx.setAttributeValue("sn", "Khan");
 		ctx.setAttributeValue("street", "Westward Avenue");
 		ctx.setAttributeValue("telephoneNumber", "+442075436521");
+		ctx.setAttributeValue("departmentNumber", "5679");
+		ctx.setAttributeValue("title", "T");
 		return ctx;
 	}
 
-	private String asJson(Person person) {
-		// @formatter:off
-		return "{\n" +
-			   "    \"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\"\n" +
-			   "}";
-		// @formatter:on
-	}
-
 }

+ 0 - 25
ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java

@@ -1,25 +0,0 @@
-/*
- * Copyright 2002-2020 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.ldap.jackson2;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * Tests for {@link LdapAuthorityMixin}.
- */
-class LdapAuthorityMixinTests {
-
-}

+ 103 - 2
ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java

@@ -13,13 +13,114 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.springframework.security.ldap.jackson2;
 
-import static org.junit.jupiter.api.Assertions.*;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 /**
  * Tests for {@link LdapUserDetailsImplMixin}.
  */
-class LdapUserDetailsImplMixinTests {
+public class LdapUserDetailsImplMixinTests {
+
+	private static final String USER_PASSWORD = "Password1234";
+
+	private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]";
+
+	// @formatter:off
+	private static final String USER_JSON = "{"
+			+ "\"@class\": \"org.springframework.security.ldap.userdetails.LdapUserDetailsImpl\", "
+			+ "\"dn\": \"ignored=ignored\","
+			+ "\"username\": \"ghengis\","
+			+ "\"password\": \"" + USER_PASSWORD + "\","
+			+ "\"accountNonExpired\": true, "
+			+ "\"accountNonLocked\": true, "
+			+ "\"credentialsNonExpired\": true, "
+			+ "\"enabled\": true, "
+			+ "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + ","
+			+ "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + ","
+			+ "\"timeBeforeExpiration\": " + Integer.MAX_VALUE
+			+ "}";
+	// @formatter:on
+
+	private ObjectMapper mapper;
+
+	@BeforeEach
+	public void setup() {
+		ClassLoader loader = getClass().getClassLoader();
+		this.mapper = new ObjectMapper();
+		this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
+	}
+
+	@Test
+	public void serializeWhenMixinRegisteredThenSerializes() throws Exception {
+		LdapUserDetailsMapper mapper = new LdapUserDetailsMapper();
+		LdapUserDetailsImpl p = (LdapUserDetailsImpl) mapper.mapUserFromContext(createUserContext(), "ghengis",
+				AuthorityUtils.NO_AUTHORITIES);
+
+		String json = this.mapper.writeValueAsString(p);
+		JSONAssert.assertEquals(USER_JSON, json, true);
+	}
+
+	@Test
+	public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull()
+			throws JsonProcessingException, JSONException {
+		LdapUserDetailsMapper mapper = new LdapUserDetailsMapper();
+		LdapUserDetailsImpl p = (LdapUserDetailsImpl) mapper.mapUserFromContext(createUserContext(), "ghengis",
+				AuthorityUtils.NO_AUTHORITIES);
+		p.eraseCredentials();
+		String actualJson = this.mapper.writeValueAsString(p);
+		JSONAssert.assertEquals(USER_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true);
+	}
+
+	@Test
+	public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() {
+		assertThatExceptionOfType(JsonProcessingException.class)
+				.isThrownBy(() -> new ObjectMapper().readValue(USER_JSON, LdapUserDetailsImpl.class));
+	}
+
+	@Test
+	public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception {
+		LdapUserDetailsMapper mapper = new LdapUserDetailsMapper();
+		LdapUserDetailsImpl expectedAuthentication = (LdapUserDetailsImpl) mapper
+				.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES);
+
+		LdapUserDetailsImpl authentication = this.mapper.readValue(USER_JSON, LdapUserDetailsImpl.class);
+		assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities());
+		assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn());
+		assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername());
+		assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword());
+		assertThat(authentication.getGraceLoginsRemaining())
+				.isEqualTo(expectedAuthentication.getGraceLoginsRemaining());
+		assertThat(authentication.getTimeBeforeExpiration())
+				.isEqualTo(expectedAuthentication.getTimeBeforeExpiration());
+		assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired());
+		assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked());
+		assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled());
+		assertThat(authentication.isCredentialsNonExpired())
+				.isEqualTo(expectedAuthentication.isCredentialsNonExpired());
+	}
+
+	private DirContextAdapter createUserContext() {
+		DirContextAdapter ctx = new DirContextAdapter();
+		ctx.setDn(new DistinguishedName("ignored=ignored"));
+		ctx.setAttributeValue("userPassword", USER_PASSWORD);
+		return ctx;
+	}
 
 }

+ 97 - 18
ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -13,23 +13,55 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.security.ldap.jackson2;
 
-import org.springframework.security.jackson2.SecurityJackson2Modules;
-import org.springframework.security.ldap.userdetails.Person;
+package org.springframework.security.ldap.jackson2;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.skyscreamer.jsonassert.JSONAssert;
 
-import static org.junit.jupiter.api.Assertions.*;
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.ldap.userdetails.Person;
+import org.springframework.security.ldap.userdetails.PersonContextMapper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 /**
  * Tests for {@link PersonMixin}.
  */
-class PersonMixinTests {
+public class PersonMixinTests {
+
+	private static final String USER_PASSWORD = "Password1234";
+
+	private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]";
+
+	// @formatter:off
+	private static final String PERSON_JSON = "{"
+			+ "\"@class\": \"org.springframework.security.ldap.userdetails.Person\", "
+			+ "\"dn\": \"ignored=ignored\","
+			+ "\"username\": \"ghengis\","
+			+ "\"password\": \"" + USER_PASSWORD + "\","
+			+ "\"givenName\": \"Ghengis\","
+			+ "\"sn\": \"Khan\","
+			+ "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]],"
+			+ "\"description\": \"Scary\","
+			+ "\"telephoneNumber\": \"+442075436521\","
+			+ "\"accountNonExpired\": true, "
+			+ "\"accountNonLocked\": true, "
+			+ "\"credentialsNonExpired\": true, "
+			+ "\"enabled\": true, "
+			+ "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + ","
+			+ "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + ","
+			+ "\"timeBeforeExpiration\": " + Integer.MAX_VALUE
+			+ "}";
+	// @formatter:on
 
 	private ObjectMapper mapper;
 
@@ -40,20 +72,67 @@ class PersonMixinTests {
 		this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
 	}
 
-	@Disabled
 	@Test
 	public void serializeWhenMixinRegisteredThenSerializes() throws Exception {
-		Person person = null;
-		String expectedJson = asJson(person);
-		String json = this.mapper.writeValueAsString(person);
-		JSONAssert.assertEquals(expectedJson, json, true);
+		PersonContextMapper mapper = new PersonContextMapper();
+		Person p = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES);
+
+		String json = this.mapper.writeValueAsString(p);
+		JSONAssert.assertEquals(PERSON_JSON, json, true);
 	}
 
-	private String asJson(Person person) {
-		// @formatter:off
-		return "{\n" +
-			   "    \"@class\": \"org.springframework.security.ldap.userdetails.Person\"\n" +
-			   "}";
-		// @formatter:on
+	@Test
+	public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull()
+			throws JsonProcessingException, JSONException {
+		PersonContextMapper mapper = new PersonContextMapper();
+		Person p = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES);
+		p.eraseCredentials();
+		String actualJson = this.mapper.writeValueAsString(p);
+		JSONAssert.assertEquals(PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true);
+	}
+
+	@Test
+	public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() {
+		assertThatExceptionOfType(JsonProcessingException.class)
+				.isThrownBy(() -> new ObjectMapper().readValue(PERSON_JSON, Person.class));
 	}
+
+	@Test
+	public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception {
+		PersonContextMapper mapper = new PersonContextMapper();
+		Person expectedAuthentication = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis",
+				AuthorityUtils.NO_AUTHORITIES);
+
+		Person authentication = this.mapper.readValue(PERSON_JSON, Person.class);
+		assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities());
+		assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn());
+		assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription());
+		assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername());
+		assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword());
+		assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn());
+		assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName());
+		assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber());
+		assertThat(authentication.getGraceLoginsRemaining())
+				.isEqualTo(expectedAuthentication.getGraceLoginsRemaining());
+		assertThat(authentication.getTimeBeforeExpiration())
+				.isEqualTo(expectedAuthentication.getTimeBeforeExpiration());
+		assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired());
+		assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked());
+		assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled());
+		assertThat(authentication.isCredentialsNonExpired())
+				.isEqualTo(expectedAuthentication.isCredentialsNonExpired());
+	}
+
+	private DirContextAdapter createUserContext() {
+		DirContextAdapter ctx = new DirContextAdapter();
+		ctx.setDn(new DistinguishedName("ignored=ignored"));
+		ctx.setAttributeValue("userPassword", USER_PASSWORD);
+		ctx.setAttributeValue("cn", "Ghengis Khan");
+		ctx.setAttributeValue("description", "Scary");
+		ctx.setAttributeValue("givenName", "Ghengis");
+		ctx.setAttributeValue("sn", "Khan");
+		ctx.setAttributeValue("telephoneNumber", "+442075436521");
+		return ctx;
+	}
+
 }