001 /* 002 * Copyright 2006-2012 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 */ 016 package org.kuali.rice.kew.actions; 017 018 import junit.framework.Assert; 019 import org.apache.commons.lang.ArrayUtils; 020 import org.junit.Test; 021 import org.kuali.rice.coreservice.api.parameter.Parameter; 022 import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator; 023 import org.kuali.rice.kew.actionitem.ActionItem; 024 import org.kuali.rice.kew.api.KewApiConstants; 025 import org.kuali.rice.kew.api.KewApiServiceLocator; 026 import org.kuali.rice.kew.api.WorkflowDocument; 027 import org.kuali.rice.kew.api.WorkflowDocumentFactory; 028 import org.kuali.rice.kew.api.action.ActionRequest; 029 import org.kuali.rice.kew.api.action.ActionRequestType; 030 import org.kuali.rice.kew.api.action.ActionType; 031 import org.kuali.rice.kew.api.action.InvalidActionTakenException; 032 import org.kuali.rice.kew.doctype.bo.DocumentType; 033 import org.kuali.rice.kew.doctype.service.impl.KimDocumentTypeAuthorizer; 034 import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent; 035 import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 036 import org.kuali.rice.kew.postprocessor.DefaultPostProcessor; 037 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 038 import org.kuali.rice.kew.service.KEWServiceLocator; 039 import org.kuali.rice.kew.test.KEWTestCase; 040 import org.kuali.rice.kim.api.KimConstants; 041 import org.kuali.rice.kim.api.common.template.Template; 042 import org.kuali.rice.kim.api.permission.Permission; 043 import org.kuali.rice.kim.api.role.Role; 044 import org.kuali.rice.kim.api.services.KimApiServiceLocator; 045 import org.kuali.rice.kim.impl.common.attribute.KimAttributeBo; 046 import org.kuali.rice.krad.service.KRADServiceLocator; 047 import org.kuali.rice.krad.util.KRADConstants; 048 049 import java.util.Collection; 050 import java.util.HashMap; 051 import java.util.List; 052 import java.util.Map; 053 054 import static org.junit.Assert.*; 055 056 public class RecallActionTest extends KEWTestCase { 057 /** 058 * test postprocessor for testing afterActionTaken hook 059 */ 060 public static class RecallTestPostProcessor extends DefaultPostProcessor { 061 public static ActionType afterActionTakenType; 062 public static ActionTakenEvent afterActionTakenEvent; 063 @Override 064 public ProcessDocReport afterActionTaken(ActionType performed, ActionTakenEvent event) throws Exception { 065 afterActionTakenType = performed; 066 afterActionTakenEvent = event; 067 return super.afterActionTaken(performed, event); 068 } 069 } 070 071 public static class RecallTestDocumentTypeAuthorizer extends KimDocumentTypeAuthorizer { 072 public static String CUSTOM_RECALL_QUALIFIER_NAME = "Dynamic Qualifier"; 073 public static String CUSTOM_RECALL_QUALIFIER_VALUE = "Dynamic Qualifier Value"; 074 // we have to use a detail already defined for the recall permission - app doc status seems the most application-controlled 075 public static String CUSTOM_RECALL_DETAIL_NAME = KimConstants.AttributeConstants.APP_DOC_STATUS; 076 public static String CUSTOM_RECALL_DETAIL_VALUE = "Dynamic Recall Permission Detail Value"; 077 078 public static boolean buildPermissionDetailsInvoked = false; 079 public static boolean buildRoleQualifiersInvoked= false; 080 081 @Override 082 protected Map<String, String> buildDocumentTypePermissionDetails(DocumentType documentType, String documentStatus, String actionRequestedCode, String routeNodeName) { 083 buildPermissionDetailsInvoked = true; 084 Map<String, String> details = super.buildDocumentTypePermissionDetails(documentType, documentStatus, actionRequestedCode, routeNodeName); 085 details.put(CUSTOM_RECALL_DETAIL_NAME, CUSTOM_RECALL_DETAIL_VALUE); 086 return details; 087 } 088 089 @Override 090 protected Map<String, String> buildDocumentRoleQualifiers(DocumentRouteHeaderValue document, String routeNodeName) { 091 buildRoleQualifiersInvoked = true; 092 Map<String, String> qualifiers = super.buildDocumentRoleQualifiers(document, routeNodeName); 093 qualifiers.put(CUSTOM_RECALL_QUALIFIER_NAME, CUSTOM_RECALL_QUALIFIER_VALUE); 094 return qualifiers; 095 } 096 } 097 098 private static final String RECALL_TEST_DOC = "RecallTest"; 099 private static final String RECALL_TEST_RESTRICTED_DOC = "RecallTestRestricted"; 100 private static final String RECALL_TEST_NOROUTING_DOC = "RecallTestNoRouting"; 101 private static final String RECALL_TEST_ONLYADHOC_DOC = "RecallTestOnlyAdhoc"; 102 private static final String RECALL_NOTIFY_TEST_DOC = "RecallWithPrevNotifyTest"; 103 private static final String RECALL_NO_PENDING_NOTIFY_TEST_DOC = "RecallWithoutPendingNotifyTest"; 104 private static final String RECALL_NOTIFY_THIRDPARTY_TEST_DOC = "RecallWithThirdPartyNotifyTest"; 105 106 private String EWESTFAL = null; 107 private String JHOPF = null; 108 private String RKIRKEND = null; 109 private String NATJOHNS = null; 110 private String BMCGOUGH = null; 111 112 protected void loadTestData() throws Exception { 113 loadXmlFile("ActionsConfig.xml"); 114 } 115 116 @Override 117 protected void setUpAfterDataLoad() throws Exception { 118 super.setUpAfterDataLoad(); 119 EWESTFAL = getPrincipalIdForName("ewestfal"); 120 JHOPF = getPrincipalIdForName("jhopf"); 121 RKIRKEND = getPrincipalIdForName("rkirkend"); 122 NATJOHNS = getPrincipalIdForName("natjohns"); 123 BMCGOUGH = getPrincipalIdForName("bmcgough"); 124 125 RecallTestPostProcessor.afterActionTakenType = null; 126 RecallTestPostProcessor.afterActionTakenEvent = null; 127 } 128 129 protected void assertAfterActionTakenCalled(ActionType performed, ActionType taken) { 130 assertEquals(performed, RecallTestPostProcessor.afterActionTakenType); 131 assertNotNull(RecallTestPostProcessor.afterActionTakenEvent); 132 assertEquals(taken, RecallTestPostProcessor.afterActionTakenEvent.getActionTaken().getActionTaken()); 133 } 134 135 @Test(expected=InvalidActionTakenException.class) public void testCantRecallUnroutedDoc() { 136 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 137 document.recall("recalling", true); 138 } 139 140 @Test public void testRecallAsInitiatorBeforeAnyApprovals() throws Exception { 141 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 142 document.route(""); 143 144 document.recall("recalling", true); 145 146 assertTrue("Document should be recalled", document.isRecalled()); 147 assertAfterActionTakenCalled(ActionType.RECALL, ActionType.RECALL); 148 149 //verify that the document is truly dead - no more action requests or action items. 150 151 List requests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId()); 152 assertEquals("Should not have any active requests", 0, requests.size()); 153 154 Collection<ActionItem> actionItems = KEWServiceLocator.getActionListService().findByDocumentId(document.getDocumentId()); 155 assertEquals("Should not have any action items", 0, actionItems.size()); 156 } 157 158 @Test public void testRecallValidActionsTaken() throws Exception { 159 // just complete 160 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC); 161 document.route("routing"); 162 document.recall("recalling", true); 163 164 // save and complete 165 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC); 166 document.saveDocument("saving"); 167 document.route("routing"); 168 document.recall("recalling", true); 169 } 170 171 @Test 172 public void testRecallInvalidActionsTaken() throws Exception { 173 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC); 174 document.route(""); 175 176 document = WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()); 177 document.approve(""); 178 179 try { 180 document.recall("recalling", true); 181 fail("Recall should NOT have succeeded. Expected InvalidActionTakenException due to invalid 'APROVE' prior action taken."); 182 } catch (InvalidActionTakenException iate) { 183 assertTrue(iate.getMessage().contains("Invalid prior action taken: 'APPROVE'")); 184 } 185 } 186 187 @Test public void testRecallOnlyAdhocRouting() throws Exception { 188 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_ONLYADHOC_DOC); 189 // adhoc it to someone to prevent doc from going final - final is itself an invalid state for recall 190 document.adHocToPrincipal(ActionRequestType.APPROVE, "adhoc approve to JHOPF", JHOPF, "adhocing to prevent finalization", true); 191 document.route("routing"); 192 try { 193 document.recall("recalling", true); 194 fail("Recall should NOT have succeeded. Expected InvalidActionTakenException due to absence of non-adhoc route nodes."); 195 } catch (InvalidActionTakenException iate) { 196 assertTrue(iate.getMessage().contains("No non-adhoc route nodes defined")); 197 } 198 } 199 200 @Test public void testRecallAsInitiatorAfterSingleApproval() throws Exception { 201 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 202 document.route(""); 203 204 document = WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()); 205 document.approve(""); 206 207 document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId()); 208 document.recall("recalling", true); 209 210 assertTrue("Document should be recalled", document.isRecalled()); 211 assertAfterActionTakenCalled(ActionType.RECALL, ActionType.RECALL); 212 213 //verify that the document is truly dead - no more action requests or action items. 214 215 List requests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId()); 216 assertEquals("Should not have any active requests", 0, requests.size()); 217 218 Collection<ActionItem> actionItems = KEWServiceLocator.getActionListService().findByDocumentId(document.getDocumentId()); 219 assertEquals("Should not have any action items", 0, actionItems.size()); 220 221 // can't recall recalled doc 222 assertFalse(document.getValidActions().getValidActions().contains(ActionType.RECALL)); 223 } 224 225 @Test(expected=InvalidActionTakenException.class) 226 public void testRecallInvalidWhenProcessed() throws Exception { 227 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 228 document.route(""); 229 230 for (String user: new String[] { JHOPF, EWESTFAL, RKIRKEND, NATJOHNS, BMCGOUGH }) { 231 document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()); 232 document.approve(""); 233 } 234 235 document.refresh(); 236 assertTrue("Document should be processed", document.isProcessed()); 237 assertTrue("Document should be approved", document.isApproved()); 238 assertFalse("Document should not be final", document.isFinal()); 239 240 document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId()); 241 document.recall("recalling when processed should fail", true); 242 } 243 244 @Test(expected=InvalidActionTakenException.class) 245 public void testRecallInvalidWhenFinal() throws Exception { 246 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 247 document.route(""); 248 249 for (String user: new String[] { JHOPF, EWESTFAL, RKIRKEND, NATJOHNS, BMCGOUGH }) { 250 document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()); 251 document.approve(""); 252 } 253 document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("xqi"), document.getDocumentId()); 254 document.acknowledge(""); 255 256 document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jthomas"), document.getDocumentId()); 257 document.fyi(); 258 259 for (ActionRequest a: document.getRootActionRequests()) { 260 System.err.println(a); 261 if (a.isAcknowledgeRequest() || a.isFyiRequest()) { 262 System.err.println(a.getPrincipalId()); 263 System.err.println(KimApiServiceLocator.getIdentityService().getPrincipal(a.getPrincipalId()).getPrincipalName()); 264 } 265 } 266 267 assertFalse("Document should not be processed", document.isProcessed()); 268 assertTrue("Document should be approved", document.isApproved()); 269 assertTrue("Document should be final", document.isFinal()); 270 271 document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId()); 272 document.recall("recalling when processed should fail", true); 273 } 274 275 @Test public void testRecallToActionListAsInitiatorBeforeAnyApprovals() throws Exception { 276 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 277 document.route(""); 278 279 document.recall("recalling", false); 280 281 assertTrue("Document should be saved", document.isSaved()); 282 assertEquals(1, document.getCurrentNodeNames().size()); 283 assertTrue(document.getCurrentNodeNames().contains("AdHoc")); 284 assertAfterActionTakenCalled(ActionType.RECALL, ActionType.COMPLETE); 285 286 // initiator has completion request 287 assertTrue(document.isCompletionRequested()); 288 // can't recall saved doc 289 assertFalse(document.getValidActions().getValidActions().contains(ActionType.RECALL)); 290 291 // first approver has FYI 292 assertTrue(WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isFYIRequested()); 293 294 document.complete("completing"); 295 296 assertTrue("Document should be enroute", document.isEnroute()); 297 298 assertTrue(WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isApprovalRequested()); 299 } 300 301 private static final String PERM_APP_DOC_STATUS = "recallable by admins"; 302 private static final String ROUTE_NODE = "NotifyFirst"; 303 private static final String ROUTE_STATUS = "R"; 304 305 protected Permission createRecallPermission(String docType, String appDocStatus, String routeNode, String routeStatus) { 306 return createPermissionForTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION + " for test case", docType, appDocStatus, routeNode, routeStatus); 307 } 308 309 protected Permission createRouteDocumentPermission(String docType, String appDocStatus, String routeNode, String routeStatus) { 310 return createPermissionForTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.ROUTE_PERMISSION, KewApiConstants.KEW_NAMESPACE, KewApiConstants.ROUTE_PERMISSION + " for test case", docType, appDocStatus, routeNode, routeStatus); 311 } 312 313 protected Permission createPermissionForTemplate(String template_ns, String template_name, String permission_ns, String permission_name, String docType, String appDocStatus, String routeNode, String routeStatus) { 314 Template permTmpl = KimApiServiceLocator.getPermissionService().findPermTemplateByNamespaceCodeAndName(template_ns, template_name); 315 assertNotNull(permTmpl); 316 Permission.Builder permission = Permission.Builder.create(permission_ns, permission_name); 317 permission.setDescription(permission_name); 318 permission.setTemplate(Template.Builder.create(permTmpl)); 319 Map<String, String> attrs = new HashMap<String, String>(); 320 attrs.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, docType); 321 attrs.put(KimConstants.AttributeConstants.APP_DOC_STATUS, appDocStatus); 322 attrs.put(KimConstants.AttributeConstants.ROUTE_NODE_NAME, routeNode); 323 attrs.put(KimConstants.AttributeConstants.ROUTE_STATUS_CODE, routeStatus); 324 permission.setActive(true); 325 permission.setAttributes(attrs); 326 327 // save the permission and check that's it's wired up correctly 328 Permission perm = KimApiServiceLocator.getPermissionService().createPermission(permission.build()); 329 assertEquals(perm.getTemplate().getId(), permTmpl.getId()); 330 int num = 1; 331 if (appDocStatus != null) num++; 332 if (routeNode != null) num++; 333 if (routeStatus != null) num++; 334 assertEquals(num, perm.getAttributes().size()); 335 assertEquals(docType, perm.getAttributes().get(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME)); 336 assertEquals(appDocStatus, perm.getAttributes().get(KimConstants.AttributeConstants.APP_DOC_STATUS)); 337 assertEquals(routeNode, perm.getAttributes().get(KimConstants.AttributeConstants.ROUTE_NODE_NAME)); 338 assertEquals(routeStatus, perm.getAttributes().get(KimConstants.AttributeConstants.ROUTE_STATUS_CODE)); 339 340 return perm; 341 } 342 343 // disable the existing Recall Permission assigned to Initiator Role for test purposes 344 protected void disableInitiatorRecallPermission() { 345 Permission p = KimApiServiceLocator.getPermissionService().findPermByNamespaceCodeAndName("KR-WKFLW", "Recall Document"); 346 Permission.Builder pb = Permission.Builder.create(p); 347 pb.setActive(false); 348 KimApiServiceLocator.getPermissionService().updatePermission(pb.build()); 349 } 350 351 // setter for Kim Priority Parameter (used for useKimPermission method call) 352 protected void setKimPriorityOnDocumentTypeParameterValue(String parameterValue) { 353 if(CoreFrameworkServiceLocator.getParameterService().parameterExists(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND)) { 354 Parameter kimPriorityOverDocTypePolicyParameter = CoreFrameworkServiceLocator.getParameterService().getParameter(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND); 355 Parameter.Builder b = Parameter.Builder.create(kimPriorityOverDocTypePolicyParameter); 356 b.setValue(parameterValue); 357 CoreFrameworkServiceLocator.getParameterService().updateParameter(b.build()); 358 } 359 } 360 361 protected String getKimPriorityOnDocumentTypeParameterValue() { 362 if(CoreFrameworkServiceLocator.getParameterService().parameterExists(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND)) { 363 return CoreFrameworkServiceLocator.getParameterService().getParameter(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND).getValue(); 364 } 365 return null; 366 } 367 368 369 /** 370 * Tests that a new permission can be configured with the Recall Permission template and that matching works correctly 371 * against the new permission 372 */ 373 @Test public void testRecallPermissionMatching() { 374 disableInitiatorRecallPermission(); 375 createRecallPermission(RECALL_TEST_DOC, PERM_APP_DOC_STATUS, ROUTE_NODE, ROUTE_STATUS); 376 377 Map<String, String> details = new HashMap<String, String>(); 378 details.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, RECALL_TEST_DOC); 379 details.put(KimConstants.AttributeConstants.APP_DOC_STATUS, PERM_APP_DOC_STATUS); 380 details.put(KimConstants.AttributeConstants.ROUTE_NODE_NAME, ROUTE_NODE); 381 details.put(KimConstants.AttributeConstants.ROUTE_STATUS_CODE, ROUTE_STATUS); 382 383 // test all single field mismatches 384 for (Map.Entry<String, String> entry: details.entrySet()) { 385 Map<String, String> testDetails = new HashMap<String, String>(details); 386 // change a single detail to a non-matching value 387 testDetails.put(entry.getKey(), entry.getValue() + " BOGUS "); 388 assertFalse("non-matching " + entry.getKey() + " detail should cause template to not match", KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, testDetails)); 389 } 390 391 assertTrue("template should match details", KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, details)); 392 } 393 394 @Test public void testRecallPermissionTemplate() throws Exception { 395 WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 396 document.route(""); 397 398 // nope, technical admins can't recall 399 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 400 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 401 402 // create a recall permission for the RECALL_TEST_DOC doctype 403 Permission perm = createRecallPermission(RECALL_TEST_DOC, PERM_APP_DOC_STATUS, ROUTE_NODE, ROUTE_STATUS); 404 405 // assign the permission to Technical Administrator role 406 Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator"); 407 KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), techadmin.getId()); 408 409 // our recall permission is assigned to the technical admin role 410 411 // but the doc will not match... 412 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_NOTIFY_TEST_DOC); 413 document.route(PERM_APP_DOC_STATUS); 414 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 415 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 416 417 // .. the app doc status will not match... 418 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 419 document.route(""); 420 // technical admins can't recall since the app doc status is not correct 421 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 422 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 423 424 // ... the node will not match ... 425 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 426 document.route(""); 427 WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).approve(""); // approve past notifyfirstnode 428 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 429 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 430 431 // ... the doc status will not match (not recallable anyway) ... 432 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 433 document.route(""); 434 document.cancel("cancelled"); 435 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 436 assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 437 438 // everything should match 439 document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC); 440 document.setApplicationDocumentStatus(PERM_APP_DOC_STATUS); 441 document.route(""); 442 // now technical admins can recall by virtue of having the recall permission on this doc 443 assertTrue(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 444 assertTrue(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL)); 445 } 446 447 @Test public void testRecallToActionListAsInitiatorAfterApprovals() throws Exception { 448 this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_TEST_DOC); 449 } 450 451 @Test public void testRecallToActionListAsInitiatorWithNotificationAfterApprovals() throws Exception { 452 this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NOTIFY_TEST_DOC); 453 } 454 455 @Test public void testRecallToActionListAsInitiatorWithoutPendingNotificationAfterApprovals() throws Exception { 456 this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NO_PENDING_NOTIFY_TEST_DOC); 457 } 458 459 @Test public void testRecallToActionListAsInitiatorWithThirdPartyNotificationAfterApprovals() throws Exception { 460 this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NOTIFY_THIRDPARTY_TEST_DOC); 461 } 462 463 /** 464 * Tests that the document is returned to the *recaller*'s action list, not the original initiator 465 * @throws Exception 466 */ 467 @Test public void testRecallToActionListAsThirdParty() throws Exception { 468 Permission perm = createRecallPermission(RECALL_TEST_DOC, null, null, null); 469 // assign the permission to Technical Administrator role 470 Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator"); 471 KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), techadmin.getId()); 472 // recall as 'admin' user 473 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC); 474 } 475 476 // the three tests below test permutations of recall permission and derived role assignment 477 protected void assignRoutePermissionToTechAdmin() { 478 // assign Route Document permission to the Technical Administrator role 479 Permission routePerm = createRouteDocumentPermission(RECALL_TEST_DOC, null, null, null); 480 Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator"); 481 KimApiServiceLocator.getRoleService().assignPermissionToRole(routePerm.getId(), techadmin.getId()); 482 } 483 protected void assignRecallPermissionToDocumentRouters() { 484 // assign Recall permission to the Document Router derived role 485 Permission recallPerm = createRecallPermission(RECALL_TEST_DOC, null, null, null); 486 Role documentRouterDerivedRole = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-WKFLW", "Document Router"); 487 KimApiServiceLocator.getRoleService().assignPermissionToRole(recallPerm.getId(), documentRouterDerivedRole.getId()); 488 } 489 /** 490 * Tests that simply assigning the Route Document permission to the Technical Admin role *without* assigning the 491 * Recall permission to the Document Router derived role, is NOT sufficient to enable recall. 492 */ 493 @Test public void testRoutePermissionAssignmentInsufficientForRouterToRecallDoc() throws Exception { 494 assignRoutePermissionToTechAdmin(); 495 // recall as 'admin' (Tech Admin) user 496 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC, false); 497 } 498 /** 499 * Tests that simply assigning the recall permission to the Document Router derived role *without* assigning the 500 * Route Document permission to the Technical Admin role, is NOT sufficient to enable recall. 501 */ 502 @Test public void testRecallPermissionAssignmentInsufficientForRouterToRecallDoc() throws Exception { 503 assignRecallPermissionToDocumentRouters(); 504 // recall as 'admin' (Tech Admin) user 505 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC, false); 506 } 507 /** 508 * Tests that we can use the Route Document derived role to assign Recall permission to document routers. 509 */ 510 @Test public void testRecallToActionListAsRouterDerivedRole() throws Exception { 511 // assign both! derived role works its magic 512 assignRoutePermissionToTechAdmin(); 513 assignRecallPermissionToDocumentRouters(); 514 // recall as 'admin' user (Tech Admin) user 515 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC); 516 } 517 518 /** 519 * Creates a new role with recall permission qualified with doc type and custom app doc status 520 * @param ns role namespace 521 * @param name role name 522 * @param recallPerm the pre-created Recall permission 523 * @return the new recall-capable Role 524 */ 525 protected Role createRoleWithRecallPermission(String ns, String name, Permission recallPerm, String roleQualifierName) { 526 // create a new role 527 Role.Builder role = Role.Builder.create(); 528 role.setActive(true); 529 role.setDescription("RecallTest custom recall role"); 530 role.setName(ns); 531 role.setNamespaceCode(name); 532 role.setKimTypeId(KimApiServiceLocator.getKimTypeInfoService().findKimTypeByNameAndNamespace(KimConstants.KIM_TYPE_DEFAULT_NAMESPACE, KimConstants.KIM_TYPE_DEFAULT_NAME).getId()); 533 Role customRole = KimApiServiceLocator.getRoleService().createRole(role.build()); 534 535 // create a custom attribute for role qualification 536 Long chartAttributeId = KRADServiceLocator.getSequenceAccessorService().getNextAvailableSequenceNumber("KRIM_ATTR_DEFN_ID_S"); 537 KimAttributeBo chartAttribute = new KimAttributeBo(); 538 chartAttribute.setId("" + chartAttributeId); 539 chartAttribute.setAttributeName(roleQualifierName); 540 chartAttribute.setComponentName("org.kuali.rice.kim.bo.impl.KimAttributes"); 541 chartAttribute.setNamespaceCode("KR-SYS"); 542 chartAttribute.setAttributeLabel(roleQualifierName); 543 chartAttribute.setActive(true); 544 KRADServiceLocator.getBusinessObjectService().save(chartAttribute); 545 546 KimApiServiceLocator.getRoleService().assignPermissionToRole(recallPerm.getId(), customRole.getId()); 547 548 List<String> recallCapableRoleIds = KimApiServiceLocator.getPermissionService().getRoleIdsForPermission(recallPerm.getNamespaceCode(), recallPerm.getName()); 549 Assert.assertFalse("No recall-capable roles found", recallCapableRoleIds.isEmpty()); 550 Assert.assertTrue("New role is not associated with Recall permission", recallCapableRoleIds.contains(customRole.getId())); 551 552 return customRole; 553 } 554 555 /** 556 * Assigns user to role with single qualification 557 * @param principalId the principal to assign to role 558 * @param role the role object 559 * @param roleQualifierName the role qualifier name 560 * @param roleQualifierValue the role qualifier value 561 */ 562 protected void assignUserQualifiedRole(String principalId, Role role, String roleQualifierName, String roleQualifierValue) { 563 // assign user to role triggered by dynamic, custom role qualifications 564 Map<String, String> qualifications = new HashMap<String, String>(); 565 qualifications.put(roleQualifierName, roleQualifierValue); 566 KimApiServiceLocator.getRoleService().assignPrincipalToRole(getPrincipalIdForName("arh14"), role.getNamespaceCode(), role.getName(), qualifications); 567 568 Collection<String> ids = KimApiServiceLocator.getRoleService().getRoleMemberPrincipalIds(role.getNamespaceCode(), role.getName(), qualifications); 569 Assert.assertTrue("Qualified role assignment failed", ids.contains(principalId)); 570 } 571 572 /** 573 * Tests that an application can customize document type routing authorization via documenttypeauthorizer 574 */ 575 @Test public void testRecallWithCustomDocumentTypeAuthorizer() throws Exception { 576 // arh14 is not associated with our doc routing, will be authorized by custom documenttypeauthorizer 577 final String ARH14 = getPrincipalIdForName("arh14"); 578 579 // remove existing initiator recall permission 580 disableInitiatorRecallPermission(); 581 582 RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked = false; 583 RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked = false; 584 585 // confirm arh14 can't recall doc 586 testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("arh14"), RECALL_TEST_DOC, false); 587 588 final String RECALL_ROLE_NM = "CustomRecall"; 589 final String RECALL_ROLE_NS = "KR-SYS"; 590 591 // assign permission triggered by dynamic, custom permission details 592 Permission recallPerm = createRecallPermission(RECALL_TEST_DOC, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE, null, null); 593 Role recallRole = createRoleWithRecallPermission(RECALL_ROLE_NM, RECALL_ROLE_NS, recallPerm, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME); 594 assignUserQualifiedRole(ARH14, recallRole, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE); 595 596 Map<String, String> d = new HashMap<String, String>(); 597 d.put(RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE); 598 d.put(KewApiConstants.DOCUMENT_TYPE_NAME_DETAIL, RECALL_TEST_DOC); 599 d.put(KewApiConstants.ROUTE_NODE_NAME_DETAIL, ROUTE_NODE); 600 d.put(KewApiConstants.DOCUMENT_STATUS_DETAIL, ROUTE_STATUS); 601 Map<String, String> q = new HashMap<String, String>(); 602 q.put(RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE); 603 // test that arh14 has recall permission via new recall role with proper qualifications 604 List<Permission> permissions = KimApiServiceLocator.getPermissionService().getAuthorizedPermissionsByTemplate(ARH14, KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, d, q); 605 Assert.assertEquals(1, permissions.size()); 606 Assert.assertEquals(recallPerm.getId(), permissions.get(0).getId()); 607 608 // verify that arh14 *still* can't recall doc - we have to set the custom documenttypeauthorizer first 609 testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false); 610 611 // now update the doctype with custom documenttype authorizer 612 org.kuali.rice.kew.api.doctype.DocumentType dt = KewApiServiceLocator.getDocumentTypeService().getDocumentTypeByName(RECALL_TEST_DOC); 613 org.kuali.rice.kew.api.doctype.DocumentType.Builder b = org.kuali.rice.kew.api.doctype.DocumentType.Builder.create(dt); 614 b.setAuthorizer(RecallTestDocumentTypeAuthorizer.class.getName()); 615 616 KEWServiceLocator.getDocumentTypeService().save(DocumentType.from(b)); 617 618 Assert.assertEquals(RecallTestDocumentTypeAuthorizer.class.getName(), KewApiServiceLocator.getDocumentTypeService().getDocumentTypeByName(RECALL_TEST_DOC).getAuthorizer()); 619 620 // custom documenttypeauthorizer has not been invoked yet 621 Assert.assertFalse(RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked); 622 Assert.assertFalse(RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked); 623 624 // arh14 should *now* be able to recall! 625 testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC); 626 627 Assert.assertTrue(RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked); 628 Assert.assertTrue(RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked); 629 630 // final counter tests - change the actual dynamic values to ensure match fails 631 String orig = RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE; 632 try { 633 RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE = "I will not match"; 634 testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false); 635 } finally { 636 RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE = orig; 637 } 638 639 orig = RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE; 640 try { 641 RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE = "I won't match either"; 642 testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false); 643 } finally { 644 RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE = orig; 645 } 646 } 647 648 protected void testRecallToActionListAsInitiatorAfterApprovals(String doctype) { 649 testRecallToActionListAfterApprovals(EWESTFAL, EWESTFAL, doctype); 650 } 651 652 // Implements various permutations of recalls - with and without doctype policies/notifications of various sorts 653 // and as initiator or a third party recaller 654 protected void testRecallToActionListAfterApprovals(String initiator, String recaller, String doctype) { 655 testRecallToActionListAfterApprovals(initiator, recaller, doctype, true); 656 } 657 protected void testRecallToActionListAfterApprovals(String initiator, String recaller, String doctype, boolean expect_recall_success) { 658 boolean notifyPreviousRecipients = !RECALL_TEST_DOC.equals(doctype); 659 boolean notifyPendingRecipients = !RECALL_NO_PENDING_NOTIFY_TEST_DOC.equals(doctype); 660 String[] thirdPartiesNotified = RECALL_NOTIFY_THIRDPARTY_TEST_DOC.equals(doctype) ? new String[] { "quickstart", "admin" } : new String[] {}; 661 662 WorkflowDocument document = WorkflowDocumentFactory.createDocument(initiator, doctype); 663 document.route(""); 664 665 WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).approve(""); 666 WorkflowDocumentFactory.loadDocument(initiator, document.getDocumentId()).approve(""); 667 WorkflowDocumentFactory.loadDocument(RKIRKEND, document.getDocumentId()).approve(""); 668 669 document = WorkflowDocumentFactory.loadDocument(recaller, document.getDocumentId()); 670 System.err.println(document.getValidActions().getValidActions()); 671 if (expect_recall_success) { 672 assertTrue("recaller '" + recaller + "' should be able to RECALL", document.getValidActions().getValidActions().contains(ActionType.RECALL)); 673 } else { 674 assertFalse("recaller '" + recaller + "' should NOT be able to RECALL", document.getValidActions().getValidActions().contains(ActionType.RECALL)); 675 return; 676 } 677 document.recall("recalling", false); 678 679 assertTrue("Document should be saved", document.isSaved()); 680 assertAfterActionTakenCalled(ActionType.RECALL, ActionType.COMPLETE); 681 682 // the recaller has a completion request 683 assertTrue(document.isCompletionRequested()); 684 685 // pending approver has FYI 686 assertEquals(notifyPendingRecipients, WorkflowDocumentFactory.loadDocument(NATJOHNS, document.getDocumentId()).isFYIRequested()); 687 // third approver has FYI 688 assertEquals(notifyPreviousRecipients, WorkflowDocumentFactory.loadDocument(RKIRKEND, document.getDocumentId()).isFYIRequested()); 689 // second approver does not have FYI - approver is initiator, FYI is skipped 690 assertFalse(WorkflowDocumentFactory.loadDocument(initiator, document.getDocumentId()).isFYIRequested()); 691 // first approver has FYI 692 assertEquals(notifyPreviousRecipients, WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isFYIRequested()); 693 694 if (!ArrayUtils.isEmpty(thirdPartiesNotified)) { 695 for (String recipient: thirdPartiesNotified) { 696 assertTrue("Expected FYI to be sent to: " + recipient, WorkflowDocumentFactory.loadDocument(getPrincipalIdForName(recipient), document.getDocumentId()).isFYIRequested()); 697 } 698 } 699 700 // omit JHOPF, and see if FYI is subsumed by approval request 701 for (String user: new String[] { RKIRKEND, NATJOHNS }) { 702 WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()).fyi(); 703 } 704 705 document.complete("completing"); 706 707 assertTrue("Document should be enroute", document.isEnroute()); 708 709 // generation of approval requests nullify FYIs (?) 710 // if JHOPF had an FYI, he doesn't any longer 711 for (String user: new String[] { JHOPF, RKIRKEND, NATJOHNS }) { 712 document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()); 713 assertFalse(getPrincipalNameForId(user) + " should not have an FYI", document.isFYIRequested()); 714 } 715 716 // submit all approvals 717 for (String user: new String[] { JHOPF, initiator, RKIRKEND, NATJOHNS, BMCGOUGH }) { 718 document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()); 719 assertTrue(getPrincipalNameForId(user) + " should have approval request", document.isApprovalRequested()); 720 document.approve("approving"); 721 } 722 723 // 2 acks outstanding, we're PROCESSED 724 assertTrue("Document should be processed", document.isProcessed()); 725 assertTrue("Document should be approved", document.isApproved()); 726 727 document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("xqi"), document.getDocumentId()); 728 document.acknowledge(""); 729 730 document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jthomas"), document.getDocumentId()); 731 document.fyi(); 732 733 assertTrue("Document should be approved", document.isApproved()); 734 assertTrue("Document should be final", document.isFinal()); 735 } 736 }