diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index d237b8b2d82..9b50e7eea06 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -15,6 +15,7 @@ package org.cloudfoundry.identity.uaa.authentication.manager; +import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; @@ -30,7 +31,27 @@ public class LdapLoginAuthenticationManager extends ExternalLoginAuthenticationM protected UaaUser getUser(UserDetails details, Map info) { UaaUser user = super.getUser(details, info); if (details instanceof LdapUserDetails) { - return user.modifySource(getOrigin(), ((LdapUserDetails)details).getDn()); + String mail = user.getEmail(); + String origin = getOrigin(); + String externalId = ((LdapUserDetails)details).getDn(); + if (details instanceof ExtendedLdapUserDetails) { + String[] addrs = ((ExtendedLdapUserDetails)details).getMail(); + if (addrs!=null && addrs.length>0) { + mail = addrs[0]; + } + } + return new UaaUser( + user.getId(), + user.getUsername(), + user.getPassword(), + mail, + user.getAuthorities(), + user.getGivenName(), + user.getFamilyName(), + user.getCreated(), + user.getModified(), + origin, + externalId); } else { logger.warn("Unable to get DN from user. Not an LDAP user:"+details+" of class:"+details.getClass()); return user.modifySource(getOrigin(), user.getExternalId()); diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java new file mode 100644 index 00000000000..1c5b85534b0 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserDetails.java @@ -0,0 +1,27 @@ +/* + * ****************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ****************************************************************************** + */ +package org.cloudfoundry.identity.uaa.ldap; + +import org.springframework.security.ldap.userdetails.LdapUserDetails; + +import java.util.Map; + +public interface ExtendedLdapUserDetails extends LdapUserDetails { + + public String[] getMail(); + + public Map getAttributes(); + +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java new file mode 100644 index 00000000000..45a98cd4b61 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/ExtendedLdapUserMapper.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + *******************************************************************************/ +package org.cloudfoundry.identity.uaa.ldap; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.ldap.extension.ExtendedLdapUserImpl; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.userdetails.LdapUserDetails; +import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.cloudfoundry.identity.uaa.ldap.extension.SpringSecurityLdapTemplate.DN_KEY; + +public class ExtendedLdapUserMapper extends LdapUserDetailsMapper { + private static final Log logger = LogFactory.getLog(ExtendedLdapUserMapper.class); + + private String mailAttributeName="mail"; + @Override + public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection authorities) { + LdapUserDetails ldapUserDetails = (LdapUserDetails)super.mapUserFromContext(ctx, username, authorities); + + DirContextAdapter adapter = (DirContextAdapter) ctx; + Map record = new HashMap(); + List attributeNames = Collections.list(adapter.getAttributes().getIDs()); + for (String attributeName : attributeNames) { + try { + String[] values = adapter.getStringAttributes(attributeName); + if (values == null || values.length == 0) { + logger.debug("No attribute value found for '" + attributeName + "'"); + } else { + record.put(attributeName, values); + } + } catch (ArrayStoreException x) { + logger.debug("Attribute value is not a string for '" + attributeName + "'"); + } + } + record.put(DN_KEY, new String[] {adapter.getDn().toString()}); + ExtendedLdapUserImpl result = new ExtendedLdapUserImpl(ldapUserDetails, record); + result.setMailAttributeName(getMailAttributeName()); + return result; + } + + public String getMailAttributeName() { + return mailAttributeName; + } + + public void setMailAttributeName(String mailAttributeName) { + this.mailAttributeName = mailAttributeName; + } +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java new file mode 100644 index 00000000000..04eb0a521b3 --- /dev/null +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/ldap/extension/ExtendedLdapUserImpl.java @@ -0,0 +1,160 @@ +/* + * ****************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ****************************************************************************** + */ +package org.cloudfoundry.identity.uaa.ldap.extension; + +import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.ldap.userdetails.LdapUserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class ExtendedLdapUserImpl implements ExtendedLdapUserDetails { + + private String mailAttributeName = "mail"; + private String dn; + private String password; + private String username; + private Collection authorities = AuthorityUtils.NO_AUTHORITIES; + private boolean accountNonExpired = true; + private boolean accountNonLocked = true; + private boolean credentialsNonExpired = true; + private boolean enabled = true; + // PPolicy data + private int timeBeforeExpiration = Integer.MAX_VALUE; + private int graceLoginsRemaining = Integer.MAX_VALUE; + private Map attributes = new HashMap<>(); + + public ExtendedLdapUserImpl() {} + public ExtendedLdapUserImpl(LdapUserDetails details) { + setDn(details.getDn()); + setUsername(details.getUsername()); + setPassword(details.getPassword()); + setEnabled(details.isEnabled()); + setAccountNonExpired(details.isAccountNonExpired()); + setCredentialsNonExpired(details.isCredentialsNonExpired()); + setAccountNonLocked(details.isAccountNonLocked()); + setAuthorities(details.getAuthorities()); + } + public ExtendedLdapUserImpl(LdapUserDetails details, Map attributes) { + this(details); + this.attributes.putAll(attributes); + } + + @Override + public String[] getMail() { + String[] mail = attributes.get(getMailAttributeName()); + if (mail==null) { + mail = new String[0]; + } + return mail; + } + + @Override + public Map getAttributes() { + return Collections.unmodifiableMap(attributes); + } + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Collection getAuthorities() { + return authorities; + } + + public void setAuthorities(Collection authorities) { + this.authorities = authorities; + } + + public boolean isAccountNonExpired() { + return accountNonExpired; + } + + public void setAccountNonExpired(boolean accountNonExpired) { + this.accountNonExpired = accountNonExpired; + } + + public boolean isAccountNonLocked() { + return accountNonLocked; + } + + public void setAccountNonLocked(boolean accountNonLocked) { + this.accountNonLocked = accountNonLocked; + } + + public boolean isCredentialsNonExpired() { + return credentialsNonExpired; + } + + public void setCredentialsNonExpired(boolean credentialsNonExpired) { + this.credentialsNonExpired = credentialsNonExpired; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getTimeBeforeExpiration() { + return timeBeforeExpiration; + } + + public void setTimeBeforeExpiration(int timeBeforeExpiration) { + this.timeBeforeExpiration = timeBeforeExpiration; + } + + public int getGraceLoginsRemaining() { + return graceLoginsRemaining; + } + + public void setGraceLoginsRemaining(int graceLoginsRemaining) { + this.graceLoginsRemaining = graceLoginsRemaining; + } + + public String getMailAttributeName() { + return mailAttributeName; + } + + public void setMailAttributeName(String mailAttributeName) { + this.mailAttributeName = mailAttributeName; + } +} diff --git a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java index 745095bff7c..7d551f87e6d 100644 --- a/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java +++ b/common/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java @@ -145,6 +145,10 @@ public Date getModified() { return modified; } + public Date getCreated() { + return created; + } + public UaaUser modifySource(String origin, String externalId) { return new UaaUser(id, username, password, email, authorities, givenName, familyName, created, modified, origin, externalId); } diff --git a/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java new file mode 100644 index 00000000000..d12afac7553 --- /dev/null +++ b/common/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -0,0 +1,163 @@ +/* + * ****************************************************************************** + * Cloud Foundry + * Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product includes a number of subcomponents with + * separate copyright notices and license terms. Your use of these + * subcomponents is subject to the terms and conditions of the + * subcomponent's license, as noted in the LICENSE file. + * ****************************************************************************** + */ +package org.cloudfoundry.identity.uaa.authentication.manager; + +import org.cloudfoundry.identity.uaa.ldap.ExtendedLdapUserDetails; +import org.cloudfoundry.identity.uaa.ldap.extension.ExtendedLdapUserImpl; +import org.cloudfoundry.identity.uaa.ldap.extension.SpringSecurityLdapTemplate; +import org.cloudfoundry.identity.uaa.user.UaaAuthority; +import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Matchers; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.ldap.userdetails.LdapUserDetails; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class LdapLoginAuthenticationManagerTests { + + public static final String DN = "cn=marissa,ou=Users,dc=test,dc=com"; + public static final String LDAP_EMAIL = "test@ldap.org"; + public static final String TEST_EMAIL = "email@email.org"; + public static final String EXTERNAL_ID = "externalId"; + public static final String USERNAME = "username"; + LdapLoginAuthenticationManager am; + ApplicationEventPublisher publisher; + String origin = "test"; + Map info; + UaaUser dbUser = getUaaUser(); + Authentication auth; + + @Before + public void setUp() { + am = new LdapLoginAuthenticationManager(); + publisher = mock(ApplicationEventPublisher.class); + am.setApplicationEventPublisher(publisher); + am.setOrigin(origin); + info = new HashMap<>(); + info.put("email",TEST_EMAIL); + UaaUserDatabase db = mock(UaaUserDatabase.class); + when(db.retrieveUserById(anyString())).thenReturn(dbUser); + am.setUserDatabase(db); + auth = mock(Authentication.class); + when(auth.getAuthorities()).thenReturn(null); + } + + @Test + public void testGetUserWithExtendedLdapInfo() throws Exception { + UaaUser user = am.getUser(getExtendedLdapUserDetails(), info); + assertEquals(DN, user.getExternalId()); + assertEquals(LDAP_EMAIL, user.getEmail()); + assertEquals(origin, user.getOrigin()); + } + + @Test + public void testGetUserWithLdapInfo() throws Exception { + UaaUser user = am.getUser(getLdapUserDetails(), info); + assertEquals(DN, user.getExternalId()); + assertEquals(TEST_EMAIL, user.getEmail()); + assertEquals(origin, user.getOrigin()); + } + + @Test + public void testGetUserWithNonLdapInfo() throws Exception { + UaaUser user = am.getUser(getUserDetails(), info); + assertEquals(USERNAME, user.getExternalId()); + assertEquals(TEST_EMAIL, user.getEmail()); + assertEquals(origin, user.getOrigin()); + } + + @Test + public void testUserAuthenticated() throws Exception { + UaaUser user = getUaaUser(); + am.setAutoAddAuthorities(true); + UaaUser result = am.userAuthenticated(auth, user); + assertSame(dbUser,result); + verify(publisher,times(1)).publishEvent(Matchers.anyObject()); + + am.setAutoAddAuthorities(false); + result = am.userAuthenticated(auth, user); + assertSame(user, result); + //count should still be 1 + verify(publisher,times(1)).publishEvent(Matchers.anyObject()); + } + + protected User getUserDetails() { + UaaUser uaaUser = getUaaUser(); + return new User( + uaaUser.getUsername(), + uaaUser.getPassword(), + true, + true, + true, + true, + uaaUser.getAuthorities() + ); + } + + protected LdapUserDetails getLdapUserDetails() { + UaaUser uaaUser = getUaaUser(); + LdapUserDetailsImpl.Essence essence = new LdapUserDetailsImpl.Essence(); + essence.setDn(DN); + essence.setUsername(uaaUser.getUsername()); + essence.setPassword(uaaUser.getPassword()); + essence.setEnabled(true); + essence.setAccountNonExpired(true); + essence.setCredentialsNonExpired(true); + essence.setAccountNonLocked(true); + essence.setAuthorities(uaaUser.getAuthorities()); + return essence.createUserDetails(); + } + + protected ExtendedLdapUserDetails getExtendedLdapUserDetails() { + Map attributes = new HashMap<>(); + LdapUserDetails details = getLdapUserDetails(); + attributes.put(SpringSecurityLdapTemplate.DN_KEY, new String[] {details.getDn()}); + attributes.put("mail", new String[] {LDAP_EMAIL}); + ExtendedLdapUserImpl result = new ExtendedLdapUserImpl(details,attributes); + return result; + } + + protected UaaUser getUaaUser() { + return new UaaUser( + "id", + USERNAME, + "password", + TEST_EMAIL, + UaaAuthority.USER_AUTHORITIES, + "givenname", + "familyname", + new Date(), + new Date(), + "origin", + EXTERNAL_ID); + } +} \ No newline at end of file diff --git a/uaa/src/main/resources/ldap_init.ldif b/uaa/src/main/resources/ldap_init.ldif index 46d8f764e97..088b6d4ab4b 100644 --- a/uaa/src/main/resources/ldap_init.ldif +++ b/uaa/src/main/resources/ldap_init.ldif @@ -86,6 +86,14 @@ uid: 20f459e0-e30b-4d1f-998c-3ded7f769db6 mail: marissa6@test.com sn: Marissa6 +dn: cn=marissa7,ou=Users,dc=test,dc=com +changetype: add +objectClass: person +objectClass: organizationalPerson +cn: marissa7 +userPassword: ldap7 +sn: Marissa7 + ############################################################################### # BEGIN GROUP TO SCOPE MAPPING ############################################################################### diff --git a/uaa/src/main/webapp/WEB-INF/spring/ldap-integration.xml b/uaa/src/main/webapp/WEB-INF/spring/ldap-integration.xml index 2546ba3ad77..872396aca82 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/ldap-integration.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/ldap-integration.xml @@ -34,6 +34,9 @@ + + + @@ -46,7 +49,6 @@ - diff --git a/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-search-and-bind.xml b/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-search-and-bind.xml index 53921b9c210..365652bf901 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-search-and-bind.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-search-and-bind.xml @@ -41,6 +41,7 @@ + diff --git a/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-search-and-compare.xml b/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-search-and-compare.xml index facc3faf0c7..2a081868964 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-search-and-compare.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-search-and-compare.xml @@ -48,6 +48,7 @@ + diff --git a/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-simple-bind.xml b/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-simple-bind.xml index bc723586cd6..3ee507c4b41 100644 --- a/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-simple-bind.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/ldap/ldap-simple-bind.xml @@ -41,6 +41,7 @@ + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java index 4f57ff528d6..a6c07773b3a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/LdapMockMvcTests.java @@ -258,7 +258,7 @@ public void validateOriginForNonLdapUser() throws Exception { } @Test - public void validateOriginForLdapUser() throws Exception { + public void validateOriginAndEmailForLdapUser() throws Exception { setUp(); String username = "marissa3"; String password = "ldap3"; @@ -277,6 +277,32 @@ public void validateOriginForLdapUser() throws Exception { String origin = jdbcTemplate.queryForObject("select origin from users where username='marissa3'", String.class); assertEquals("ldap", origin); + String email = jdbcTemplate.queryForObject("select email from users where username='marissa3' and origin='ldap'", String.class); + assertEquals("marissa3@test.com", email); + } + + @Test + public void validateEmailMissingForLdapUser() throws Exception { + setUp(); + String username = "marissa7"; + String password = "ldap7"; + + MockHttpServletRequestBuilder post = + post("/authenticate") + .accept(MediaType.APPLICATION_JSON) + .param("username", username) + .param("password", password); + + MvcResult result = mockMvc.perform(post) + .andExpect(status().isOk()) + .andReturn(); + + assertEquals("{\"username\":\"" + username + "\"}", result.getResponse().getContentAsString()); + + String origin = jdbcTemplate.queryForObject("select origin from users where username='marissa7'", String.class); + assertEquals("ldap", origin); + String email = jdbcTemplate.queryForObject("select email from users where username='marissa7' and origin='ldap'", String.class); + assertEquals("marissa7@user.from.ldap.cf", email); } @Test