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 }