001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.isis.extensions.shirorealmldap.realm.impl; 020 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashSet; 024import java.util.Map; 025import java.util.Set; 026 027import javax.naming.AuthenticationException; 028import javax.naming.NamingEnumeration; 029import javax.naming.NamingException; 030import javax.naming.directory.Attribute; 031import javax.naming.directory.SearchControls; 032import javax.naming.directory.SearchResult; 033import javax.naming.ldap.LdapContext; 034 035import org.apache.shiro.authz.AuthorizationInfo; 036import org.apache.shiro.authz.SimpleAuthorizationInfo; 037import org.apache.shiro.config.Ini; 038import org.apache.shiro.realm.ldap.DefaultLdapRealm; 039import org.apache.shiro.realm.ldap.LdapContextFactory; 040import org.apache.shiro.realm.ldap.LdapUtils; 041import org.apache.shiro.subject.PrincipalCollection; 042import org.apache.shiro.util.StringUtils; 043 044import org.apache.isis.commons.internal.collections._Maps; 045import org.apache.isis.commons.internal.collections._Sets; 046import org.apache.isis.security.shiro.permrolemapper.PermissionToRoleMapper; 047import org.apache.isis.security.shiro.permrolemapper.PermissionToRoleMapperFromIni; 048import org.apache.isis.security.shiro.permrolemapper.PermissionToRoleMapperFromString; 049 050import static org.apache.isis.commons.internal.base._NullSafe.stream; 051 052/** 053 * Implementation of {@link org.apache.shiro.realm.ldap.JndiLdapRealm} that also 054 * returns each user's groups. 055 * <p/> 056 * <p> 057 * Sample config for <tt>shiro.ini</tt>: 058 * <p/> 059 * <pre> 060 * contextFactory = org.apache.isis.extensions.shirorealmldap.realm.impl.IsisLdapContextFactory 061 * contextFactory.url = ldap://localhost:10389 062 * contextFactory.authenticationMechanism = CRAM-MD5 063 * contextFactory.systemAuthenticationMechanism = simple 064 * contextFactory.systemUsername = uid=admin,ou=system 065 * contextFactory.systemPassword = secret 066 * 067 * ldapRealm = org.apache.isis.extensions.shirorealmldap.realm.impl.IsisLdapRealm 068 * ldapRealm.contextFactory = $contextFactory 069 * 070 * ldapRealm.searchBase = ou=groups,o=mojo 071 * ldapRealm.groupObjectClass = groupOfUniqueNames 072 * ldapRealm.uniqueMemberAttribute = uniqueMember 073 * ldapRealm.uniqueMemberAttributeValueTemplate = uid={0} 074 * 075 * ldapRealm.searchUserBase = ou=users,o=mojo 076 * ldapRealm.userObjectClass=inetOrgPerson 077 * ldapRealm.groupExtractedAttribute=street,country 078 * ldapRealm.userExtractedAttribute=street,country 079 * ldapRealm.permissionByGroupAttribute=attribute:Folder.{street}:Read,attribute:Portfolio.{country} 080 * ldapRealm.permissionByUserAttribute=attribute:Folder.{street}:Read,attribute:Portfolio.{country} 081 * 082 * # optional mapping from physical groups to logical application roles 083 * ldapRealm.rolesByGroup = \ 084 * LDN_USERS: user_role,\ 085 * NYK_USERS: user_role,\ 086 * HKG_USERS: user_role,\ 087 * GLOBAL_ADMIN: admin_role,\ 088 * DEMOS: self-install_role 089 * 090 * securityManager.realms = $ldapRealm 091 * </pre> 092 * <p/> 093 * <p> 094 * The permissions for each role can be specified using the 095 * {@link #setResourcePath(String)} to an 'ini' file with a [roles] section, eg: 096 * <p/> 097 * <pre> 098 * ldapRealm.resourcePath=classpath:webapp/myroles.ini 099 * </pre> 100 * <p/> 101 * <p> 102 * where <tt>myroles.ini</tt> is in <tt>src/main/resources/webapp</tt>, and takes the form: 103 * <p/> 104 * <pre> 105 * [roles] 106 * user_role = *:ToDoItemsJdo:*:*,\ 107 * *:ToDoItem:*:* 108 * self-install_role = *:ToDoItemsFixturesService:install:* 109 * admin_role = * 110 * </pre> 111 * <p/> 112 * <p> 113 * This 'ini' file can then be referenced by other realms (if multiple realm are configured 114 * with the Shiro security manager). 115 * <p/> 116 * <p> 117 * Alternatively, permissions can be set directly using {@link #setPermissionsByRole(String)}, 118 * where the string is the same information, formatted thus: 119 * <p/> 120 * <pre> 121 * ldapRealm.permissionsByRole=\ 122 * user_role = *:ToDoItemsJdo:*:*,\ 123 * *:ToDoItem:*:*; \ 124 * self-install_role = *:ToDoItemsFixturesService:install:* ; \ 125 * admin_role = * 126 * </pre> 127 * <p/> 128 * <p> 129 * Alternatively, permissions can be extracted from the base itself with the parameter searchUserBase, 130 * the attribute list as userExtractedAttribute and the permission url as permissionByUserAttribute. 131 * The idea is to extract attribute from the user or the group of the user and map directly to permission rule in 132 * replacing the string {attribute} by the extracted attribute (can me multiple). 133 * See the sample for group and user attribute and mapping. 134 * <p/> 135 * </p> 136 */ 137public class IsisLdapRealm extends DefaultLdapRealm { 138 139 private static final String UNIQUEMEMBER_SUBSTITUTION_TOKEN = "{0}"; 140 private static final SearchControls SUBTREE_SCOPE = new SearchControls(); 141 142 static { 143 SUBTREE_SCOPE.setSearchScope(SearchControls.SUBTREE_SCOPE); 144 } 145 146 private String searchBase; 147 private String groupObjectClass; 148 private String uniqueMemberAttribute = "uniqueMember"; 149 private String uniqueMemberAttributeValuePrefix; 150 private String uniqueMemberAttributeValueSuffix; 151 152 /** 153 * For Group Extracted attribute name with mapping name in parenthesis. Ex: street,country 154 */ 155 protected Set<String> groupExtractedAttribute = _Sets.newConcurrentHashSet(); 156 157 /** 158 * For User Extracted attribute name with mapping name in parenthesis. Ex: street,country 159 */ 160 protected Set<String> userExtractedAttribute = _Sets.newConcurrentHashSet(); 161 162 /** 163 * For Group Mapping of attributes. Ex: 164 * attribute:Folder.{street}:Read,attribute:Portfolio.{country}:* 165 */ 166 protected Set<String> permissionByGroupAttribute = _Sets.newConcurrentHashSet(); 167 168 /** 169 * For User Mapping of attributes. Ex: 170 * attribute:Folder.{street}:Read,attribute:Portfolio.{country}:* 171 */ 172 protected Set<String> permissionByUserAttribute = _Sets.newConcurrentHashSet(); 173 174 /** 175 * For search ldap on user 176 */ 177 private String searchUserBase = ""; 178 179 /** 180 * The object className as person 181 */ 182 private String userObjectClass; 183 184 private final Map<String, String> rolesByGroup = _Maps.newLinkedHashMap(); 185 186 private PermissionToRoleMapper permissionToRoleMapper; 187 188 /** 189 * cn attribute 190 */ 191 private String cnAttribute = "cn"; 192 193 public IsisLdapRealm() { 194 setGroupObjectClass("groupOfUniqueNames"); 195 setUniqueMemberAttribute("uniqueMember"); 196 setUniqueMemberAttributeValueTemplate("uid={0}"); 197 } 198 199 /** 200 * Get groups from LDAP. 201 * 202 * @param principals the principals of the Subject whose AuthenticationInfo should 203 * be queried from the LDAP server. 204 * @param ldapContextFactory factory used to retrieve LDAP connections. 205 * @return an {@link AuthorizationInfo} instance containing information 206 * retrieved from the LDAP server. 207 * @throws NamingException if any LDAP errors occur during the search. 208 */ 209 @Override 210 protected AuthorizationInfo queryForAuthorizationInfo(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException { 211 final Set<String> roleNames = getRoles(principals, ldapContextFactory); 212 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roleNames); 213 Set<String> stringPermissions = permsFor(roleNames); 214 final String username = (String) getAvailablePrincipal(principals); 215 final LdapContext finalLdapContext = ldapContextFactory.getSystemLdapContext(); 216 stringPermissions.addAll(getPermissionForUser(username, finalLdapContext)); 217 stringPermissions.addAll(getPermissionForRole(username, finalLdapContext)); 218 simpleAuthorizationInfo.setStringPermissions(stringPermissions); 219 220 return simpleAuthorizationInfo; 221 } 222 223 private Set<String> 224 getPermissionForRole(String username, LdapContext ldapContext) 225 throws NamingException { 226 final Set<String> permissions = _Sets.newLinkedHashSet(); 227 228 Set<String> groups = groupFor(username, ldapContext); 229 final NamingEnumeration<SearchResult> searchResultEnum = ldapContext.search(searchBase, 230 "objectClass=" + groupObjectClass, SUBTREE_SCOPE); 231 while (searchResultEnum.hasMore()) { 232 final SearchResult group = searchResultEnum.next(); 233 if (memberOf(group, groups)) { 234 addPermIfFound(group, permissions, groupExtractedAttribute, permissionByGroupAttribute); 235 } 236 } 237 return permissions; 238 } 239 240 protected Set<String> groupFor(final String userName, final LdapContext ldapCtx) 241 throws NamingException { 242 final Set<String> roleNames = _Sets.newLinkedHashSet(); 243 final NamingEnumeration<SearchResult> searchResultEnum = ldapCtx.search(searchBase, 244 "objectClass=" + groupObjectClass, SUBTREE_SCOPE); 245 while (searchResultEnum.hasMore()) { 246 final SearchResult group = searchResultEnum.next(); 247 addRoleIfMember(userName, group, roleNames); 248 } 249 return roleNames; 250 } 251 252 protected boolean memberOf(SearchResult group, Set<String> groups) throws NamingException { 253 Attribute attribute = group.getAttributes().get(cnAttribute); 254 String groupName = attribute.get().toString(); 255 return groups.contains(groupName); 256 } 257 258 private Collection<String> getPermissionForUser( 259 String username, 260 LdapContext ldapContextFactory) throws NamingException { 261 262 try { 263 return permUser(username, ldapContextFactory); 264 } catch (org.apache.shiro.authc.AuthenticationException ex) { 265 return Collections.emptySet(); 266 } 267 } 268 269 private Collection<String> permUser(String username, LdapContext systemLdapCtx) 270 throws NamingException { 271 final Set<String> permissions = _Sets.newLinkedHashSet(); 272 final NamingEnumeration<SearchResult> searchResultEnum = systemLdapCtx.search( 273 searchUserBase, "objectClass=" + userObjectClass, SUBTREE_SCOPE); 274 while (searchResultEnum.hasMore()) { 275 final SearchResult group = searchResultEnum.next(); 276 addPermIfFound(group, permissions, userExtractedAttribute, permissionByUserAttribute); 277 } 278 return permissions; 279 } 280 281 private void addPermIfFound( 282 SearchResult group, Set<String> permissions, 283 Set<String> extractedAttributeP, Set<String> permissionByAttributeP) 284 throws NamingException { 285 final NamingEnumeration<? extends Attribute> attributeEnum = group.getAttributes().getAll(); 286 Map<String, Set<String>> keyValues = _Maps.newHashMap(); 287 while (attributeEnum.hasMore()) { 288 final Attribute attr = attributeEnum.next(); 289 if (extractedAttributeP.contains(attr.getID())) { 290 final NamingEnumeration<?> e = attr.getAll(); 291 keyValues.put(attr.getID(), new HashSet<String>()); 292 while (e.hasMore()) { 293 String attrValue = e.next().toString(); 294 keyValues.get(attr.getID()).add(attrValue); 295 } 296 } 297 } 298 for (String permTempl : permissionByAttributeP) { 299 for (String key : keyValues.keySet()) { 300 if (permTempl.contains("{" + key + "}")) { 301 for (String value : keyValues.get(key)) { 302 permissions.add(permTempl.replaceAll("\\{" + key + "\\}", value)); 303 } 304 } 305 } 306 } 307 } 308 309 private Set<String> getRoles(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException { 310 final String username = (String) getAvailablePrincipal(principals); 311 312 LdapContext systemLdapCtx = null; 313 try { 314 systemLdapCtx = ldapContextFactory.getSystemLdapContext(); 315 return rolesFor(username, systemLdapCtx); 316 } catch (AuthenticationException ex) { 317 // principal was not authenticated on LDAP 318 return Collections.emptySet(); 319 } finally { 320 LdapUtils.closeContext(systemLdapCtx); 321 } 322 } 323 324 private Set<String> rolesFor(final String userName, final LdapContext ldapCtx) throws NamingException { 325 final Set<String> roleNames = _Sets.newLinkedHashSet(); 326 final NamingEnumeration<SearchResult> searchResultEnum = ldapCtx.search(searchBase, "objectClass=" + groupObjectClass, SUBTREE_SCOPE); 327 while (searchResultEnum.hasMore()) { 328 final SearchResult group = searchResultEnum.next(); 329 addRoleIfMember(userName, group, roleNames); 330 } 331 return roleNames; 332 } 333 334 private void addRoleIfMember(final String userName, final SearchResult group, final Set<String> roleNames) throws NamingException { 335 final NamingEnumeration<? extends Attribute> attributeEnum = group.getAttributes().getAll(); 336 while (attributeEnum.hasMore()) { 337 final Attribute attr = attributeEnum.next(); 338 if (!uniqueMemberAttribute.equalsIgnoreCase(attr.getID())) { 339 continue; 340 } 341 final NamingEnumeration<?> e = attr.getAll(); 342 while (e.hasMore()) { 343 String attrValue = e.next().toString(); 344 if ((uniqueMemberAttributeValuePrefix + userName + uniqueMemberAttributeValueSuffix).equals(attrValue)) { 345 Attribute attribute = group.getAttributes().get("cn"); 346 String groupName = attribute.get().toString(); 347 String roleName = roleNameFor(groupName); 348 if (roleName != null) { 349 roleNames.add(roleName); 350 } 351 break; 352 } 353 } 354 } 355 } 356 357 private String roleNameFor(String groupName) { 358 return !rolesByGroup.isEmpty() ? rolesByGroup.get(groupName) : groupName; 359 } 360 361 private Set<String> permsFor(Set<String> roleNames) { 362 Set<String> perms = _Sets.newLinkedHashSet(); // preserve order 363 for (String role : roleNames) { 364 Set<String> permsForRole = getPermissionsByRole().get(role); 365 if (permsForRole != null) { 366 perms.addAll(permsForRole); 367 } 368 } 369 return perms; 370 } 371 372 public void setSearchBase(String searchBase) { 373 this.searchBase = searchBase; 374 } 375 376 public void setGroupObjectClass(String groupObjectClassAttribute) { 377 this.groupObjectClass = groupObjectClassAttribute; 378 } 379 380 public void setUniqueMemberAttribute(String uniqueMemberAttribute) { 381 this.uniqueMemberAttribute = uniqueMemberAttribute; 382 } 383 384 public void setUniqueMemberAttributeValueTemplate(String template) { 385 if (!StringUtils.hasText(template)) { 386 String msg = "User DN template cannot be null or empty."; 387 throw new IllegalArgumentException(msg); 388 } 389 int index = template.indexOf(UNIQUEMEMBER_SUBSTITUTION_TOKEN); 390 if (index < 0) { 391 String msg = "UniqueMember attribute value template must contain the '" + 392 UNIQUEMEMBER_SUBSTITUTION_TOKEN + "' replacement token to understand how to " + 393 "parse the group members."; 394 throw new IllegalArgumentException(msg); 395 } 396 String prefix = template.substring(0, index); 397 String suffix = template.substring(prefix.length() + UNIQUEMEMBER_SUBSTITUTION_TOKEN.length()); 398 this.uniqueMemberAttributeValuePrefix = prefix; 399 this.uniqueMemberAttributeValueSuffix = suffix; 400 } 401 402 public void setRolesByGroup(Map<String, String> rolesByGroup) { 403 this.rolesByGroup.putAll(rolesByGroup); 404 } 405 406 /** 407 * Retrieves permissions by role set using either 408 * {@link #setPermissionsByRole(String)} or {@link #setResourcePath(String)}. 409 */ 410 private Map<String, Set<String>> getPermissionsByRole() { 411 if (permissionToRoleMapper == null) { 412 throw new IllegalStateException("Permissions by role not yet set."); 413 } 414 return permissionToRoleMapper.getPermissionsByRole(); 415 } 416 417 /** 418 * <pre> 419 * ldapRealm.resourcePath=classpath:webapp/myroles.ini 420 * </pre> 421 * <p/> 422 * <p/> 423 * where <tt>myroles.ini</tt> is in <tt>src/main/resources/webapp</tt>, and takes the form: 424 * <p/> 425 * <pre> 426 * [roles] 427 * user_role = *:ToDoItemsJdo:*:*,\ 428 * *:ToDoItem:*:* 429 * self-install_role = *:ToDoItemsFixturesService:install:* 430 * admin_role = * 431 * </pre> 432 * <p/> 433 * <p/> 434 * This 'ini' file can then be referenced by other realms (if multiple realm are configured 435 * with the Shiro security manager). 436 * 437 * @see #setResourcePath(String) 438 */ 439 public void setResourcePath(String resourcePath) { 440 if (permissionToRoleMapper != null) { 441 throw new IllegalStateException("Permissions already set, " + permissionToRoleMapper.getClass().getName()); 442 } 443 final Ini ini = Ini.fromResourcePath(resourcePath); 444 this.permissionToRoleMapper = new PermissionToRoleMapperFromIni(ini); 445 } 446 447 /** 448 * Specify permissions for each role using a formatted string. 449 * <p/> 450 * <pre> 451 * ldapRealm.permissionsByRole=\ 452 * user_role = *:ToDoItemsJdo:*:*,\ 453 * *:ToDoItem:*:*; \ 454 * self-install_role = *:ToDoItemsFixturesService:install:* ; \ 455 * admin_role = * 456 * </pre> 457 * 458 * @see #setResourcePath(String) 459 */ 460 @Deprecated 461 public void setPermissionsByRole(String permissionsByRoleStr) { 462 if (permissionToRoleMapper != null) { 463 throw new IllegalStateException("Permissions already set, " + permissionToRoleMapper.getClass().getName()); 464 } 465 this.permissionToRoleMapper = new PermissionToRoleMapperFromString(permissionsByRoleStr); 466 } 467 468 public void setPermissionByUserAttribute(String permissionByUserAttr) { 469 String[] list = permissionByUserAttr.split(","); 470 stream(list).forEach(this.permissionByUserAttribute::add); 471 } 472 473 public void setPermissionByGroupAttribute(String permissionByGroupAttribute) { 474 String[] list = permissionByGroupAttribute.split(","); 475 stream(list).forEach(this.permissionByGroupAttribute::add); 476 } 477 478 public void setUserExtractedAttribute(String userExtractedAttribute) { 479 String[] list = userExtractedAttribute.split(","); 480 stream(list).forEach(this.userExtractedAttribute::add); 481 } 482 483 public void setGroupExtractedAttribute(String groupExtractedAttribute) { 484 String[] list = groupExtractedAttribute.split(","); 485 stream(list).forEach(this.groupExtractedAttribute::add); 486 } 487 488 public void setSearchUserBase(String searchUserBase) { 489 this.searchUserBase = searchUserBase; 490 } 491 492 public void setUserObjectClass(String userObjectClass) { 493 this.userObjectClass = userObjectClass; 494 } 495 496 public void setCnAttribute(String cnAttribute) { 497 this.cnAttribute = cnAttribute; 498 } 499 500}