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 febd2b755c..ceaed240ab 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java @@ -84,6 +84,8 @@ public final class SecurityJackson2Modules { private static final String javaTimeJackson2ModuleClass = "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"; + private static final String ldapJackson2ModuleClass = "org.springframework.security.ldap.jackson2.LdapJackson2Module"; + private SecurityJackson2Modules() { } @@ -129,6 +131,9 @@ public final class SecurityJackson2Modules { if (ClassUtils.isPresent(javaTimeJackson2ModuleClass, loader)) { addToModulesList(loader, modules, javaTimeJackson2ModuleClass); } + if (ClassUtils.isPresent(ldapJackson2ModuleClass, loader)) { + addToModulesList(loader, modules, ldapJackson2ModuleClass); + } return modules; } diff --git a/ldap/spring-security-ldap.gradle b/ldap/spring-security-ldap.gradle index e16802ea45..f1c8074af4 100644 --- a/ldap/spring-security-ldap.gradle +++ b/ldap/spring-security-ldap.gradle @@ -8,6 +8,7 @@ dependencies { api 'org.springframework:spring-core' api 'org.springframework:spring-tx' + optional 'com.fasterxml.jackson.core:jackson-databind' optional 'ldapsdk:ldapsdk' optional "com.unboundid:unboundid-ldapsdk" optional "org.apache.directory.server:apacheds-core" @@ -34,6 +35,7 @@ dependencies { testImplementation "org.mockito:mockito-core" testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" + testImplementation 'org.skyscreamer:jsonassert' } integrationTest { 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 new file mode 100644 index 0000000000..bcf7dd4220 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-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 com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; + +/** + * 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 + * + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class InetOrgPersonMixin { + +} 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 new file mode 100644 index 0000000000..151500df82 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-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 java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; + +/** + * 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 + * + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +@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 new file mode 100644 index 0000000000..1362f76b00 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-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 com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; +import org.springframework.security.ldap.userdetails.LdapAuthority; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; +import org.springframework.security.ldap.userdetails.Person; + +/** + * Jackson module for 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. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new LdapJackson2Module());
+ * 
+ * + * Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list of all + * security modules. + * + * @see SecurityJackson2Modules + */ +public class LdapJackson2Module extends SimpleModule { + + public LdapJackson2Module() { + super(LdapJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void setupModule(SetupContext context) { + SecurityJackson2Modules.enableDefaultTyping(context.getOwner()); + context.setMixInAnnotations(LdapAuthority.class, LdapAuthorityMixin.class); + context.setMixInAnnotations(LdapUserDetailsImpl.class, LdapUserDetailsImplMixin.class); + context.setMixInAnnotations(Person.class, PersonMixin.class); + context.setMixInAnnotations(InetOrgPerson.class, InetOrgPersonMixin.class); + } + +} 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 new file mode 100644 index 0000000000..ecf060ba49 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-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 com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; + +/** + * 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 + * + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class LdapUserDetailsImplMixin { + +} 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 new file mode 100644 index 0000000000..c261c253a2 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-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 com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; + +/** + * 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 + * + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class PersonMixin { + +} 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 new file mode 100644 index 0000000000..efd328d812 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java @@ -0,0 +1,95 @@ +/* + * 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 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link InetOrgPersonMixin}. + */ +class InetOrgPersonMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + 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(); + + String expectedJson = asJson(p); + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(expectedJson, json, true); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(new DistinguishedName("ignored=ignored")); + ctx.setAttributeValue("uid", "ghengis"); + ctx.setAttributeValue("userPassword", "pillage"); + ctx.setAttributeValue("carLicense", "HORS1"); + ctx.setAttributeValue("cn", "Ghengis Khan"); + ctx.setAttributeValue("description", "Scary"); + ctx.setAttributeValue("destinationIndicator", "West"); + ctx.setAttributeValue("displayName", "Ghengis McCann"); + ctx.setAttributeValue("givenName", "Ghengis"); + ctx.setAttributeValue("homePhone", "+467575436521"); + ctx.setAttributeValue("initials", "G"); + ctx.setAttributeValue("employeeNumber", "00001"); + ctx.setAttributeValue("homePostalAddress", "Steppes"); + ctx.setAttributeValue("mail", "ghengis@mongolia"); + ctx.setAttributeValue("mobile", "always"); + ctx.setAttributeValue("o", "Hordes"); + ctx.setAttributeValue("ou", "Horde1"); + 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"); + 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 new file mode 100644 index 0000000000..b2e1255cde --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java @@ -0,0 +1,25 @@ +/* + * 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 new file mode 100644 index 0000000000..70c8b81d02 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java @@ -0,0 +1,25 @@ +/* + * 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 LdapUserDetailsImplMixin}. + */ +class LdapUserDetailsImplMixinTests { + +} 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 new file mode 100644 index 0000000000..7040c73174 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java @@ -0,0 +1,59 @@ +/* + * 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 org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.Person; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.*; + +/** + * Tests for {@link PersonMixin}. + */ +class PersonMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + 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); + } + + private String asJson(Person person) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.ldap.userdetails.Person\"\n" + + "}"; + // @formatter:on + } +}