From bbeca7cd653e585cc06c1326a3409f4d85f05fc0 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Mon, 29 Nov 2021 17:48:04 +0100 Subject: [PATCH] Polish LDAP serialization Closes gh-9263 --- .../jackson2/SecurityJackson2Modules.java | 3 +- .../ldap/jackson2/InetOrgPersonMixin.java | 15 +- .../ldap/jackson2/LdapAuthorityMixin.java | 22 +-- .../ldap/jackson2/LdapJackson2Module.java | 13 +- .../jackson2/LdapUserDetailsImplMixin.java | 16 +- .../security/ldap/jackson2/PersonMixin.java | 15 +- .../jackson2/InetOrgPersonMixinTests.java | 151 +++++++++++++++--- .../jackson2/LdapAuthorityMixinTests.java | 25 --- .../LdapUserDetailsImplMixinTests.java | 105 +++++++++++- .../ldap/jackson2/PersonMixinTests.java | 115 ++++++++++--- 10 files changed, 352 insertions(+), 128 deletions(-) delete mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java index ceaed240ab..7bd06f379d 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/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); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java index bcf7dd4220..fca449114e 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java +++ b/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}. - * - *
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * 
- * - * Note: This class will save full class name into a property called @class + * This Jackson mixin is used to serialize/deserialize {@link InetOrgPerson}. * + * @since 5.7 * @see LdapJackson2Module * @see SecurityJackson2Modules */ diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java index 151500df82..85fe16f5fd 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java +++ b/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}. - * - *
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * 
- * - * Note: This class will save full class name into a property called @class + * 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> attributes) { diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java index 1362f76b00..62cb17a11a 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java +++ b/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. * *
  *     ObjectMapper mapper = new ObjectMapper();
@@ -40,6 +42,7 @@ import org.springframework.security.ldap.userdetails.Person;
  * Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list of all
  * security modules.
  *
+ * @since 5.7
  * @see SecurityJackson2Modules
  */
 public class LdapJackson2Module extends SimpleModule {
diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java
index ecf060ba49..a441102e6b 100644
--- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java
+++ b/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}.
- *
- * 
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * 
- * - * Note: This class will save full class name into a property called @class + * This Jackson mixin is used to serialize/deserialize {@link LdapUserDetailsImpl}. * + * @since 5.7 * @see LdapJackson2Module * @see SecurityJackson2Modules */ diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java index c261c253a2..a3a0ddebc5 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java +++ b/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}. - * - *
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * 
- * - * Note: This class will save full class name into a property called @class + * This Jackson mixin is used to serialize/deserialize {@link Person}. * + * @since 5.7 * @see LdapJackson2Module * @see SecurityJackson2Modules */ diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java index efd328d812..d9a05e6531 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java +++ b/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 - } - } diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java deleted file mode 100644 index b2e1255cde..0000000000 --- a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java +++ /dev/null @@ -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 { - -} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java index 70c8b81d02..755623ba8f 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java +++ b/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; + } } diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java index 7040c73174..018058888e 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java +++ b/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; + } + }