/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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 org.apache.isis.security.shiro;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.naming.AuthenticationException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.config.Ini;
import org.apache.shiro.realm.ldap.JndiLdapRealm;
import org.apache.shiro.realm.ldap.LdapContextFactory;
import org.apache.shiro.realm.ldap.LdapUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.StringUtils;
import org.apache.isis.security.shiro.permrolemapper.PermissionToRoleMapper;
import org.apache.isis.security.shiro.permrolemapper.PermissionToRoleMapperFromIni;
import org.apache.isis.security.shiro.permrolemapper.PermissionToRoleMapperFromString;
/**
* Implementation of {@link org.apache.shiro.realm.ldap.JndiLdapRealm} that also
* returns each user's groups.
* <p/>
* <p>
* Sample config for <tt>shiro.ini</tt>:
* <p/>
* <pre>
* contextFactory = org.apache.isis.security.shiro.IsisLdapContextFactory
* contextFactory.url = ldap://localhost:10389
* contextFactory.authenticationMechanism = CRAM-MD5
* contextFactory.systemAuthenticationMechanism = simple
* contextFactory.systemUsername = uid=admin,ou=system
* contextFactory.systemPassword = secret
*
* ldapRealm = org.apache.isis.security.shiro.IsisLdapRealm
* ldapRealm.contextFactory = $contextFactory
*
* ldapRealm.searchBase = ou=groups,o=mojo
* ldapRealm.groupObjectClass = groupOfUniqueNames
* ldapRealm.uniqueMemberAttribute = uniqueMember
* ldapRealm.uniqueMemberAttributeValueTemplate = uid={0}
*
* ldapRealm.searchUserBase = ou=users,o=mojo
* ldapRealm.userObjectClass=inetOrgPerson
* ldapRealm.groupExtractedAttribute=street,country
* ldapRealm.userExtractedAttribute=street,country
* ldapRealm.permissionByGroupAttribute=attribute:Folder.{street}:Read,attribute:Portfolio.{country}
* ldapRealm.permissionByUserAttribute=attribute:Folder.{street}:Read,attribute:Portfolio.{country}
*
* # 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,\
* DEMOS: self-install_role
*
* securityManager.realms = $ldapRealm
* </pre>
* <p/>
* <p>
* The permissions for each role can be specified using the
* {@link #setResourcePath(String)} to an 'ini' file with a [roles] section, eg:
* <p/>
* <pre>
* ldapRealm.resourcePath=classpath:webapp/myroles.ini
* </pre>
* <p/>
* <p>
* where <tt>myroles.ini</tt> is in <tt>src/main/resources/webapp</tt>, and takes the form:
* <p/>
* <pre>
* [roles]
* user_role = *:ToDoItemsJdo:*:*,\
* *:ToDoItem:*:*
* self-install_role = *:ToDoItemsFixturesService:install:*
* admin_role = *
* </pre>
* <p/>
* <p>
* This 'ini' file can then be referenced by other realms (if multiple realm are configured
* with the Shiro security manager).
* <p/>
* <p>
* Alternatively, permissions can be set directly using {@link #setPermissionsByRole(String)},
* where the string is the same information, formatted thus:
* <p/>
* <pre>
* ldapRealm.permissionsByRole=\
* user_role = *:ToDoItemsJdo:*:*,\
* *:ToDoItem:*:*; \
* self-install_role = *:ToDoItemsFixturesService:install:* ; \
* admin_role = *
* </pre>
* <p/>
* <p>
* Alternatively, permissions can be extracted from the base itself with the parameter searchUserBase,
* the attribute list as userExtractedAttribute and the permission url as permissionByUserAttribute.
* The idea is to extract attribute from the user or the group of the user and map directly to permission rule in
* replacing the string {attribute} by the extracted attribute (can me multiple).
* See the sample for group and user attribute and mapping.
* <p/>
* </p>
*/
public class IsisLdapRealm extends JndiLdapRealm {
private static final String UNIQUEMEMBER_SUBSTITUTION_TOKEN = "{0}";
private final static SearchControls SUBTREE_SCOPE = new SearchControls();
static {
SUBTREE_SCOPE.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
private String searchBase;
private String groupObjectClass;
private String uniqueMemberAttribute = "uniqueMember";
private String uniqueMemberAttributeValuePrefix;
private String uniqueMemberAttributeValueSuffix;
/**
* For Group Extracted attribute name with mapping name in parenthesis. Ex: street,country
*/
protected Set<String> groupExtractedAttribute = Sets.newConcurrentHashSet();
/**
* For User Extracted attribute name with mapping name in parenthesis. Ex: street,country
*/
protected Set<String> userExtractedAttribute = Sets.newConcurrentHashSet();
/**
* For Group Mapping of attributes. Ex:
* attribute:Folder.{street}:Read,attribute:Portfolio.{country}:*
*/
protected Set<String> permissionByGroupAttribute = Sets.newConcurrentHashSet();
/**
* For User Mapping of attributes. Ex:
* attribute:Folder.{street}:Read,attribute:Portfolio.{country}:*
*/
protected Set<String> permissionByUserAttribute = Sets.newConcurrentHashSet();
/**
* For search ldap on user
*/
private String searchUserBase = "";
/**
* The object className as person
*/
private String userObjectClass;
private final Map<String, String> rolesByGroup = Maps.newLinkedHashMap();
private PermissionToRoleMapper permissionToRoleMapper;
/**
* cn attribute
*/
private String cnAttribute = "cn";
public IsisLdapRealm() {
setGroupObjectClass("groupOfUniqueNames");
setUniqueMemberAttribute("uniqueMember");
setUniqueMemberAttributeValueTemplate("uid={0}");
}
/**
* Get groups from LDAP.
*
* @param principals the principals of the Subject whose AuthenticationInfo should
* be queried from the LDAP server.
* @param ldapContextFactory factory used to retrieve LDAP connections.
* @return an {@link AuthorizationInfo} instance containing information
* retrieved from the LDAP server.
* @throws NamingException if any LDAP errors occur during the search.
*/
@Override
protected AuthorizationInfo queryForAuthorizationInfo(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException {
final Set<String> roleNames = getRoles(principals, ldapContextFactory);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roleNames);
Set<String> stringPermissions = permsFor(roleNames);
final String username = (String) getAvailablePrincipal(principals);
final LdapContext finalLdapContext = ldapContextFactory.getSystemLdapContext();
stringPermissions.addAll(getPermissionForUser(username, finalLdapContext));
stringPermissions.addAll(getPermissionForRole(username, finalLdapContext));
simpleAuthorizationInfo.setStringPermissions(stringPermissions);
return simpleAuthorizationInfo;
}
private Set<String>
getPermissionForRole(String username, LdapContext ldapContext)
throws NamingException {
final Set<String> permissions = Sets.newLinkedHashSet();
Set<String> groups = groupFor(username, ldapContext);
final NamingEnumeration<SearchResult> searchResultEnum = ldapContext.search(searchBase,
"objectClass=" + groupObjectClass, SUBTREE_SCOPE);
while (searchResultEnum.hasMore()) {
final SearchResult group = searchResultEnum.next();
if (memberOf(group, groups)) {
addPermIfFound(group, permissions, groupExtractedAttribute, permissionByGroupAttribute);
}
}
return permissions;
}
protected Set<String> groupFor(final String userName, final LdapContext ldapCtx)
throws NamingException {
final Set<String> roleNames = Sets.newLinkedHashSet();
final NamingEnumeration<SearchResult> searchResultEnum = ldapCtx.search(searchBase,
"objectClass=" + groupObjectClass, SUBTREE_SCOPE);
while (searchResultEnum.hasMore()) {
final SearchResult group = searchResultEnum.next();
addRoleIfMember(userName, group, roleNames);
}
return roleNames;
}
protected boolean memberOf(SearchResult group, Set<String> groups) throws NamingException {
Attribute attribute = group.getAttributes().get(cnAttribute);
String groupName = attribute.get().toString();
return groups.contains(groupName);
}
private Collection<String> getPermissionForUser(
String username,
LdapContext ldapContextFactory) throws NamingException {
try {
return permUser(username, ldapContextFactory);
} catch (org.apache.shiro.authc.AuthenticationException ex) {
return Collections.emptySet();
}
}
private Collection<String> permUser(String username, LdapContext systemLdapCtx)
throws NamingException {
final Set<String> permissions = Sets.newLinkedHashSet();
final NamingEnumeration<SearchResult> searchResultEnum = systemLdapCtx.search(
searchUserBase, "objectClass=" + userObjectClass, SUBTREE_SCOPE);
while (searchResultEnum.hasMore()) {
final SearchResult group = searchResultEnum.next();
addPermIfFound(group, permissions, userExtractedAttribute, permissionByUserAttribute);
}
return permissions;
}
private void addPermIfFound(
SearchResult group, Set<String> permissions,
Set<String> extractedAttributeP, Set<String> permissionByAttributeP)
throws NamingException {
final NamingEnumeration<? extends Attribute> attributeEnum = group.getAttributes().getAll();
Map<String, Set<String>> keyValues = Maps.newHashMap();
while (attributeEnum.hasMore()) {
final Attribute attr = attributeEnum.next();
if (extractedAttributeP.contains(attr.getID())) {
final NamingEnumeration<?> e = attr.getAll();
keyValues.put(attr.getID(), new HashSet<String>());
while (e.hasMore()) {
String attrValue = e.next().toString();
keyValues.get(attr.getID()).add(attrValue);
}
}
}
for (String permTempl : permissionByAttributeP) {
for (String key : keyValues.keySet()) {
if (permTempl.contains("{" + key + "}")) {
for (String value : keyValues.get(key)) {
permissions.add(permTempl.replaceAll("\\{" + key + "\\}", value));
}
}
}
}
}
private Set<String> getRoles(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException {
final String username = (String) getAvailablePrincipal(principals);
LdapContext systemLdapCtx = null;
try {
systemLdapCtx = ldapContextFactory.getSystemLdapContext();
return rolesFor(username, systemLdapCtx);
} catch (AuthenticationException ex) {
// principal was not authenticated on LDAP
return Collections.emptySet();
} finally {
LdapUtils.closeContext(systemLdapCtx);
}
}
private Set<String> rolesFor(final String userName, final LdapContext ldapCtx) throws NamingException {
final Set<String> roleNames = Sets.newLinkedHashSet();
final NamingEnumeration<SearchResult> searchResultEnum = ldapCtx.search(searchBase, "objectClass=" + groupObjectClass, SUBTREE_SCOPE);
while (searchResultEnum.hasMore()) {
final SearchResult group = searchResultEnum.next();
addRoleIfMember(userName, group, roleNames);
}
return roleNames;
}
private void addRoleIfMember(final String userName, final SearchResult group, final Set<String> roleNames) throws NamingException {
final NamingEnumeration<? extends Attribute> attributeEnum = group.getAttributes().getAll();
while (attributeEnum.hasMore()) {
final Attribute attr = attributeEnum.next();
if (!uniqueMemberAttribute.equalsIgnoreCase(attr.getID())) {
continue;
}
final NamingEnumeration<?> e = attr.getAll();
while (e.hasMore()) {
String attrValue = e.next().toString();
if ((uniqueMemberAttributeValuePrefix + userName + uniqueMemberAttributeValueSuffix).equals(attrValue)) {
Attribute attribute = group.getAttributes().get("cn");
String groupName = attribute.get().toString();
String roleName = roleNameFor(groupName);
if (roleName != null) {
roleNames.add(roleName);
}
break;
}
}
}
}
private String roleNameFor(String groupName) {
return !rolesByGroup.isEmpty() ? rolesByGroup.get(groupName) : groupName;
}
private Set<String> permsFor(Set<String> roleNames) {
Set<String> perms = Sets.newLinkedHashSet(); // preserve order
for (String role : roleNames) {
List<String> permsForRole = getPermissionsByRole().get(role);
if (permsForRole != null) {
perms.addAll(permsForRole);
}
}
return perms;
}
public void setSearchBase(String searchBase) {
this.searchBase = searchBase;
}
public void setGroupObjectClass(String groupObjectClassAttribute) {
this.groupObjectClass = groupObjectClassAttribute;
}
public void setUniqueMemberAttribute(String uniqueMemberAttribute) {
this.uniqueMemberAttribute = uniqueMemberAttribute;
}
public void setUniqueMemberAttributeValueTemplate(String template) {
if (!StringUtils.hasText(template)) {
String msg = "User DN template cannot be null or empty.";
throw new IllegalArgumentException(msg);
}
int index = template.indexOf(UNIQUEMEMBER_SUBSTITUTION_TOKEN);
if (index < 0) {
String msg = "UniqueMember attribute value template must contain the '" +
UNIQUEMEMBER_SUBSTITUTION_TOKEN + "' replacement token to understand how to " +
"parse the group members.";
throw new IllegalArgumentException(msg);
}
String prefix = template.substring(0, index);
String suffix = template.substring(prefix.length() + UNIQUEMEMBER_SUBSTITUTION_TOKEN.length());
this.uniqueMemberAttributeValuePrefix = prefix;
this.uniqueMemberAttributeValueSuffix = suffix;
}
public void setRolesByGroup(Map<String, String> rolesByGroup) {
this.rolesByGroup.putAll(rolesByGroup);
}
/**
* Retrieves permissions by role set using either
* {@link #setPermissionsByRole(String)} or {@link #setResourcePath(String)}.
*/
private Map<String, List<String>> getPermissionsByRole() {
if (permissionToRoleMapper == null) {
throw new IllegalStateException("Permissions by role not yet set.");
}
return permissionToRoleMapper.getPermissionsByRole();
}
/**
* <pre>
* ldapRealm.resourcePath=classpath:webapp/myroles.ini
* </pre>
* <p/>
* <p/>
* where <tt>myroles.ini</tt> is in <tt>src/main/resources/webapp</tt>, and takes the form:
* <p/>
* <pre>
* [roles]
* user_role = *:ToDoItemsJdo:*:*,\
* *:ToDoItem:*:*
* self-install_role = *:ToDoItemsFixturesService:install:*
* admin_role = *
* </pre>
* <p/>
* <p/>
* This 'ini' file can then be referenced by other realms (if multiple realm are configured
* with the Shiro security manager).
*
* @see #setResourcePath(String)
*/
public void setResourcePath(String resourcePath) {
if (permissionToRoleMapper != null) {
throw new IllegalStateException("Permissions already set, " + permissionToRoleMapper.getClass().getName());
}
final Ini ini = Ini.fromResourcePath(resourcePath);
this.permissionToRoleMapper = new PermissionToRoleMapperFromIni(ini);
}
/**
* Specify permissions for each role using a formatted string.
* <p/>
* <pre>
* ldapRealm.permissionsByRole=\
* user_role = *:ToDoItemsJdo:*:*,\
* *:ToDoItem:*:*; \
* self-install_role = *:ToDoItemsFixturesService:install:* ; \
* admin_role = *
* </pre>
*
* @see #setResourcePath(String)
*/
@Deprecated
public void setPermissionsByRole(String permissionsByRoleStr) {
if (permissionToRoleMapper != null) {
throw new IllegalStateException("Permissions already set, " + permissionToRoleMapper.getClass().getName());
}
this.permissionToRoleMapper = new PermissionToRoleMapperFromString(permissionsByRoleStr);
}
public void setPermissionByUserAttribute(String permissionByUserAttr) {
String[] list = permissionByUserAttr.split(",");
this.permissionByUserAttribute.addAll(Lists.newArrayList(list));
}
public void setPermissionByGroupAttribute(String permissionByGroupAttribute) {
String[] list = permissionByGroupAttribute.split(",");
this.permissionByGroupAttribute.addAll(Lists.newArrayList(list));
}
public void setUserExtractedAttribute(String userExtractedAttribute) {
String[] list = userExtractedAttribute.split(",");
this.userExtractedAttribute.addAll(Lists.newArrayList(list));
}
public void setGroupExtractedAttribute(String groupExtractedAttribute) {
String[] list = groupExtractedAttribute.split(",");
this.groupExtractedAttribute.addAll(Lists.newArrayList(list));
}
public void setSearchUserBase(String searchUserBase) {
this.searchUserBase = searchUserBase;
}
public void setUserObjectClass(String userObjectClass) {
this.userObjectClass = userObjectClass;
}
public void setCnAttribute(String cnAttribute) {
this.cnAttribute = cnAttribute;
}
}