Skip to content

Commit 4f8c1b3

Browse files
committed
Polish LDAP serialization
Closes gh-9263
1 parent 7cfd415 commit 4f8c1b3

File tree

10 files changed

+352
-128
lines changed

10 files changed

+352
-128
lines changed

core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2020 the original author or authors.
2+
* Copyright 2015-2021 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.
@@ -209,6 +209,7 @@ static class AllowlistTypeIdResolver implements TypeIdResolver {
209209
names.add("java.util.HashMap");
210210
names.add("java.util.LinkedHashMap");
211211
names.add("org.springframework.security.core.context.SecurityContextImpl");
212+
names.add("java.util.Arrays$ArrayList");
212213
ALLOWLIST_CLASS_NAMES = Collections.unmodifiableSet(names);
213214
}
214215

ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2020 the original author or authors.
2+
* Copyright 2015-2021 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.
@@ -21,19 +21,12 @@
2121
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2222

2323
import org.springframework.security.jackson2.SecurityJackson2Modules;
24+
import org.springframework.security.ldap.userdetails.InetOrgPerson;
2425

2526
/**
26-
* This is a Jackson mixin class helps in serialize/deserialize
27-
* {@link org.springframework.security.ldap.userdetails.InetOrgPerson} class. To use this
28-
* class you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}.
29-
*
30-
* <pre>
31-
* ObjectMapper mapper = new ObjectMapper();
32-
* mapper.registerModule(new LdapJackson2Module());
33-
* </pre>
34-
*
35-
* <i>Note: This class will save full class name into a property called @class</i>
27+
* This Jackson mixin is used to serialize/deserialize {@link InetOrgPerson}.
3628
*
29+
* @since 5.7
3730
* @see LdapJackson2Module
3831
* @see SecurityJackson2Modules
3932
*/

ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2020 the original author or authors.
2+
* Copyright 2015-2021 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.
@@ -26,19 +26,12 @@
2626
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2727

2828
import org.springframework.security.jackson2.SecurityJackson2Modules;
29+
import org.springframework.security.ldap.userdetails.LdapAuthority;
2930

3031
/**
31-
* This is a Jackson mixin class helps in serialize/deserialize
32-
* {@link org.springframework.security.ldap.userdetails.LdapAuthority} class. To use this
33-
* class you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}.
34-
*
35-
* <pre>
36-
* ObjectMapper mapper = new ObjectMapper();
37-
* mapper.registerModule(new LdapJackson2Module());
38-
* </pre>
39-
*
40-
* <i>Note: This class will save full class name into a property called @class</i>
32+
* This Jackson mixin is used to serialize/deserialize {@link LdapAuthority}.
4133
*
34+
* @since 5.7
4235
* @see LdapJackson2Module
4336
* @see SecurityJackson2Modules
4437
*/
@@ -47,13 +40,6 @@
4740
@JsonIgnoreProperties(ignoreUnknown = true)
4841
abstract class LdapAuthorityMixin {
4942

50-
/**
51-
* Constructor used by Jackson to create object of
52-
* {@link org.springframework.security.ldap.userdetails.LdapAuthority}.
53-
* @param role
54-
* @param dn
55-
* @param attributes
56-
*/
5743
@JsonCreator
5844
LdapAuthorityMixin(@JsonProperty("role") String role, @JsonProperty("dn") String dn,
5945
@JsonProperty("attributes") Map<String, List<String>> attributes) {

ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2020 the original author or authors.
2+
* Copyright 2015-2021 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.
@@ -26,11 +26,13 @@
2626
import org.springframework.security.ldap.userdetails.Person;
2727

2828
/**
29-
* Jackson module for spring-security-ldap. This module registers
29+
* Jackson module for {@code spring-security-ldap}. This module registers
3030
* {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin},
31-
* {@link InetOrgPersonMixin}. If no default typing enabled by default then it'll enable
32-
* it because typing info is needed to properly serialize/deserialize objects. In order to
33-
* use this module just add this module into your ObjectMapper configuration.
31+
* {@link InetOrgPersonMixin}.
32+
*
33+
* If not already enabled, default typing will be automatically enabled as type info is
34+
* required to properly serialize/deserialize objects. In order to use this module just
35+
* add it to your {@code ObjectMapper} configuration.
3436
*
3537
* <pre>
3638
* ObjectMapper mapper = new ObjectMapper();
@@ -40,6 +42,7 @@
4042
* <b>Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list of all
4143
* security modules.</b>
4244
*
45+
* @since 5.7
4346
* @see SecurityJackson2Modules
4447
*/
4548
public class LdapJackson2Module extends SimpleModule {

ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2020 the original author or authors.
2+
* Copyright 2015-2021 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.
@@ -21,20 +21,12 @@
2121
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2222

2323
import org.springframework.security.jackson2.SecurityJackson2Modules;
24+
import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
2425

2526
/**
26-
* This is a Jackson mixin class helps in serialize/deserialize
27-
* {@link org.springframework.security.ldap.userdetails.LdapUserDetailsImpl} class. To use
28-
* this class you need to register it with
29-
* {@link com.fasterxml.jackson.databind.ObjectMapper}.
30-
*
31-
* <pre>
32-
* ObjectMapper mapper = new ObjectMapper();
33-
* mapper.registerModule(new LdapJackson2Module());
34-
* </pre>
35-
*
36-
* <i>Note: This class will save full class name into a property called @class</i>
27+
* This Jackson mixin is used to serialize/deserialize {@link LdapUserDetailsImpl}.
3728
*
29+
* @since 5.7
3830
* @see LdapJackson2Module
3931
* @see SecurityJackson2Modules
4032
*/

ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2020 the original author or authors.
2+
* Copyright 2015-2021 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.
@@ -21,19 +21,12 @@
2121
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2222

2323
import org.springframework.security.jackson2.SecurityJackson2Modules;
24+
import org.springframework.security.ldap.userdetails.Person;
2425

2526
/**
26-
* This is a Jackson mixin class helps in serialize/deserialize
27-
* {@link org.springframework.security.ldap.userdetails.Person} class. To use this class
28-
* you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}.
29-
*
30-
* <pre>
31-
* ObjectMapper mapper = new ObjectMapper();
32-
* mapper.registerModule(new LdapJackson2Module());
33-
* </pre>
34-
*
35-
* <i>Note: This class will save full class name into a property called @class</i>
27+
* This Jackson mixin is used to serialize/deserialize {@link Person}.
3628
*
29+
* @since 5.7
3730
* @see LdapJackson2Module
3831
* @see SecurityJackson2Modules
3932
*/
Lines changed: 126 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -13,27 +13,74 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.springframework.security.ldap.jackson2;
1716

18-
import org.springframework.ldap.core.DirContextAdapter;
19-
import org.springframework.ldap.core.DistinguishedName;
20-
import org.springframework.security.jackson2.SecurityJackson2Modules;
21-
import org.springframework.security.ldap.userdetails.InetOrgPerson;
22-
import org.springframework.security.ldap.userdetails.Person;
17+
package org.springframework.security.ldap.jackson2;
2318

19+
import com.fasterxml.jackson.core.JsonProcessingException;
2420
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import org.json.JSONException;
2522
import org.junit.jupiter.api.BeforeEach;
26-
import org.junit.jupiter.api.Disabled;
2723
import org.junit.jupiter.api.Test;
2824
import org.skyscreamer.jsonassert.JSONAssert;
2925

26+
import org.springframework.ldap.core.DirContextAdapter;
27+
import org.springframework.ldap.core.DistinguishedName;
28+
import org.springframework.security.core.authority.AuthorityUtils;
29+
import org.springframework.security.jackson2.SecurityJackson2Modules;
30+
import org.springframework.security.ldap.userdetails.InetOrgPerson;
31+
import org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper;
32+
3033
import static org.assertj.core.api.Assertions.assertThat;
31-
import static org.junit.jupiter.api.Assertions.*;
34+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3235

3336
/**
3437
* Tests for {@link InetOrgPersonMixin}.
3538
*/
36-
class InetOrgPersonMixinTests {
39+
public class InetOrgPersonMixinTests {
40+
41+
private static final String USER_PASSWORD = "Password1234";
42+
43+
private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]";
44+
45+
// @formatter:off
46+
private static final String INET_ORG_PERSON_JSON = "{\n"
47+
+ "\"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\","
48+
+ "\"dn\": \"ignored=ignored\","
49+
+ "\"uid\": \"ghengis\","
50+
+ "\"username\": \"ghengis\","
51+
+ "\"password\": \"" + USER_PASSWORD + "\","
52+
+ "\"carLicense\": \"HORS1\","
53+
+ "\"givenName\": \"Ghengis\","
54+
+ "\"destinationIndicator\": \"West\","
55+
+ "\"displayName\": \"Ghengis McCann\","
56+
+ "\"givenName\": \"Ghengis\","
57+
+ "\"homePhone\": \"+467575436521\","
58+
+ "\"initials\": \"G\","
59+
+ "\"employeeNumber\": \"00001\","
60+
+ "\"homePostalAddress\": \"Steppes\","
61+
+ "\"mail\": \"ghengis@mongolia\","
62+
+ "\"mobile\": \"always\","
63+
+ "\"o\": \"Hordes\","
64+
+ "\"ou\": \"Horde1\","
65+
+ "\"postalAddress\": \"On the Move\","
66+
+ "\"postalCode\": \"Changes Frequently\","
67+
+ "\"roomNumber\": \"Yurt 1\","
68+
+ "\"sn\": \"Khan\","
69+
+ "\"street\": \"Westward Avenue\","
70+
+ "\"telephoneNumber\": \"+442075436521\","
71+
+ "\"departmentNumber\": \"5679\","
72+
+ "\"title\": \"T\","
73+
+ "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]],"
74+
+ "\"description\": \"Scary\","
75+
+ "\"accountNonExpired\": true, "
76+
+ "\"accountNonLocked\": true, "
77+
+ "\"credentialsNonExpired\": true, "
78+
+ "\"enabled\": true, "
79+
+ "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + ","
80+
+ "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + ","
81+
+ "\"timeBeforeExpiration\": " + Integer.MAX_VALUE
82+
+ "}";
83+
// @formatter:on
3784

3885
private ObjectMapper mapper;
3986

@@ -44,22 +91,83 @@ public void setup() {
4491
this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
4592
}
4693

47-
@Disabled
4894
@Test
4995
public void serializeWhenMixinRegisteredThenSerializes() throws Exception {
50-
InetOrgPerson.Essence essence = new InetOrgPerson.Essence(createUserContext());
51-
InetOrgPerson p = (InetOrgPerson) essence.createUserDetails();
96+
InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper();
97+
InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis",
98+
AuthorityUtils.NO_AUTHORITIES);
5299

53-
String expectedJson = asJson(p);
54100
String json = this.mapper.writeValueAsString(p);
55-
JSONAssert.assertEquals(expectedJson, json, true);
101+
JSONAssert.assertEquals(INET_ORG_PERSON_JSON, json, true);
102+
}
103+
104+
@Test
105+
public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull()
106+
throws JsonProcessingException, JSONException {
107+
InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper();
108+
InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis",
109+
AuthorityUtils.NO_AUTHORITIES);
110+
p.eraseCredentials();
111+
String actualJson = this.mapper.writeValueAsString(p);
112+
JSONAssert.assertEquals(INET_ORG_PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true);
113+
}
114+
115+
@Test
116+
public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() {
117+
assertThatExceptionOfType(JsonProcessingException.class)
118+
.isThrownBy(() -> new ObjectMapper().readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class));
119+
}
120+
121+
@Test
122+
public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception {
123+
InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper();
124+
InetOrgPerson expectedAuthentication = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis",
125+
AuthorityUtils.NO_AUTHORITIES);
126+
127+
InetOrgPerson authentication = this.mapper.readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class);
128+
assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities());
129+
assertThat(authentication.getCarLicense()).isEqualTo(expectedAuthentication.getCarLicense());
130+
assertThat(authentication.getDepartmentNumber()).isEqualTo(expectedAuthentication.getDepartmentNumber());
131+
assertThat(authentication.getDestinationIndicator())
132+
.isEqualTo(expectedAuthentication.getDestinationIndicator());
133+
assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn());
134+
assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription());
135+
assertThat(authentication.getDisplayName()).isEqualTo(expectedAuthentication.getDisplayName());
136+
assertThat(authentication.getUid()).isEqualTo(expectedAuthentication.getUid());
137+
assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername());
138+
assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword());
139+
assertThat(authentication.getHomePhone()).isEqualTo(expectedAuthentication.getHomePhone());
140+
assertThat(authentication.getEmployeeNumber()).isEqualTo(expectedAuthentication.getEmployeeNumber());
141+
assertThat(authentication.getHomePostalAddress()).isEqualTo(expectedAuthentication.getHomePostalAddress());
142+
assertThat(authentication.getInitials()).isEqualTo(expectedAuthentication.getInitials());
143+
assertThat(authentication.getMail()).isEqualTo(expectedAuthentication.getMail());
144+
assertThat(authentication.getMobile()).isEqualTo(expectedAuthentication.getMobile());
145+
assertThat(authentication.getO()).isEqualTo(expectedAuthentication.getO());
146+
assertThat(authentication.getOu()).isEqualTo(expectedAuthentication.getOu());
147+
assertThat(authentication.getPostalAddress()).isEqualTo(expectedAuthentication.getPostalAddress());
148+
assertThat(authentication.getPostalCode()).isEqualTo(expectedAuthentication.getPostalCode());
149+
assertThat(authentication.getRoomNumber()).isEqualTo(expectedAuthentication.getRoomNumber());
150+
assertThat(authentication.getStreet()).isEqualTo(expectedAuthentication.getStreet());
151+
assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn());
152+
assertThat(authentication.getTitle()).isEqualTo(expectedAuthentication.getTitle());
153+
assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName());
154+
assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber());
155+
assertThat(authentication.getGraceLoginsRemaining())
156+
.isEqualTo(expectedAuthentication.getGraceLoginsRemaining());
157+
assertThat(authentication.getTimeBeforeExpiration())
158+
.isEqualTo(expectedAuthentication.getTimeBeforeExpiration());
159+
assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired());
160+
assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked());
161+
assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled());
162+
assertThat(authentication.isCredentialsNonExpired())
163+
.isEqualTo(expectedAuthentication.isCredentialsNonExpired());
56164
}
57165

58166
private DirContextAdapter createUserContext() {
59167
DirContextAdapter ctx = new DirContextAdapter();
60168
ctx.setDn(new DistinguishedName("ignored=ignored"));
61169
ctx.setAttributeValue("uid", "ghengis");
62-
ctx.setAttributeValue("userPassword", "pillage");
170+
ctx.setAttributeValue("userPassword", USER_PASSWORD);
63171
ctx.setAttributeValue("carLicense", "HORS1");
64172
ctx.setAttributeValue("cn", "Ghengis Khan");
65173
ctx.setAttributeValue("description", "Scary");
@@ -77,19 +185,12 @@ private DirContextAdapter createUserContext() {
77185
ctx.setAttributeValue("postalAddress", "On the Move");
78186
ctx.setAttributeValue("postalCode", "Changes Frequently");
79187
ctx.setAttributeValue("roomNumber", "Yurt 1");
80-
ctx.setAttributeValue("roomNumber", "Yurt 1");
81188
ctx.setAttributeValue("sn", "Khan");
82189
ctx.setAttributeValue("street", "Westward Avenue");
83190
ctx.setAttributeValue("telephoneNumber", "+442075436521");
191+
ctx.setAttributeValue("departmentNumber", "5679");
192+
ctx.setAttributeValue("title", "T");
84193
return ctx;
85194
}
86195

87-
private String asJson(Person person) {
88-
// @formatter:off
89-
return "{\n" +
90-
" \"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\"\n" +
91-
"}";
92-
// @formatter:on
93-
}
94-
95196
}

0 commit comments

Comments
 (0)