001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.kew.doctype.service.impl; 017 018import org.apache.commons.collections.CollectionUtils; 019import org.apache.commons.lang.StringUtils; 020import org.apache.commons.lang.builder.EqualsBuilder; 021import org.apache.commons.lang.builder.HashCodeBuilder; 022import org.kuali.rice.core.api.CoreConstants; 023import org.kuali.rice.core.api.datetime.DateTimeService; 024import org.kuali.rice.core.api.reflect.ObjectDefinition; 025import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader; 026import org.kuali.rice.core.api.util.KeyValue; 027import org.kuali.rice.kew.api.KewApiServiceLocator; 028import org.kuali.rice.kew.api.WorkflowRuntimeException; 029import org.kuali.rice.kew.api.document.Document; 030import org.kuali.rice.kew.api.document.search.DocumentSearchResult; 031import org.kuali.rice.kew.api.document.search.DocumentSearchResults; 032import org.kuali.rice.kew.api.extension.ExtensionDefinition; 033import org.kuali.rice.kew.api.extension.ExtensionRepositoryService; 034import org.kuali.rice.kew.doctype.DocumentTypeSecurity; 035import org.kuali.rice.kew.framework.KewFrameworkServiceLocator; 036import org.kuali.rice.kew.framework.document.security.DocumentSecurityDirective; 037import org.kuali.rice.kew.framework.document.security.DocumentSecurityHandlerService; 038import org.kuali.rice.kew.framework.document.security.DocumentSecurityAttribute; 039import org.kuali.rice.kew.doctype.SecurityPermissionInfo; 040import org.kuali.rice.kew.doctype.SecuritySession; 041import org.kuali.rice.kew.doctype.bo.DocumentType; 042import org.kuali.rice.kew.doctype.service.DocumentSecurityService; 043import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 044import org.kuali.rice.kew.service.KEWServiceLocator; 045import org.kuali.rice.kew.user.UserUtils; 046import org.kuali.rice.kew.api.KewApiConstants; 047import org.kuali.rice.kim.api.group.Group; 048import org.kuali.rice.kim.api.services.KimApiServiceLocator; 049import org.springframework.util.LinkedMultiValueMap; 050import org.springframework.util.MultiValueMap; 051 052import java.lang.reflect.Field; 053import java.util.ArrayList; 054import java.util.Calendar; 055import java.util.Collection; 056import java.util.Collections; 057import java.util.HashMap; 058import java.util.HashSet; 059import java.util.Iterator; 060import java.util.List; 061import java.util.Map; 062import java.util.Set; 063 064public class DocumentSecurityServiceImpl implements DocumentSecurityService { 065 066 public static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger( 067 DocumentSecurityServiceImpl.class); 068 069 private ExtensionRepositoryService extensionRepositoryService; 070 071 @Override 072 public boolean routeLogAuthorized(String principalId, DocumentRouteHeaderValue routeHeader, 073 SecuritySession securitySession) { 074 List<Document> documents = Collections.singletonList(DocumentRouteHeaderValue.to(routeHeader)); 075 Set<String> authorizationResults = checkAuthorizations(principalId, securitySession, documents); 076 return authorizationResults.contains(routeHeader.getDocumentId()); 077 } 078 079 @Override 080 public Set<String> documentSearchResultAuthorized(String principalId, DocumentSearchResults results, 081 SecuritySession securitySession) { 082 List<Document> documents = new ArrayList<Document>(); 083 for (DocumentSearchResult result : results.getSearchResults()) { 084 documents.add(result.getDocument()); 085 } 086 return checkAuthorizations(principalId, securitySession, documents); 087 } 088 089 protected Set<String> checkAuthorizations(String principalId, SecuritySession securitySession, 090 List<Document> documents) { 091 Set<String> authorizations = new HashSet<String>(); 092 // a list of documents which need to be processed with security extension attributes after the standard set of 093 // security has been attempted 094 List<Document> documentsRequiringExtensionProcessing = new ArrayList<Document>(); 095 boolean admin = isAdmin(securitySession); 096 for (Document document : documents) { 097 if (admin) { 098 authorizations.add(document.getDocumentId()); 099 continue; 100 } 101 DocumentTypeSecurity security = null; 102 try { 103 security = getDocumentTypeSecurity(document.getDocumentTypeName(), securitySession); 104 if (security == null || !security.isActive() || checkStandardAuthorization(security, principalId, 105 document, securitySession)) { 106 authorizations.add(document.getDocumentId()); 107 } else { 108 // if we get to this point, it means we aren't authorized yet, last chance for authorization will be 109 // security extension attributes, so prepare for execution of those after the main loop is complete 110 if (CollectionUtils.isNotEmpty(security.getSecurityAttributeExtensionNames())) { 111 documentsRequiringExtensionProcessing.add(document); 112 } 113 } 114 } catch (Exception e) { 115 LOG.warn( 116 "Not able to retrieve DocumentTypeSecurity from remote system for documentTypeName: " + document 117 .getDocumentTypeName(), e); 118 continue; 119 } 120 } 121 processDocumentRequiringExtensionProcessing(documentsRequiringExtensionProcessing, securitySession, 122 authorizations); 123 return authorizations; 124 } 125 126 protected void processDocumentRequiringExtensionProcessing(List<Document> documentsRequiringExtensionProcessing, 127 SecuritySession securitySession, Set<String> authorizations) { 128 if (CollectionUtils.isNotEmpty(documentsRequiringExtensionProcessing)) { 129 LOG.info("Beginning processing of documents requiring extension processing (total: " 130 + documentsRequiringExtensionProcessing.size() 131 + " documents)"); 132 long start = System.currentTimeMillis(); 133 MultiValueMap<PartitionKey, Document> partitions = partitionDocumentsForSecurity( 134 documentsRequiringExtensionProcessing, securitySession); 135 MultiValueMap<String, DocumentSecurityDirective> applicationSecurityDirectives = 136 new LinkedMultiValueMap<String, DocumentSecurityDirective>(); 137 for (PartitionKey partitionKey : partitions.keySet()) { 138 DocumentSecurityDirective directive = DocumentSecurityDirective.create( 139 partitionKey.getDocumentSecurityAttributeNameList(), partitions.get(partitionKey)); 140 applicationSecurityDirectives.add(partitionKey.applicationId, directive); 141 } 142 for (String applicationId : applicationSecurityDirectives.keySet()) { 143 List<DocumentSecurityDirective> documentSecurityDirectives = applicationSecurityDirectives.get( 144 applicationId); 145 DocumentSecurityHandlerService securityHandler = loadSecurityHandler(applicationId); 146 List<String> authorizedDocumentIds = securityHandler.getAuthorizedDocumentIds( 147 securitySession.getPrincipalId(), documentSecurityDirectives); 148 if (CollectionUtils.isNotEmpty(authorizedDocumentIds)) { 149 authorizations.addAll(authorizedDocumentIds); 150 } 151 } 152 long end = System.currentTimeMillis(); 153 LOG.info("Finished processing of documents requiring extension processing (total time: " 154 + (start - end) 155 + ")"); 156 } 157 } 158 159 protected MultiValueMap<PartitionKey, Document> partitionDocumentsForSecurity(List<Document> documents, 160 SecuritySession securitySession) { 161 MultiValueMap<PartitionKey, Document> partitions = new LinkedMultiValueMap<PartitionKey, Document>(); 162 for (Document document : documents) { 163 DocumentTypeSecurity security = getDocumentTypeSecurity(document.getDocumentTypeName(), securitySession); 164 MultiValueMap<String, ExtensionDefinition> securityAttributeExtensionDefinitions = loadExtensionDefinitions( 165 security, securitySession); 166 for (String applicationId : securityAttributeExtensionDefinitions.keySet()) { 167 List<ExtensionDefinition> extensionDefinitions = securityAttributeExtensionDefinitions.get( 168 applicationId); 169 PartitionKey key = new PartitionKey(applicationId, extensionDefinitions); 170 partitions.add(key, document); 171 } 172 } 173 return partitions; 174 } 175 176 protected MultiValueMap<String, ExtensionDefinition> loadExtensionDefinitions(DocumentTypeSecurity security, 177 SecuritySession securitySession) { 178 MultiValueMap<String, ExtensionDefinition> securityAttributeExtensionDefinitions = 179 new LinkedMultiValueMap<String, ExtensionDefinition>(); 180 List<String> securityAttributeExtensionNames = security.getSecurityAttributeExtensionNames(); 181 for (String securityAttributeExtensionName : securityAttributeExtensionNames) { 182 ExtensionDefinition extensionDefinition = extensionRepositoryService.getExtensionByName( 183 securityAttributeExtensionName); 184 securityAttributeExtensionDefinitions.add(extensionDefinition.getApplicationId(), extensionDefinition); 185 } 186 return securityAttributeExtensionDefinitions; 187 } 188 189 protected DocumentSecurityHandlerService loadSecurityHandler(String applicationId) { 190 DocumentSecurityHandlerService service = KewFrameworkServiceLocator.getDocumentSecurityHandlerService( 191 applicationId); 192 if (service == null) { 193 throw new WorkflowRuntimeException( 194 "Failed to locate DocumentSecurityHandlerService for applicationId: " + applicationId); 195 } 196 return service; 197 } 198 199 protected boolean isAdmin(SecuritySession session) { 200 if (session.getPrincipalId() == null) { 201 return false; 202 } 203 return KimApiServiceLocator.getPermissionService().isAuthorized(session.getPrincipalId(), 204 KewApiConstants.KEW_NAMESPACE, KewApiConstants.PermissionNames.UNRESTRICTED_DOCUMENT_SEARCH, new HashMap<String, String>()); 205 } 206 207 protected boolean checkStandardAuthorization(DocumentTypeSecurity security, String principalId, Document document, 208 SecuritySession securitySession) { 209 String documentId = document.getDocumentId(); 210 String initiatorPrincipalId = document.getInitiatorPrincipalId(); 211 212 LOG.debug("auth check user=" + principalId + " docId=" + documentId); 213 214 // Doc Initiator Authorization 215 if (security.getInitiatorOk() != null && security.getInitiatorOk()) { 216 boolean isInitiator = StringUtils.equals(initiatorPrincipalId, principalId); 217 if (isInitiator) { 218 return true; 219 } 220 } 221 222 // Permission Authorization 223 List<SecurityPermissionInfo> securityPermissions = security.getPermissions(); 224 if (securityPermissions != null) { 225 for (SecurityPermissionInfo securityPermission : securityPermissions) { 226 if (isAuthenticatedByPermission(documentId, securityPermission.getPermissionNamespaceCode(), 227 securityPermission.getPermissionName(), securityPermission.getPermissionDetails(), 228 securityPermission.getQualifications(), securitySession)) { 229 return true; 230 } 231 } 232 } 233 234 // Group Authorization 235 List<Group> securityWorkgroups = security.getWorkgroups(); 236 if (securityWorkgroups != null) { 237 for (Group securityWorkgroup : securityWorkgroups) { 238 if (isGroupAuthenticated(securityWorkgroup.getNamespaceCode(), securityWorkgroup.getName(), 239 securitySession)) { 240 return true; 241 } 242 } 243 } 244 245 // Searchable Attribute Authorization 246 Collection searchableAttributes = security.getSearchableAttributes(); 247 if (searchableAttributes != null) { 248 for (Iterator iterator = searchableAttributes.iterator(); iterator.hasNext(); ) { 249 KeyValue searchableAttr = (KeyValue) iterator.next(); 250 String attrName = searchableAttr.getKey(); 251 String idType = searchableAttr.getValue(); 252 String idValue = UserUtils.getIdValue(idType, principalId); 253 if (!StringUtils.isEmpty(idValue)) { 254 if (KEWServiceLocator.getRouteHeaderService().hasSearchableAttributeValue(documentId, attrName, 255 idValue)) { 256 return true; 257 } 258 } 259 } 260 } 261 262 // Route Log Authorization 263 if (security.getRouteLogAuthenticatedOk() != null && security.getRouteLogAuthenticatedOk()) { 264 boolean isInitiator = StringUtils.equals(initiatorPrincipalId, principalId); 265 if (isInitiator) { 266 return true; 267 } 268 boolean hasTakenAction = KEWServiceLocator.getActionTakenService().hasUserTakenAction(principalId, 269 documentId); 270 if (hasTakenAction) { 271 return true; 272 } 273 boolean hasRequest = KEWServiceLocator.getActionRequestService().doesPrincipalHaveRequest(principalId, 274 documentId); 275 if (hasRequest) { 276 return true; 277 } 278 } 279 280 // local security attribute authorization 281 List<DocumentSecurityAttribute> immediateSecurityAttributes = getImmediateSecurityAttributes(document, security, 282 securitySession); 283 if (immediateSecurityAttributes != null) { 284 for (DocumentSecurityAttribute immediateSecurityAttribute : immediateSecurityAttributes) { 285 boolean isAuthorized = immediateSecurityAttribute.isAuthorizedForDocument(principalId, document); 286 if (isAuthorized) { 287 return true; 288 } 289 } 290 } 291 292 LOG.debug("user not authorized"); 293 return false; 294 } 295 296 protected List<DocumentSecurityAttribute> getImmediateSecurityAttributes(Document document, DocumentTypeSecurity security, 297 SecuritySession securitySession) { 298 List<DocumentSecurityAttribute> securityAttributes = new ArrayList<DocumentSecurityAttribute>(); 299 for (String securityAttributeClassName : security.getSecurityAttributeClassNames()) { 300 DocumentSecurityAttribute securityAttribute = securitySession.getSecurityAttributeForClass( 301 securityAttributeClassName); 302 if (securityAttribute == null) { 303 securityAttribute = GlobalResourceLoader.getObject(new ObjectDefinition(securityAttributeClassName)); 304 securitySession.setSecurityAttributeForClass(securityAttributeClassName, securityAttribute); 305 } 306 securityAttributes.add(securityAttribute); 307 } 308 return securityAttributes; 309 } 310 311 protected DocumentTypeSecurity getDocumentTypeSecurity(String documentTypeName, SecuritySession session) { 312 DocumentTypeSecurity security = session.getDocumentTypeSecurity().get(documentTypeName); 313 if (security == null) { 314 DocumentType docType = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeName); 315 if (docType != null) { 316 security = docType.getDocumentTypeSecurity(); 317 session.getDocumentTypeSecurity().put(documentTypeName, security); 318 } 319 } 320 return security; 321 } 322 323 protected boolean isGroupAuthenticated(String namespace, String groupName, SecuritySession session) { 324 String key = namespace.trim() + KewApiConstants.KIM_GROUP_NAMESPACE_NAME_DELIMITER_CHARACTER + groupName.trim(); 325 Boolean existingAuth = session.getAuthenticatedWorkgroups().get(key); 326 if (existingAuth != null) { 327 return existingAuth; 328 } 329 boolean memberOfGroup = isMemberOfGroupWithName(namespace, groupName, session.getPrincipalId()); 330 session.getAuthenticatedWorkgroups().put(key, memberOfGroup); 331 return memberOfGroup; 332 } 333 334 private boolean isMemberOfGroupWithName(String namespace, String groupName, String principalId) { 335 for (Group group : KimApiServiceLocator.getGroupService().getGroupsByPrincipalId(principalId)) { 336 if (StringUtils.equals(namespace, group.getNamespaceCode()) && StringUtils.equals(groupName, 337 group.getName())) { 338 return true; 339 } 340 } 341 return false; 342 } 343 344 protected boolean isAuthenticatedByPermission(String documentId, String permissionNamespaceCode, 345 String permissionName, Map<String, String> permissionDetails, Map<String, String> qualification, 346 SecuritySession session) { 347 348 Document document; 349 try { 350 document = KewApiServiceLocator.getWorkflowDocumentService().getDocument(documentId); 351 352 for (String qualificationKey : qualification.keySet()) { 353 String qualificationValue = qualification.get(qualificationKey); 354 String replacementValue = getReplacementString(document, qualificationValue); 355 qualification.put(qualificationKey, replacementValue); 356 } 357 358 for (String permissionDetailKey : permissionDetails.keySet()) { 359 String detailValue = qualification.get(permissionDetailKey); 360 String replacementValue = getReplacementString(document, detailValue); 361 qualification.put(permissionDetailKey, replacementValue); 362 } 363 } catch (Exception e) { 364 LOG.error(e.getMessage(), e); 365 return false; 366 } 367 return KimApiServiceLocator.getPermissionService().isAuthorized(session.getPrincipalId(), 368 permissionNamespaceCode, permissionName, qualification); 369 } 370 371 private String getReplacementString(Document document, String value) throws Exception { 372 String startsWith = "${document."; 373 String endsWith = "}"; 374 if (value.startsWith(startsWith)) { 375 int tokenStart = value.indexOf(startsWith); 376 int tokenEnd = value.indexOf(endsWith, tokenStart + startsWith.length()); 377 if (tokenEnd == -1) { 378 throw new RuntimeException("No ending bracket on token in value " + value); 379 } 380 String token = value.substring(tokenStart + startsWith.length(), tokenEnd); 381 382 return getRouteHeaderVariableValue(document, token); 383 } 384 return value; 385 386 } 387 388 private String getRouteHeaderVariableValue(Document document, String variableName) throws Exception { 389 Field field; 390 try { 391 field = document.getClass().getDeclaredField(variableName); 392 } catch (NoSuchFieldException nsfe) { 393 LOG.error("Field '" + variableName + "' not found on Document object."); 394 // instead of raising an exception, return null as a value 395 // this leaves it up to proper permission configuration to fail the check if a field value 396 // is required 397 return null; 398 } 399 field.setAccessible(true); 400 Object fieldValue = field.get(document); 401 Class<?> clazzType = field.getType(); 402 if (clazzType.equals(String.class)) { 403 return (String) fieldValue; 404 } else if (clazzType.getName().equals("boolean") || clazzType.getName().equals("java.lang.Boolean")) { 405 if ((Boolean) fieldValue) { 406 return "Y"; 407 } 408 return "N"; 409 } else if (clazzType.getName().equals("java.util.Calendar")) { 410 411 DateTimeService dateTimeService = GlobalResourceLoader.getService(CoreConstants.Services.DATETIME_SERVICE); 412 return dateTimeService.toDateString(((Calendar) fieldValue).getTime()); 413 } 414 return String.valueOf(fieldValue); 415 } 416 417 public ExtensionRepositoryService getExtensionRepositoryService() { 418 return extensionRepositoryService; 419 } 420 421 public void setExtensionRepositoryService(ExtensionRepositoryService extensionRepositoryService) { 422 this.extensionRepositoryService = extensionRepositoryService; 423 } 424 425 /** 426 * Simple class which defines the key of a partition of security attributes associated with an application id. 427 * 428 * <p>This class allows direct field access since it is intended for internal use only.</p> 429 */ 430 private static final class PartitionKey { 431 String applicationId; 432 Set<String> documentSecurityAttributeNames; 433 434 PartitionKey(String applicationId, Collection<ExtensionDefinition> extensionDefinitions) { 435 this.applicationId = applicationId; 436 this.documentSecurityAttributeNames = new HashSet<String>(); 437 for (ExtensionDefinition extensionDefinition : extensionDefinitions) { 438 this.documentSecurityAttributeNames.add(extensionDefinition.getName()); 439 } 440 } 441 442 List<String> getDocumentSecurityAttributeNameList() { 443 return new ArrayList<String>(documentSecurityAttributeNames); 444 } 445 446 @Override 447 public boolean equals(Object o) { 448 if (!(o instanceof PartitionKey)) { 449 return false; 450 } 451 PartitionKey key = (PartitionKey) o; 452 EqualsBuilder builder = new EqualsBuilder(); 453 builder.append(applicationId, key.applicationId); 454 builder.append(documentSecurityAttributeNames, key.documentSecurityAttributeNames); 455 return builder.isEquals(); 456 } 457 458 @Override 459 public int hashCode() { 460 HashCodeBuilder builder = new HashCodeBuilder(); 461 builder.append(applicationId); 462 builder.append(documentSecurityAttributeNames); 463 return builder.hashCode(); 464 } 465 } 466 467}