Skip to content
Open
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
13 changes: 12 additions & 1 deletion docs/setup/security/shiro_authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,19 @@ ldapRealm.groupSearchScope = subtree;
ldapRealm.memberAttributeValueTemplate = cn={0},ou=people,dc=hadoop,dc=apache,dc=org
ldapRealm.contextFactory.systemUsername = uid=guest,ou=people,dc=hadoop,dc=apache,dc=org
ldapRealm.contextFactory.systemPassword = S{ALIAS=ldcSystemPassword}
# enable support for nested groups using the LDAP_MATCHING_RULE_IN_CHAIN operator
# enable support for nested groups using the LDAP_MATCHING_RULE_IN_CHAIN operator (Active Directory only)
ldapRealm.groupSearchEnableMatchingRuleInChain = true
# enable support for nested groups on directories that lack LDAP_MATCHING_RULE_IN_CHAIN
# (e.g. FreeIPA / 389 Directory Server) by reading the user entry's own memberOf attribute,
# which the MemberOf plugin pre-flattens to include direct and indirect group membership.
# If both this and groupSearchEnableMatchingRuleInChain are enabled, the matching-rule-in-chain
# path takes precedence and this setting is ignored.
# Note: the LDAP bind used by ldapRealm.contextFactory must be authenticated (not anonymous) or
# the directory may not return memberOf; if group members span multiple backends/replicas, the
# directory's own server-side scope configuration must be set up for memberOf to be complete.
ldapRealm.groupSearchEnableMemberOf = false
# customize the attribute name read by groupSearchEnableMemberOf (defaults to memberOf)
ldapRealm.memberOfAttribute = memberOf
# optional mapping from physical groups to logical application roles
ldapRealm.rolesByGroup = LDN_USERS: user_role, NYK_USERS: user_role, HKG_USERS: user_role, GLOBAL_ADMIN: admin_role
# optional list of roles that are allowed to authenticate. Incase not present all groups are allowed to authenticate (login).
Expand Down
233 changes: 184 additions & 49 deletions zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.regex.Pattern;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.InvalidNameException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
Expand All @@ -44,6 +45,7 @@
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.Rdn;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.alias.CredentialProvider;
import org.apache.hadoop.security.alias.CredentialProviderFactory;
Expand Down Expand Up @@ -165,6 +167,9 @@ public class LdapRealm extends DefaultLdapRealm {
private String userSearchScope = "subtree";
private String groupSearchScope = "subtree";
private boolean groupSearchEnableMatchingRuleInChain;
private boolean groupSearchEnableMemberOf;
private String memberOfAttribute = "memberOf";
private volatile boolean warnedBothGroupSearchModes;

private String groupSearchBase;

Expand Down Expand Up @@ -343,6 +348,14 @@ protected Set<String> rolesFor(PrincipalCollection principals, String userNameIn

String userDn = getUserDnForSearch(userName);

if (groupSearchEnableMatchingRuleInChain && groupSearchEnableMemberOf
&& !warnedBothGroupSearchModes) {
LOGGER.warn("Both groupSearchEnableMatchingRuleInChain and groupSearchEnableMemberOf are "
+ "enabled; groupSearchEnableMatchingRuleInChain takes precedence and "
+ "groupSearchEnableMemberOf is ignored.");
warnedBothGroupSearchModes = true;
}

// Activate paged results
int pageSize = getPagingSize();
LOGGER.debug("Ldap PagingSize: {}", pageSize);
Expand All @@ -356,62 +369,18 @@ protected Set<String> rolesFor(PrincipalCollection principals, String userNameIn
// ldapsearch -h localhost -p 33389 -D
// uid=guest,ou=people,dc=hadoop,dc=apache,dc=org -w guest-password
// -b dc=hadoop,dc=apache,dc=org -s sub '(objectclass=*)'
NamingEnumeration<SearchResult> searchResultEnum = null;
SearchControls searchControls = getGroupSearchControls();
try {
if (groupSearchEnableMatchingRuleInChain) {
searchResultEnum = ldapCtx.search(
getGroupSearchBase(),
String.format(
MATCHING_RULE_IN_CHAIN_FORMAT,
LdapFilterEncoder.escapeFilterValue(groupObjectClass),
LdapFilterEncoder.escapeFilterValue(memberAttribute),
LdapFilterEncoder.escapeFilterValue(userDn)),
searchControls);
while (searchResultEnum != null && searchResultEnum.hasMore()) {
// searchResults contains all the groups in search scope
numResults++;
final SearchResult group = searchResultEnum.next();

Attribute attribute = group.getAttributes().get(getGroupIdAttribute());
String groupName = attribute.get().toString();

String roleName = roleNameFor(groupName);
if (roleName != null) {
roleNames.add(roleName);
} else {
roleNames.add(groupName);
}
}
numResults += rolesForMatchingRuleInChain(userDn, ldapCtx, searchControls, roleNames);
} else if (groupSearchEnableMemberOf) {
numResults += rolesForMemberOf(userDn, ldapCtx, roleNames, groupNames);
} else {
// Default group search filter
String searchFilter = String.format("(objectclass=%1$s)",
LdapFilterEncoder.escapeFilterValue(groupObjectClass));

// If group search filter is defined in Shiro config, then use it
if (groupSearchFilter != null) {
searchFilter = expandFilterTemplate(groupSearchFilter, userName);
//searchFilter = String.format("%1$s", groupSearchFilter);
}
LOGGER.debug("Group SearchBase|SearchFilter|GroupSearchScope: " + "{}|{}|{}",
getGroupSearchBase(), searchFilter, groupSearchScope);
searchResultEnum = ldapCtx.search(
getGroupSearchBase(),
searchFilter,
searchControls);
while (searchResultEnum != null && searchResultEnum.hasMore()) {
// searchResults contains all the groups in search scope
numResults++;
final SearchResult group = searchResultEnum.next();
addRoleIfMember(userDn, group, roleNames, groupNames, ldapContextFactory);
}
numResults += rolesForGroupMembership(userName, userDn, ldapCtx, searchControls,
ldapContextFactory, roleNames, groupNames);
}
} catch (PartialResultException e) {
LOGGER.debug("Ignoring PartitalResultException");
} finally {
if (searchResultEnum != null) {
searchResultEnum.close();
}
}
// Re-activate paged results
ldapCtx.setRequestControls(new Control[]{new PagedResultsControl(pageSize,
Expand All @@ -432,6 +401,156 @@ protected Set<String> rolesFor(PrincipalCollection principals, String userNameIn
return roleNames;
}

// AD-only path: LDAP_MATCHING_RULE_IN_CHAIN walks group ancestry server-side.
private int rolesForMatchingRuleInChain(String userDn, LdapContext ldapCtx,
SearchControls searchControls, Set<String> roleNames) throws NamingException {
int numResults = 0;
NamingEnumeration<SearchResult> searchResultEnum = null;
try {
searchResultEnum = ldapCtx.search(
getGroupSearchBase(),
String.format(
MATCHING_RULE_IN_CHAIN_FORMAT,
LdapFilterEncoder.escapeFilterValue(groupObjectClass),
LdapFilterEncoder.escapeFilterValue(memberAttribute),
LdapFilterEncoder.escapeFilterValue(userDn)),
searchControls);
while (searchResultEnum != null && searchResultEnum.hasMore()) {
// searchResults contains all the groups in search scope
numResults++;
final SearchResult group = searchResultEnum.next();

Attribute attribute = group.getAttributes().get(getGroupIdAttribute());
String groupName = attribute.get().toString();

String roleName = roleNameFor(groupName);
if (roleName != null) {
roleNames.add(roleName);
} else {
roleNames.add(groupName);
}
}
} finally {
if (searchResultEnum != null) {
searchResultEnum.close();
}
}
return numResults;
}

// Default path: search groups and check the member attribute for the user DN.
private int rolesForGroupMembership(String userName, String userDn, LdapContext ldapCtx,
SearchControls searchControls, LdapContextFactory ldapContextFactory,
Set<String> roleNames, Set<String> groupNames) throws NamingException {
int numResults = 0;
NamingEnumeration<SearchResult> searchResultEnum = null;
try {
// Default group search filter
String searchFilter = String.format("(objectclass=%1$s)",
LdapFilterEncoder.escapeFilterValue(groupObjectClass));

// If group search filter is defined in Shiro config, then use it
if (groupSearchFilter != null) {
searchFilter = expandFilterTemplate(groupSearchFilter, userName);
//searchFilter = String.format("%1$s", groupSearchFilter);
}
LOGGER.debug("Group SearchBase|SearchFilter|GroupSearchScope: " + "{}|{}|{}",
getGroupSearchBase(), searchFilter, groupSearchScope);
searchResultEnum = ldapCtx.search(
getGroupSearchBase(),
searchFilter,
searchControls);
while (searchResultEnum != null && searchResultEnum.hasMore()) {
// searchResults contains all the groups in search scope
numResults++;
final SearchResult group = searchResultEnum.next();
addRoleIfMember(userDn, group, roleNames, groupNames, ldapContextFactory);
}
} finally {
if (searchResultEnum != null) {
searchResultEnum.close();
}
}
return numResults;
}

/**
* FreeIPA/389 DS path: reads the user entry's {@code memberOf} attribute,
* which the directory pre-flattens with nested (indirect) group membership.
*/
private int rolesForMemberOf(String userDn, LdapContext ldapCtx,
Set<String> roleNames, Set<String> groupNames) throws NamingException {
SearchControls memberOfControls = new SearchControls();
memberOfControls.setSearchScope(SearchControls.OBJECT_SCOPE);
memberOfControls.setReturningAttributes(new String[]{memberOfAttribute});

int numResults = 0;
NamingEnumeration<SearchResult> searchResultEnum = null;
try {
searchResultEnum = ldapCtx.search(userDn, "(objectclass=*)", memberOfControls);
if (searchResultEnum != null && searchResultEnum.hasMore()) {
numResults++;
final SearchResult userEntry = searchResultEnum.next();
Attribute memberOf = userEntry.getAttributes().get(memberOfAttribute);
if (memberOf != null) {
NamingEnumeration<?> memberOfValues = memberOf.getAll();
try {
while (memberOfValues.hasMore()) {
String groupDn = memberOfValues.next().toString();
String groupName = groupNameFromMemberOfDn(groupDn);
if (groupName != null) {
recordGroupRole(groupName, roleNames, groupNames);
}
}
} finally {
memberOfValues.close();
}
}
}
} finally {
if (searchResultEnum != null) {
searchResultEnum.close();
}
}
return numResults;
}

/**
* Extracts the group name from a memberOf DN value using its leaf RDN.
* Ancestor RDNs are not scanned: a container RDN on the path (e.g. FreeIPA's
* {@code cn=groups,cn=accounts}) shares the group's RDN type and would be
* mistaken for the group. Returns null for an unparseable DN so the caller skips it.
*/
String groupNameFromMemberOfDn(String groupDn) {
try {
LdapName groupLdapName = new LdapName(groupDn);
List<Rdn> rdns = groupLdapName.getRdns();
if (rdns.isEmpty()) {
return null;
}
Rdn leafRdn = rdns.get(rdns.size() - 1);
if (!getGroupIdAttribute().equalsIgnoreCase(leafRdn.getType())) {
LOGGER.warn("memberOf value '{}' leaf RDN type '{}' does not match groupIdAttribute "
+ "'{}'; using the leaf RDN value anyway.",
groupDn, leafRdn.getType(), getGroupIdAttribute());
}
return leafRdn.getValue().toString();
} catch (InvalidNameException e) {
LOGGER.warn("Skipping malformed memberOf value '{}': {}", groupDn, e.getMessage());
return null;
}
}

private void recordGroupRole(String groupName, Set<String> roleNames, Set<String> groupNames) {
groupNames.add(groupName);
String roleName = roleNameFor(groupName);
if (roleName != null) {
roleNames.add(roleName);
} else {
roleNames.add(groupName);
}
}

protected String getUserDnForSearch(String userName) {
if (userSearchAttributeName == null || userSearchAttributeName.isEmpty()) {
// memberAttributeValuePrefix and memberAttributeValueSuffix
Expand Down Expand Up @@ -821,6 +940,22 @@ public void setGroupSearchEnableMatchingRuleInChain(
this.groupSearchEnableMatchingRuleInChain = groupSearchEnableMatchingRuleInChain;
}

public boolean isGroupSearchEnableMemberOf() {
return groupSearchEnableMemberOf;
}

public void setGroupSearchEnableMemberOf(boolean groupSearchEnableMemberOf) {
this.groupSearchEnableMemberOf = groupSearchEnableMemberOf;
}

public String getMemberOfAttribute() {
return memberOfAttribute;
}

public void setMemberOfAttribute(String memberOfAttribute) {
this.memberOfAttribute = memberOfAttribute;
}

private SearchControls getUserSearchControls() {
SearchControls searchControls = SUBTREE_SCOPE;
if ("onelevel".equalsIgnoreCase(userSearchScope)) {
Expand Down
Loading
Loading