Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
import java.rmi.Naming;
import java.util.*;


Expand Down Expand Up @@ -105,7 +106,7 @@ protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken toke
// Binds using the username and password provided by the user.
LdapContext ctx = null;
try {
ctx = ldapContextFactory.getLdapContext(upToken.getUsername(), String.valueOf(upToken.getPassword()));
ctx = ldapContextFactory.getLdapContext(getUserDn(upToken.getUsername(), ldapContextFactory), String.valueOf(upToken.getPassword()));
} finally {
LdapUtils.closeContext(ctx);
}
Expand Down Expand Up @@ -159,17 +160,7 @@ protected Set<String> getRoleNamesForUser(String username, LdapContext ldapConte
Set<String> roleNames;
roleNames = new LinkedHashSet<String>();

SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

String userPrincipalName = username;
if (principalSuffix != null) {
userPrincipalName += principalSuffix;
}

Object[] searchArguments = new Object[]{userPrincipalName};

NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
NamingEnumeration answer = lookupUsername(username, authorizationSearchFilter, ldapContext);

while (answer.hasMoreElements()) {
SearchResult sr = (SearchResult) answer.next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;

/**
* <p>A {@link org.apache.shiro.realm.Realm} that authenticates with an LDAP
Expand Down Expand Up @@ -73,11 +77,15 @@ public abstract class AbstractLdapRealm extends AuthorizingRealm {

protected String systemPassword = null;

private String authenticationSearchFilter = null;

//SHIRO-115 - prevent potential code injection:
protected String searchFilter = "(&(objectClass=*)(userPrincipalName={0}))";
protected String authorizationSearchFilter = "(&(objectClass=*)(userPrincipalName={0}))";

private LdapContextFactory ldapContextFactory = null;

private UserDnTemplate userDnTemplate = null;

/*--------------------------------------------
| C O N S T R U C T O R S |
============================================*/
Expand All @@ -86,6 +94,13 @@ public abstract class AbstractLdapRealm extends AuthorizingRealm {
| A C C E S S O R S / M O D I F I E R S |
============================================*/

/**
* @see DefaultLdapRealm#setUserDnTemplate(String)
*/
public void setUserDnTemplate(String template) throws IllegalArgumentException {
userDnTemplate = UserDnTemplate.fromString(template);
}

/*--------------------------------------------
| M E T H O D S |
============================================*/
Expand Down Expand Up @@ -161,8 +176,21 @@ public void setLdapContextFactory(LdapContextFactory ldapContextFactory) {
}


/**
* Configures search filter for user lookup during authorization.
* In order to keep compatibility with previous versions of Shiro, this
* is separate from {@link #setUserSearchFilter(String)}.
*/
public void setSearchFilter(String searchFilter) {
this.searchFilter = searchFilter;
this.authorizationSearchFilter = searchFilter;
}

/**
* Configures search filter for user lookup.
*/
public void setUserSearchFilter(String userSearchFilter) {
this.authenticationSearchFilter = userSearchFilter;
this.authorizationSearchFilter = userSearchFilter;
}

/*--------------------------------------------
Expand Down Expand Up @@ -247,4 +275,61 @@ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal
*/
protected abstract AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principal, LdapContextFactory ldapContextFactory) throws NamingException;

/**
* Returns all users accounts with a given name.
*
* @param username account identity submitted during authentication.
* @param searchFilter either {@link #authenticationSearchFilter} or {@link #authorizationSearchFilter}
* @param ldapContext LDAP connection.
* @return user accounts with a given name.
* @throws NamingException if a naming exception is encountered.
*/
protected NamingEnumeration lookupUsername(String username, String searchFilter, LdapContext ldapContext) throws NamingException {
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

String userPrincipalName = username;
if (principalSuffix != null) {
userPrincipalName += principalSuffix;
}

Object[] searchArguments = new Object[]{userPrincipalName};

return ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
}

/**
* Resolves user DN.
*
* First resolution step shares the logic with {@link DefaultLdapRealm}: if
* {@link #userDnTemplate} is configured, then it is used to construct user
* DN by inserting principal name at placeholder position.
*
* If {@link #userDnTemplate} is not configured, then LDAP lookup is
* performed using {@link #authenticationSearchFilter}. An exception is
* thrown in case lookup fails.
*
* Finally, if neither {@link #userDnTemplate} nor
* {@link #authenticationSearchFilter} are configured, principal name is
* assumed to be a DN and is returned as is.
*
* @param principal account identity submitted during authentication.
* @param ldapContextFactory factory used to retrieve LDAP connections.
* @return a user's DN.
* @throws NamingException if user's DN could not be resolved or if a naming exception is encountered.
*/
protected String getUserDn(String principal, LdapContextFactory ldapContextFactory) throws NamingException {
if (userDnTemplate != null) {
return userDnTemplate.getUserDn(principal);
}
if (authenticationSearchFilter != null) {
NamingEnumeration answer = lookupUsername(principal, authenticationSearchFilter, ldapContextFactory.getSystemLdapContext());
if (answer.hasMoreElements()) {
return ((SearchResult) answer.next()).getNameInNamespace();
} else {
throw new NamingException("Unknown principal: " + principal);
}
}
return principal;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,7 @@ public class DefaultLdapRealm extends AuthorizingRealm {

private static final Logger log = LoggerFactory.getLogger(DefaultLdapRealm.class);

//The zero index currently means nothing, but could be utilized in the future for other substitution techniques.
private static final String USERDN_SUBSTITUTION_TOKEN = "{0}";

private String userDnPrefix;
private String userDnSuffix;
private UserDnTemplate userDnTemplate = UserDnTemplate.EMPTY;

/*--------------------------------------------
| I N S T A N C E V A R I A B L E S |
Expand Down Expand Up @@ -123,25 +119,25 @@ public DefaultLdapRealm() {
/**
* Returns the User DN prefix to use when building a runtime User DN value or {@code null} if no
* {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that
* occurs before the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
* occurs before the {@link UserDnTemplate#SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
*
* @return the the User DN prefix to use when building a runtime User DN value or {@code null} if no
* {@link #getUserDnTemplate() userDnTemplate} has been configured.
*/
protected String getUserDnPrefix() {
return userDnPrefix;
return userDnTemplate.getPrefix();
}

/**
* Returns the User DN suffix to use when building a runtime User DN value. or {@code null} if no
* {@link #getUserDnTemplate() userDnTemplate} has been configured. If configured, this value is the text that
* occurs after the {@link #USERDN_SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
* occurs after the {@link UserDnTemplate#SUBSTITUTION_TOKEN} in the {@link #getUserDnTemplate() userDnTemplate} value.
*
* @return the User DN suffix to use when building a runtime User DN value or {@code null} if no
* {@link #getUserDnTemplate() userDnTemplate} has been configured.
*/
protected String getUserDnSuffix() {
return userDnSuffix;
return userDnTemplate.getSuffix();
}

/*--------------------------------------------
Expand Down Expand Up @@ -179,24 +175,7 @@ protected String getUserDnSuffix() {
* @see LdapContextFactory#getLdapContext(Object,Object)
*/
public void setUserDnTemplate(String template) throws IllegalArgumentException {
if (!StringUtils.hasText(template)) {
String msg = "User DN template cannot be null or empty.";
throw new IllegalArgumentException(msg);
}
int index = template.indexOf(USERDN_SUBSTITUTION_TOKEN);
if (index < 0) {
String msg = "User DN template must contain the '" +
USERDN_SUBSTITUTION_TOKEN + "' replacement token to understand where to " +
"insert the runtime authentication principal.";
throw new IllegalArgumentException(msg);
}
String prefix = template.substring(0, index);
String suffix = template.substring(prefix.length() + USERDN_SUBSTITUTION_TOKEN.length());
if (log.isDebugEnabled()) {
log.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix);
}
this.userDnPrefix = prefix;
this.userDnSuffix = suffix;
userDnTemplate = UserDnTemplate.fromString(template);
}

/**
Expand All @@ -206,7 +185,7 @@ public void setUserDnTemplate(String template) throws IllegalArgumentException {
* @return the User Distinguished Name (DN) template to use when creating User DNs at runtime.
*/
public String getUserDnTemplate() {
return getUserDn(USERDN_SUBSTITUTION_TOKEN);
return userDnTemplate.toString();
}

/**
Expand All @@ -225,29 +204,7 @@ public String getUserDnTemplate() {
* @see LdapContextFactory#getLdapContext(Object, Object)
*/
protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException {
if (!StringUtils.hasText(principal)) {
throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction.");
}
String prefix = getUserDnPrefix();
String suffix = getUserDnSuffix();
if (prefix == null && suffix == null) {
log.debug("userDnTemplate property has not been configured, indicating the submitted " +
"AuthenticationToken's principal is the same as the User DN. Returning the method argument " +
"as is.");
return principal;
}

int prefixLength = prefix != null ? prefix.length() : 0;
int suffixLength = suffix != null ? suffix.length() : 0;
StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
if (prefixLength > 0) {
sb.append(prefix);
}
sb.append(principal);
if (suffixLength > 0) {
sb.append(suffix);
}
return sb.toString();
return userDnTemplate.getUserDn(principal);
}

/**
Expand Down
79 changes: 79 additions & 0 deletions core/src/main/java/org/apache/shiro/realm/ldap/UserDnTemplate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.apache.shiro.realm.ldap;

import org.apache.shiro.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class UserDnTemplate {
private static final Logger log = LoggerFactory.getLogger(UserDnTemplate.class);

//The zero index currently means nothing, but could be utilized in the future for other substitution techniques.
static final String SUBSTITUTION_TOKEN = "{0}";

private final String prefix;
private final String suffix;

static UserDnTemplate fromString(final String template) throws IllegalArgumentException {
if (!StringUtils.hasText(template)) {
String msg = "User DN template cannot be null or empty.";
throw new IllegalArgumentException(msg);
}
final int index = template.indexOf(SUBSTITUTION_TOKEN);
if (index < 0) {
String msg = "User DN template must contain the '" +
SUBSTITUTION_TOKEN + "' replacement token to understand where to " +
"insert the runtime authentication principal.";
throw new IllegalArgumentException(msg);
}
final String prefix = template.substring(0, index);
final String suffix = template.substring(prefix.length() + SUBSTITUTION_TOKEN.length());
if (log.isDebugEnabled()) {
log.debug("Determined user DN prefix [{}] and suffix [{}]", prefix, suffix);
}
return new UserDnTemplate(prefix, suffix);
}

static final UserDnTemplate EMPTY = new UserDnTemplate(null, null);

private UserDnTemplate(final String prefix, final String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}

String getPrefix() {
return prefix;
}

String getSuffix() {
return suffix;
}

String getUserDn(final String principal) throws IllegalArgumentException, IllegalStateException {
if (!StringUtils.hasText(principal)) {
throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction.");
}
if (prefix == null && suffix == null) {
log.debug("userDnTemplate property has not been configured, indicating the submitted " +
"AuthenticationToken's principal is the same as the User DN. Returning the method argument " +
"as is.");
return principal;
}

final int prefixLength = prefix != null ? prefix.length() : 0;
final int suffixLength = suffix != null ? suffix.length() : 0;
final StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
if (prefixLength > 0) {
sb.append(prefix);
}
sb.append(principal);
if (suffixLength > 0) {
sb.append(suffix);
}
return sb.toString();
}

@Override
public String toString() {
return getUserDn(SUBSTITUTION_TOKEN);
}
}