diff --git a/servlet/spring-boot/java/hello/gradle/libs.versions.toml b/servlet/spring-boot/java/hello/gradle/libs.versions.toml
deleted file mode 120000
index 8adb6c9..0000000
--- a/servlet/spring-boot/java/hello/gradle/libs.versions.toml
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../gradle/libs.versions.toml
\ No newline at end of file
diff --git a/servlet/spring-boot/java/hello/src/main/resources/templates/index.html b/servlet/spring-boot/java/hello/src/main/resources/templates/index.html
deleted file mode 100644
index 4e71378..0000000
--- a/servlet/spring-boot/java/hello/src/main/resources/templates/index.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- Hello Security!
-
-
-
Hello Security
- Log Out
-
-
\ No newline at end of file
diff --git a/servlet/spring-boot/java/hello/src/test/java/example/HelloApplicationTests.java b/servlet/spring-boot/java/hello/src/test/java/example/HelloApplicationTests.java
deleted file mode 100644
index ac8c2a2..0000000
--- a/servlet/spring-boot/java/hello/src/test/java/example/HelloApplicationTests.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2002-2024 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package example;
-
-import org.junit.jupiter.api.Test;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.web.servlet.MockMvc;
-
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-/**
- * @author Rob Winch
- */
-@SpringBootTest
-@AutoConfigureMockMvc
-public class HelloApplicationTests {
-
- @Autowired
- private MockMvc mockMvc;
-
- @Test
- void indexThenOk() throws Exception {
- // @formatter:off
- this.mockMvc.perform(get("/"))
- .andExpect(status().isOk());
- // @formatter:on
- }
-
-}
diff --git a/servlet/spring-boot/java/security/README.adoc b/servlet/spring-boot/java/security/README.adoc
new file mode 100644
index 0000000..3cf160e
--- /dev/null
+++ b/servlet/spring-boot/java/security/README.adoc
@@ -0,0 +1,72 @@
+= A CRUD Spring LDAP application using Spring Boot
+
+The application is protected by Spring Security and uses an embedded UnboundID container for its LDAP server.
+
+You can authenticate with HTTP basic using `dante`/`secret`:
+
+[source,bash]
+----
+curl --user dante:secret localhost:8080/people
+----
+
+And you should see a response like this one:
+
+[source,bash]
+----
+[
+[
+ {
+ "dn": "uid=dante,ou=people",
+ "lastName": "Alvarez",
+ "username": "dante"
+ },
+ {
+ "dn": "uid=hal,ou=people",
+ "lastName": "Hal",
+ "username": "hal"
+ },
+ {
+ "dn": "uid=may,ou=people",
+ "lastName": "May",
+ "username": "may"
+ },
+ // ...
+}
+----
+
+Or, if you use `hal`/`sorrydave`, you'll see more information:
+
+[source,bash]
+----
+[
+ {
+ "dn": "uid=dante,ou=people",
+ "lastName": "Alvarez",
+ "name": "Dante Alvarez",
+ "username": "dante"
+ },
+ {
+ "dn": "uid=hal,ou=people",
+ "lastName": "Hal",
+ "name": "Hal 2000",
+ "username": "hal"
+ },
+ {
+ "dn": "uid=may,ou=people",
+ "lastName": "May",
+ "name": "May Bea",
+ "username": "may"
+ },
+ // ...
+----
+
+The sample supports the following operations:
+
+* `GET /people` - retrieve all the people in the application
+* `GET /people/uid=may,ou=people` - retrieve May Bea's details; you can replace the DN with another one to see another person's information
+* `GET /people/me` - retrieve the current user's details
+* `POST /people` - add a new person, for example `{ "username": "newuser", "sn": "User", "cn": "New User" }`
+* `PUT /people/uid=may,ou=people` - update May Bea's details, supports partial update; you can replace the DN with another one to make changes to a different entry
+* `DELETE /people/uid=may,ou=people` - remove May Bea from the system; you can replace the DN with another one to remove a different entry
+
+To run the sample, do `./gradlew :bootRun`.
diff --git a/servlet/spring-boot/java/hello/build.gradle b/servlet/spring-boot/java/security/build.gradle
similarity index 56%
rename from servlet/spring-boot/java/hello/build.gradle
rename to servlet/spring-boot/java/security/build.gradle
index d80999c..2141c23 100644
--- a/servlet/spring-boot/java/hello/build.gradle
+++ b/servlet/spring-boot/java/security/build.gradle
@@ -11,12 +11,17 @@ repositories {
maven { url "https://repo.spring.io/snapshot" }
}
+ext['spring-security.version'] = "6.4.0-SNAPSHOT"
dependencies {
- implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ implementation 'org.springframework.boot:spring-boot-starter-data-ldap'
implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.security:spring-security-ldap'
+ implementation 'com.unboundid:unboundid-ldapsdk:7.0.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
}
tasks.withType(Test).configureEach {
diff --git a/servlet/spring-boot/java/hello/gradle.properties b/servlet/spring-boot/java/security/gradle.properties
similarity index 73%
rename from servlet/spring-boot/java/hello/gradle.properties
rename to servlet/spring-boot/java/security/gradle.properties
index 06c276c..817ab8a 100644
--- a/servlet/spring-boot/java/hello/gradle.properties
+++ b/servlet/spring-boot/java/security/gradle.properties
@@ -1,4 +1,4 @@
version=6.1.1
-spring-ldap.version=6.4.0-SNAPSHOT
+spring-ldap.version=3.3.0-SNAPSHOT
org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError
org.gradle.caching=true
diff --git a/servlet/spring-boot/java/security/gradle/libs.versions.toml b/servlet/spring-boot/java/security/gradle/libs.versions.toml
new file mode 100644
index 0000000..4266b1c
--- /dev/null
+++ b/servlet/spring-boot/java/security/gradle/libs.versions.toml
@@ -0,0 +1,12 @@
+[versions]
+org-springframework-boot = "3.4.0-SNAPSHOT"
+
+[libraries]
+org-springframework-spring-framework-bom = "org.springframework:spring-framework-bom:6.2.0-M6"
+org-springframework-data-spring-data-bom = "org.springframework.data:spring-data-bom:2024.0.2"
+org-springframework-ldap-spring-ldap-core = "org.springframework.ldap:spring-ldap-core:3.3.0-SNAPSHOT"
+org-springframework-ldap-spring-ldap-test = "org.springframework.ldap:spring-ldap-test:3.3.0-SNAPSHOT"
+
+[plugins]
+io-spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.6" }
+org-springframework-boot = { id = "org.springframework.boot", version.ref = "org-springframework-boot" }
diff --git a/servlet/spring-boot/java/hello/gradle/wrapper/gradle-wrapper.jar b/servlet/spring-boot/java/security/gradle/wrapper/gradle-wrapper.jar
similarity index 100%
rename from servlet/spring-boot/java/hello/gradle/wrapper/gradle-wrapper.jar
rename to servlet/spring-boot/java/security/gradle/wrapper/gradle-wrapper.jar
diff --git a/servlet/spring-boot/java/hello/gradle/wrapper/gradle-wrapper.properties b/servlet/spring-boot/java/security/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
rename from servlet/spring-boot/java/hello/gradle/wrapper/gradle-wrapper.properties
rename to servlet/spring-boot/java/security/gradle/wrapper/gradle-wrapper.properties
diff --git a/servlet/spring-boot/java/hello/gradlew b/servlet/spring-boot/java/security/gradlew
similarity index 100%
rename from servlet/spring-boot/java/hello/gradlew
rename to servlet/spring-boot/java/security/gradlew
diff --git a/servlet/spring-boot/java/hello/gradlew.bat b/servlet/spring-boot/java/security/gradlew.bat
similarity index 100%
rename from servlet/spring-boot/java/hello/gradlew.bat
rename to servlet/spring-boot/java/security/gradlew.bat
diff --git a/servlet/spring-boot/java/hello/settings.gradle b/servlet/spring-boot/java/security/settings.gradle
similarity index 100%
rename from servlet/spring-boot/java/hello/settings.gradle
rename to servlet/spring-boot/java/security/settings.gradle
diff --git a/servlet/spring-boot/java/security/src/main/java/example/Person.java b/servlet/spring-boot/java/security/src/main/java/example/Person.java
new file mode 100644
index 0000000..9e14647
--- /dev/null
+++ b/servlet/spring-boot/java/security/src/main/java/example/Person.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package example;
+
+import javax.naming.Name;
+
+import example.security.NullMethodAuthorizationDeniedHandler;
+
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.DnAttribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.method.HandleAuthorizationDenied;
+
+@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }, base = "ou=people")
+public class Person {
+
+ @Id
+ private Name dn;
+
+ @DnAttribute(value = "uid", index = 1)
+ @Attribute(name = "uid")
+ private String username;
+
+ @Attribute(name = "cn")
+ private String name;
+
+ @Attribute(name = "sn")
+ private String lastName;
+
+ public Person() {
+
+ }
+
+ public Person(String username) {
+ this.username = username;
+ }
+
+ public Person(Person person) {
+ this.dn = person.getDn();
+ this.username = person.getUsername();
+ this.name = person.getName();
+ this.lastName = person.getLastName();
+ }
+
+ public Name getDn() {
+ return this.dn;
+ }
+
+ public void setDn(Name dn) {
+ this.dn = dn;
+ }
+
+ public String getUsername() {
+ return this.username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ @PreAuthorize("hasAuthority('ROLE_ADMIN')")
+ @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class)
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLastName() {
+ return this.lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+}
diff --git a/servlet/spring-boot/java/security/src/main/java/example/PersonContextMapper.java b/servlet/spring-boot/java/security/src/main/java/example/PersonContextMapper.java
new file mode 100644
index 0000000..f501959
--- /dev/null
+++ b/servlet/spring-boot/java/security/src/main/java/example/PersonContextMapper.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package example;
+
+import java.util.Collection;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import example.ldap.DirContextOperationsMapper;
+
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.LdapClient;
+import org.springframework.ldap.odm.core.ObjectDirectoryMapper;
+import org.springframework.ldap.odm.core.impl.DefaultObjectDirectoryMapper;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
+
+public final class PersonContextMapper implements UserDetailsContextMapper {
+
+ private final LdapClient ldap;
+
+ private final ObjectDirectoryMapper odm = new DefaultObjectDirectoryMapper();
+
+ public PersonContextMapper(LdapClient ldap) {
+ this.ldap = ldap;
+ }
+
+ @Override
+ public UserDetails mapUserFromContext(DirContextOperations ctx, String username,
+ Collection extends GrantedAuthority> authorities) {
+ Person person = this.odm.mapFromLdapDataEntry(ctx, Person.class);
+ DirContextOperationsMapper toAuthority = (c) -> {
+ List members = List.of(c.getStringAttributes("uniqueMember"));
+ return (members.contains(ctx.getNameInNamespace())) ? "ROLE_ADMIN" : "ROLE_USER";
+ };
+ String authority = this.ldap.search().name("cn=managers,ou=groups").toObject(toAuthority);
+ return new UserDetailsPerson(person, authority);
+ }
+
+ @Override
+ public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
+ throw new UnsupportedOperationException("not supported");
+ }
+
+ @JsonSerialize(as = Person.class)
+ public static class UserDetailsPerson extends Person implements UserDetails {
+
+ Collection authorities;
+
+ public UserDetailsPerson(Person person, String authority) {
+ super(person);
+ this.authorities = List.of(new SimpleGrantedAuthority(authority));
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return this.authorities;
+ }
+
+ @Override
+ public String getPassword() {
+ return null;
+ }
+
+ }
+
+}
diff --git a/servlet/spring-boot/java/security/src/main/java/example/PersonController.java b/servlet/spring-boot/java/security/src/main/java/example/PersonController.java
new file mode 100644
index 0000000..f34700d
--- /dev/null
+++ b/servlet/spring-boot/java/security/src/main/java/example/PersonController.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package example;
+
+import java.io.IOException;
+
+import javax.naming.Name;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.ModificationItem;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.LdapClient;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authorization.method.AuthorizeReturnObject;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Controller for "/".
+ *
+ * @author Josh Cummings
+ */
+@RestController
+@RequestMapping("/people")
+@AuthorizeReturnObject
+public class PersonController {
+
+ private final PersonRepository persons;
+
+ private final ObjectMapper mapper;
+
+ private final LdapClient ldap;
+
+ public PersonController(PersonRepository persons, ObjectMapper mapper, LdapClient ldap) {
+ this.persons = persons;
+ this.mapper = mapper;
+ this.ldap = ldap;
+ }
+
+ @GetMapping
+ @PreAuthorize("hasAuthority('ROLE_USER')")
+ public Iterable list() {
+ Iterable people = this.persons.findAll();
+ return people;
+ }
+
+ @GetMapping("/me")
+ @PreAuthorize("isAuthenticated()")
+ public Person me(@AuthenticationPrincipal Person person) {
+ return person;
+ }
+
+ @GetMapping("/{uid}")
+ @PreAuthorize("hasAuthority('ROLE_USER')")
+ public Person get(@PathVariable("uid") Name uid) {
+ return this.persons.findByDn(uid);
+ }
+
+ @PostMapping
+ @PreAuthorize("hasAuthority('ROLE_ADMIN')")
+ public Person create(@RequestBody Person person) {
+ return this.persons.save(person);
+ }
+
+ @PutMapping("/{uid}")
+ @PreAuthorize("hasAuthority('ROLE_ADMIN')")
+ public Person update(@PathVariable("uid") Name uid, @RequestBody String person) throws IOException {
+ Person toUpdate = this.persons.findByDn(uid);
+ this.mapper.readerForUpdating(toUpdate).readValue(person, Person.class);
+ toUpdate.setDn(uid);
+ return this.persons.save(toUpdate);
+ }
+
+ @PutMapping("/{uid}/password")
+ @PreAuthorize("hasAuthority('ROLE_ADMIN')")
+ public void updatePassword(@PathVariable("uid") Name uid, @RequestBody Attributes attributes) {
+ Attribute password = attributes.get("userPassword");
+ this.ldap.modify(uid)
+ .attributes(new ModificationItem(DirContextOperations.REPLACE_ATTRIBUTE, password))
+ .execute();
+ }
+
+ @DeleteMapping("/{uid}")
+ @PreAuthorize("hasAuthority('ROLE_ADMIN')")
+ public void delete(@PathVariable("uid") Name uid) {
+ this.persons.deleteById(uid);
+ }
+
+}
diff --git a/servlet/spring-boot/java/hello/src/main/java/example/IndexController.java b/servlet/spring-boot/java/security/src/main/java/example/PersonRepository.java
similarity index 65%
rename from servlet/spring-boot/java/hello/src/main/java/example/IndexController.java
rename to servlet/spring-boot/java/security/src/main/java/example/PersonRepository.java
index 2114050..9d25dad 100644
--- a/servlet/spring-boot/java/hello/src/main/java/example/IndexController.java
+++ b/servlet/spring-boot/java/security/src/main/java/example/PersonRepository.java
@@ -16,20 +16,15 @@
package example;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
+import javax.naming.Name;
-/**
- * Controller for "/".
- *
- * @author Joe Grandja
- */
-@Controller
-public class IndexController {
+import org.springframework.data.ldap.repository.LdapRepository;
+import org.springframework.ldap.NameNotFoundException;
- @GetMapping("/")
- public String index() {
- return "index";
+public interface PersonRepository extends LdapRepository {
+
+ default Person findByDn(Name dn) {
+ return findById(dn).orElseThrow(() -> new NameNotFoundException("user not found"));
}
}
diff --git a/servlet/spring-boot/java/hello/src/main/java/example/HelloApplication.java b/servlet/spring-boot/java/security/src/main/java/example/SecurityApplication.java
similarity index 90%
rename from servlet/spring-boot/java/hello/src/main/java/example/HelloApplication.java
rename to servlet/spring-boot/java/security/src/main/java/example/SecurityApplication.java
index 6bd754a..69ed34c 100644
--- a/servlet/spring-boot/java/hello/src/main/java/example/HelloApplication.java
+++ b/servlet/spring-boot/java/security/src/main/java/example/SecurityApplication.java
@@ -25,10 +25,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @author Joe Grandja
*/
@SpringBootApplication
-public class HelloApplication {
+public class SecurityApplication {
public static void main(String[] args) {
- SpringApplication.run(HelloApplication.class, args);
+ SpringApplication.run(SecurityApplication.class, args);
}
}
diff --git a/servlet/spring-boot/java/security/src/main/java/example/SecurityConfig.java b/servlet/spring-boot/java/security/src/main/java/example/SecurityConfig.java
new file mode 100644
index 0000000..577c018
--- /dev/null
+++ b/servlet/spring-boot/java/security/src/main/java/example/SecurityConfig.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package example;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Role;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.core.LdapClient;
+import org.springframework.ldap.core.support.BaseLdapPathContextSource;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
+import org.springframework.security.ldap.authentication.BindAuthenticator;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.authentication.LdapAuthenticator;
+import org.springframework.security.ldap.server.UnboundIdContainer;
+import org.springframework.security.web.SecurityFilterChain;
+
+import static org.springframework.security.config.Customizer.withDefaults;
+
+@Configuration(proxyBeanMethods = false)
+@EnableMethodSecurity
+public class SecurityConfig {
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ static RoleHierarchy roles() {
+ return RoleHierarchyImpl.withDefaultRolePrefix().role("ADMIN").implies("USER").build();
+ }
+
+ @Bean
+ SecurityFilterChain http(HttpSecurity http) throws Exception {
+ http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
+ .httpBasic(withDefaults())
+ .csrf((csrf) -> csrf.disable());
+ return http.build();
+ }
+
+ @Bean
+ UnboundIdContainer ldapContainer() {
+ UnboundIdContainer container = new UnboundIdContainer("dc=springframework,dc=org", "classpath:users.ldif");
+ container.setPort(0);
+ return container;
+ }
+
+ @Bean
+ ContextSource contextSource(UnboundIdContainer container) {
+ int port = container.getPort();
+ return new DefaultSpringSecurityContextSource("ldap://localhost:" + port + "/dc=springframework,dc=org");
+ }
+
+ @Bean
+ BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
+ BindAuthenticator authenticator = new BindAuthenticator(contextSource);
+ authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" });
+ return authenticator;
+ }
+
+ @Bean
+ LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator, LdapClient ldap) {
+ LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator);
+ provider.setUserDetailsContextMapper(new PersonContextMapper(ldap));
+ return provider;
+ }
+
+ @Bean
+ LdapClient ldapClient(ContextSource contextSource) {
+ return LdapClient.create(contextSource);
+ }
+
+}
diff --git a/servlet/spring-boot/java/security/src/main/java/example/WebConfig.java b/servlet/spring-boot/java/security/src/main/java/example/WebConfig.java
new file mode 100644
index 0000000..2a2474f
--- /dev/null
+++ b/servlet/spring-boot/java/security/src/main/java/example/WebConfig.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package example;
+
+import javax.naming.Name;
+import javax.naming.directory.Attributes;
+
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import example.ldap.AttributesDeserializer;
+import example.ldap.AttributesSerializer;
+import example.ldap.NameDeserializer;
+import example.ldap.NameSerializer;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+public class WebConfig {
+
+ @Bean
+ public SimpleModule nameModule() {
+ SimpleModule module = new SimpleModule();
+ module.addSerializer(Name.class, new NameSerializer());
+ module.addDeserializer(Name.class, new NameDeserializer());
+ module.addSerializer(Attributes.class, new AttributesSerializer());
+ module.addDeserializer(Attributes.class, new AttributesDeserializer());
+ return module;
+ }
+
+}
diff --git a/servlet/spring-boot/java/security/src/main/java/example/ldap/AttributesDeserializer.java b/servlet/spring-boot/java/security/src/main/java/example/ldap/AttributesDeserializer.java
new file mode 100644
index 0000000..d1e49e0
--- /dev/null
+++ b/servlet/spring-boot/java/security/src/main/java/example/ldap/AttributesDeserializer.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package example.ldap;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.naming.directory.Attributes;
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+import org.springframework.ldap.core.NameAwareAttributes;
+
+public class AttributesDeserializer extends JsonDeserializer {
+
+ @Override
+ public Attributes deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
+ NameAwareAttributes attributes = new NameAwareAttributes();
+ if (p.currentToken() == JsonToken.START_OBJECT) {
+ p.nextToken();
+ }
+ Map map = p.getCodec().readValue(p, new TypeReference<>() {
+ });
+ for (Map.Entry entry : map.entrySet()) {
+ attributes.put(entry.getKey(), entry.getValue());
+ }
+ return attributes;
+ }
+
+}
diff --git a/servlet/spring-boot/java/security/src/main/java/example/ldap/AttributesSerializer.java b/servlet/spring-boot/java/security/src/main/java/example/ldap/AttributesSerializer.java
new file mode 100644
index 0000000..c56f052
--- /dev/null
+++ b/servlet/spring-boot/java/security/src/main/java/example/ldap/AttributesSerializer.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package example.ldap;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.naming.directory.Attributes;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+import org.springframework.ldap.core.NameAwareAttribute;
+import org.springframework.ldap.core.NameAwareAttributes;
+import org.springframework.util.CollectionUtils;
+
+public class AttributesSerializer extends JsonSerializer {
+
+ @Override
+ public void serialize(Attributes value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ if (!(value instanceof NameAwareAttributes attributes)) {
+ serializers.defaultSerializeValue(value, gen);
+ return;
+ }
+ Iterator iterator = CollectionUtils.toIterator(attributes.getAll());
+ while (iterator.hasNext()) {
+ NameAwareAttribute attribute = iterator.next();
+ if (attribute.size() == 0) {
+ serializers.defaultSerializeField(attribute.getID(), null, gen);
+ }
+ else if (attribute.size() == 1) {
+ serializers.defaultSerializeField(attribute.getID(), attribute.get(), gen);
+ }
+ else {
+ List