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