diff --git a/samples/boot/build.gradle b/samples/boot/build.gradle new file mode 100644 index 00000000..b16fa96b --- /dev/null +++ b/samples/boot/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'org.springframework.boot' version '2.6.3' + id 'io.spring.dependency-management' + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-ldap' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.security:spring-security-ldap' + implementation 'com.unboundid:unboundid-ldapsdk' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/samples/boot/readme.md b/samples/boot/readme.md new file mode 100644 index 00000000..e2cc9ab2 --- /dev/null +++ b/samples/boot/readme.md @@ -0,0 +1,31 @@ +A Hello World 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 `bob`/`bobspassword`: + +```bash +curl --user bob:bobspassword localhost:8080 +``` + +And you should see the response: + +```bash +Hello, bob +``` + +Also, you can hit the `cn` endpoint which uses `LdapTemplate` to query the datastore for the user's `cn` attribute value, like so: + +```bash +curl --user bob:bobspassword localhost:8080/cn +``` + +This should result in: + +```bash +[ + "Bob Hamilton" +] +``` + +To run the example, do `./gradlew :bootRun`. diff --git a/samples/boot/src/main/java/sample/HelloController.java b/samples/boot/src/main/java/sample/HelloController.java new file mode 100644 index 00000000..2d4d8ff0 --- /dev/null +++ b/samples/boot/src/main/java/sample/HelloController.java @@ -0,0 +1,27 @@ +package sample; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + @Autowired + LdapTemplate ldap; + + @GetMapping + public String hello(Authentication authentication) { + return "Hello, " + authentication.getName(); + } + + @GetMapping("/cn") + public List cn(Authentication authentication) { + AttributesMapper mapper = (attrs) -> attrs.get("cn").get().toString(); + return this.ldap.search("ou=people", "uid=" + authentication.getName(), mapper); + } +} diff --git a/samples/boot/src/main/java/sample/SecurityConfig.java b/samples/boot/src/main/java/sample/SecurityConfig.java new file mode 100644 index 00000000..b82a3f02 --- /dev/null +++ b/samples/boot/src/main/java/sample/SecurityConfig.java @@ -0,0 +1,42 @@ +package sample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ldap.core.ContextSource; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +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.ldap.userdetails.PersonContextMapper; + +@Configuration +public class SecurityConfig { + @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) { + LdapAuthenticationProvider provider = new LdapAuthenticationProvider(authenticator); + provider.setUserDetailsContextMapper(new PersonContextMapper()); + return provider; + } +} diff --git a/samples/boot/src/main/java/sample/SpringLdapSimpleSampleApplication.java b/samples/boot/src/main/java/sample/SpringLdapSimpleSampleApplication.java new file mode 100644 index 00000000..88cc9f71 --- /dev/null +++ b/samples/boot/src/main/java/sample/SpringLdapSimpleSampleApplication.java @@ -0,0 +1,13 @@ +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringLdapSimpleSampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringLdapSimpleSampleApplication.class, args); + } + +} diff --git a/samples/boot/src/main/resources/users.ldif b/samples/boot/src/main/resources/users.ldif new file mode 100644 index 00000000..289f3357 --- /dev/null +++ b/samples/boot/src/main/resources/users.ldif @@ -0,0 +1,124 @@ +dn: ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: groups + +dn: ou=subgroups,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: subgroups + +dn: ou=people,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: people + +dn: ou=space cadets,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: space cadets + +dn: ou=\"quoted people\",dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: "quoted people" + +dn: ou=otherpeople,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: otherpeople + +dn: uid=ben,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Ben Alex +sn: Alex +uid: ben +userPassword: $2a$10$c6bSeWPhg06xB1lvmaWNNe4NROmZiSpYhlocU/98HNr2MhIOiSt36 + +dn: uid=bob,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Bob Hamilton +sn: Hamilton +uid: bob +userPassword: bobspassword + +dn: uid=joe,ou=otherpeople,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Joe Smeth +sn: Smeth +uid: joe +userPassword: joespassword + +dn: cn=mouse\, jerry,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Mouse, Jerry +sn: Mouse +uid: jerry +userPassword: jerryspassword + +dn: cn=slash/guy,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: slash/guy +sn: Slash +uid: slashguy +userPassword: slashguyspassword + +dn: cn=quote\"guy,ou=\"quoted people\",dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: quote\"guy +sn: Quote +uid: quoteguy +userPassword: quoteguyspassword + +dn: uid=space cadet,ou=space cadets,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Space Cadet +sn: Cadet +uid: space cadet +userPassword: spacecadetspassword + + + +dn: cn=developers,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfUniqueNames +cn: developers +ou: developer +uniqueMember: uid=ben,ou=people,dc=springframework,dc=org +uniqueMember: uid=bob,ou=people,dc=springframework,dc=org + +dn: cn=managers,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfUniqueNames +cn: managers +ou: manager +uniqueMember: uid=ben,ou=people,dc=springframework,dc=org +uniqueMember: cn=mouse\, jerry,ou=people,dc=springframework,dc=org + +dn: cn=submanagers,ou=subgroups,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfUniqueNames +cn: submanagers +ou: submanager +uniqueMember: uid=ben,ou=people,dc=springframework,dc=org \ No newline at end of file diff --git a/samples/boot/src/test/java/sample/SpringLdapSimpleSampleApplicationTests.java b/samples/boot/src/test/java/sample/SpringLdapSimpleSampleApplicationTests.java new file mode 100644 index 00000000..d084724f --- /dev/null +++ b/samples/boot/src/test/java/sample/SpringLdapSimpleSampleApplicationTests.java @@ -0,0 +1,40 @@ +package sample; + +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.http.HttpHeaders; +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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class SpringLdapSimpleSampleApplicationTests { + + @Autowired + MockMvc mvc; + + @Test + void indexWhenCorrectUsernameAndPasswordThenAuthenticates() throws Exception { + HttpHeaders http = new HttpHeaders(); + http.setBasicAuth("bob", "bobspassword"); + this.mvc.perform(get("/").headers(http)) + .andExpect(status().isOk()) + .andExpect(content().string("Hello, bob")); + } + + @Test + void cnWhenCorrectUsernameAndPasswordThenShowsCommonName() throws Exception { + HttpHeaders http = new HttpHeaders(); + http.setBasicAuth("bob", "bobspassword"); + this.mvc.perform(get("/cn").headers(http)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[0]").value("Bob Hamilton")); + } +} diff --git a/settings.gradle b/settings.gradle index 414d51e9..6a91d647 100644 --- a/settings.gradle +++ b/settings.gradle @@ -37,6 +37,7 @@ include 'test/integration-tests-sunone' include 'test/integration-tests-ad' include 'samples/plain' include 'samples/odm' +include 'samples/boot' rootProject.children.each { p->