View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.core.logic;
20  
21  import java.lang.reflect.Method;
22  import java.time.OffsetDateTime;
23  import java.util.ArrayList;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  import javax.ws.rs.core.Response;
31  import org.apache.commons.lang3.ArrayUtils;
32  import org.apache.commons.lang3.tuple.Pair;
33  import org.apache.commons.lang3.tuple.Triple;
34  import org.apache.syncope.common.lib.SyncopeClientException;
35  import org.apache.syncope.common.lib.form.SyncopeForm;
36  import org.apache.syncope.common.lib.to.ExecTO;
37  import org.apache.syncope.common.lib.to.JobTO;
38  import org.apache.syncope.common.lib.to.MacroTaskTO;
39  import org.apache.syncope.common.lib.to.PropagationTaskTO;
40  import org.apache.syncope.common.lib.to.SchedTaskTO;
41  import org.apache.syncope.common.lib.to.TaskTO;
42  import org.apache.syncope.common.lib.types.AnyTypeKind;
43  import org.apache.syncope.common.lib.types.ClientExceptionType;
44  import org.apache.syncope.common.lib.types.ExecStatus;
45  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
46  import org.apache.syncope.common.lib.types.JobAction;
47  import org.apache.syncope.common.lib.types.JobType;
48  import org.apache.syncope.common.lib.types.TaskType;
49  import org.apache.syncope.common.rest.api.RESTHeaders;
50  import org.apache.syncope.common.rest.api.batch.BatchResponseItem;
51  import org.apache.syncope.common.rest.api.beans.ExecSpecs;
52  import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
53  import org.apache.syncope.core.persistence.api.dao.JobStatusDAO;
54  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
55  import org.apache.syncope.core.persistence.api.dao.NotificationDAO;
56  import org.apache.syncope.core.persistence.api.dao.TaskDAO;
57  import org.apache.syncope.core.persistence.api.dao.TaskExecDAO;
58  import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
59  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
60  import org.apache.syncope.core.persistence.api.entity.Notification;
61  import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
62  import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
63  import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
64  import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
65  import org.apache.syncope.core.persistence.api.entity.task.Task;
66  import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
67  import org.apache.syncope.core.persistence.api.entity.task.TaskUtils;
68  import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory;
69  import org.apache.syncope.core.provisioning.api.data.TaskDataBinder;
70  import org.apache.syncope.core.provisioning.api.job.JobManager;
71  import org.apache.syncope.core.provisioning.api.job.JobNamer;
72  import org.apache.syncope.core.provisioning.api.notification.NotificationJobDelegate;
73  import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
74  import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
75  import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
76  import org.apache.syncope.core.provisioning.java.job.MacroJobDelegate;
77  import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
78  import org.apache.syncope.core.spring.security.AuthContextUtils;
79  import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
80  import org.identityconnectors.framework.common.objects.ObjectClass;
81  import org.quartz.JobDataMap;
82  import org.quartz.JobKey;
83  import org.quartz.SchedulerException;
84  import org.springframework.dao.InvalidDataAccessApiUsageException;
85  import org.springframework.scheduling.quartz.SchedulerFactoryBean;
86  import org.springframework.security.access.prepost.PreAuthorize;
87  import org.springframework.transaction.annotation.Transactional;
88  
89  public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
90  
91      protected final TaskDAO taskDAO;
92  
93      protected final TaskExecDAO taskExecDAO;
94  
95      protected final ExternalResourceDAO resourceDAO;
96  
97      protected final NotificationDAO notificationDAO;
98  
99      protected final TaskDataBinder binder;
100 
101     protected final PropagationTaskExecutor taskExecutor;
102 
103     protected final NotificationJobDelegate notificationJobDelegate;
104 
105     protected final TaskUtilsFactory taskUtilsFactory;
106 
107     public TaskLogic(
108             final JobManager jobManager,
109             final SchedulerFactoryBean scheduler,
110             final JobStatusDAO jobStatusDAO,
111             final TaskDAO taskDAO,
112             final TaskExecDAO taskExecDAO,
113             final ExternalResourceDAO resourceDAO,
114             final NotificationDAO notificationDAO,
115             final TaskDataBinder binder,
116             final PropagationTaskExecutor taskExecutor,
117             final NotificationJobDelegate notificationJobDelegate,
118             final TaskUtilsFactory taskUtilsFactory) {
119 
120         super(jobManager, scheduler, jobStatusDAO);
121 
122         this.taskDAO = taskDAO;
123         this.taskExecDAO = taskExecDAO;
124         this.resourceDAO = resourceDAO;
125         this.notificationDAO = notificationDAO;
126         this.binder = binder;
127         this.taskExecutor = taskExecutor;
128         this.notificationJobDelegate = notificationJobDelegate;
129         this.taskUtilsFactory = taskUtilsFactory;
130     }
131 
132     protected void securityChecks(final String entitlement, final String realm) {
133         Set<String> authRealms = AuthContextUtils.getAuthorizations().get(entitlement);
134         if (authRealms.stream().noneMatch(r -> realm.startsWith(r))) {
135             throw new DelegatedAdministrationException(realm, MacroTask.class.getSimpleName(), null);
136         }
137     }
138 
139     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_CREATE + "')")
140     public <T extends SchedTaskTO> T createSchedTask(final TaskType type, final T taskTO) {
141         TaskUtils taskUtils = taskUtilsFactory.getInstance(taskTO);
142         if (taskUtils.getType() != type) {
143             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidRequest);
144             sce.getElements().add("Found " + type + ", expected " + taskUtils.getType());
145             throw sce;
146         }
147 
148         if (taskUtils.getType() == TaskType.MACRO) {
149             securityChecks(IdRepoEntitlement.TASK_CREATE, ((MacroTaskTO) taskTO).getRealm());
150         }
151 
152         SchedTask task = binder.createSchedTask(taskTO, taskUtils);
153         task = taskDAO.save(task);
154 
155         try {
156             jobManager.register(
157                     task,
158                     task.getStartAt(),
159                     AuthContextUtils.getUsername());
160         } catch (Exception e) {
161             LOG.error("While registering quartz job for task " + task.getKey(), e);
162 
163             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
164             sce.getElements().add(e.getMessage());
165             throw sce;
166         }
167 
168         return binder.getTaskTO(task, taskUtils, false);
169     }
170 
171     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_UPDATE + "')")
172     public <T extends SchedTaskTO> T updateSchedTask(final TaskType type, final SchedTaskTO taskTO) {
173         SchedTask task = taskDAO.find(type, taskTO.getKey());
174         if (task == null) {
175             throw new NotFoundException("Task " + taskTO.getKey());
176         }
177 
178         TaskUtils taskUtils = taskUtilsFactory.getInstance(task);
179         if (taskUtils.getType() != type) {
180             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidRequest);
181             sce.getElements().add("Found " + type + ", expected " + taskUtils.getType());
182             throw sce;
183         }
184 
185         if (taskUtils.getType() == TaskType.MACRO) {
186             securityChecks(IdRepoEntitlement.TASK_UPDATE, ((MacroTask) task).getRealm().getFullPath());
187             securityChecks(IdRepoEntitlement.TASK_UPDATE, ((MacroTaskTO) taskTO).getRealm());
188         }
189 
190         binder.updateSchedTask(task, taskTO, taskUtils);
191         task = taskDAO.save(task);
192         try {
193             jobManager.register(
194                     task,
195                     task.getStartAt(),
196                     AuthContextUtils.getUsername());
197         } catch (Exception e) {
198             LOG.error("While registering quartz job for task " + task.getKey(), e);
199 
200             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
201             sce.getElements().add(e.getMessage());
202             throw sce;
203         }
204 
205         return binder.getTaskTO(task, taskUtils, false);
206     }
207 
208     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_LIST + "')")
209     @Transactional(readOnly = true)
210     public <T extends TaskTO> Pair<Integer, List<T>> search(
211             final TaskType type,
212             final String resource,
213             final String notification,
214             final AnyTypeKind anyTypeKind,
215             final String entityKey,
216             final int page,
217             final int size,
218             final List<OrderByClause> orderByClauses,
219             final boolean details) {
220 
221         try {
222             if (type == null) {
223                 throw new IllegalArgumentException("type is required");
224             }
225 
226             ExternalResource resourceObj = resourceDAO.find(resource);
227             if (resource != null && resourceObj == null) {
228                 throw new IllegalArgumentException("Missing External Resource: " + resource);
229             }
230 
231             Notification notificationObj = notificationDAO.find(notification);
232             if (notification != null && notificationObj == null) {
233                 throw new IllegalArgumentException("Missing Notification: " + notification);
234             }
235 
236             int count = taskDAO.count(
237                     type,
238                     resourceObj,
239                     notificationObj,
240                     anyTypeKind,
241                     entityKey);
242 
243             List<T> result = taskDAO.findAll(
244                     type,
245                     resourceObj,
246                     notificationObj,
247                     anyTypeKind,
248                     entityKey,
249                     page,
250                     size,
251                     orderByClauses).stream().
252                     <T>map(task -> binder.getTaskTO(task, taskUtilsFactory.getInstance(type), details)).
253                     collect(Collectors.toList());
254 
255             return Pair.of(count, result);
256         } catch (IllegalArgumentException | InvalidDataAccessApiUsageException e) {
257             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidRequest);
258             sce.getElements().add(e.getMessage());
259             throw sce;
260         }
261     }
262 
263     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_READ + "')")
264     @Transactional(readOnly = true)
265     public <T extends TaskTO> T read(final TaskType type, final String key, final boolean details) {
266         Task<?> task = taskDAO.find(type, key);
267         if (task == null) {
268             throw new NotFoundException("Task " + key);
269         }
270 
271         TaskUtils taskUtils = taskUtilsFactory.getInstance(task);
272         if (type != null && taskUtils.getType() != type) {
273             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidRequest);
274             sce.getElements().add("Found " + type + ", expected " + taskUtils.getType());
275             throw sce;
276         }
277 
278         if (taskUtils.getType() == TaskType.MACRO) {
279             securityChecks(IdRepoEntitlement.TASK_READ, ((MacroTask) task).getRealm().getFullPath());
280         }
281 
282         return binder.getTaskTO(task, taskUtilsFactory.getInstance(task), details);
283     }
284 
285     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_READ + "')")
286     @Transactional(readOnly = true)
287     public SyncopeForm getMacroTaskForm(final String key) {
288         MacroTask task = taskDAO.find(key).
289                 filter(MacroTask.class::isInstance).map(MacroTask.class::cast).
290                 orElseThrow(() -> new NotFoundException("MacroTask " + key));
291 
292         securityChecks(IdRepoEntitlement.TASK_READ, task.getRealm().getFullPath());
293 
294         return binder.getMacroTaskForm(task);
295     }
296 
297     protected ExecTO doExecute(
298             final Task<?> task,
299             final OffsetDateTime startAt,
300             final Map<String, Object> additionalDataMap) {
301 
302         if (startAt != null && startAt.isBefore(OffsetDateTime.now())) {
303             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
304             sce.getElements().add("Cannot schedule in the past");
305             throw sce;
306         }
307 
308         TaskUtils taskUtils = taskUtilsFactory.getInstance(task);
309         String executor = AuthContextUtils.getUsername();
310 
311         ExecTO result = null;
312         switch (taskUtils.getType()) {
313             case PROPAGATION:
314                 PropagationTask propagationTask = (PropagationTask) task;
315                 PropagationTaskInfo taskInfo = new PropagationTaskInfo(
316                         propagationTask.getResource(),
317                         propagationTask.getOperation(),
318                         new ObjectClass(propagationTask.getObjectClassName()),
319                         propagationTask.getAnyTypeKind(),
320                         propagationTask.getAnyType(),
321                         propagationTask.getEntityKey(),
322                         propagationTask.getConnObjectKey(),
323                         propagationTask.getPropagationData());
324                 taskInfo.setKey(propagationTask.getKey());
325                 taskInfo.setOldConnObjectKey(propagationTask.getOldConnObjectKey());
326 
327                 TaskExec<PropagationTask> propExec = taskExecutor.execute(
328                         taskInfo, new DefaultPropagationReporter(), executor);
329                 result = binder.getExecTO(propExec);
330                 break;
331 
332             case NOTIFICATION:
333                 TaskExec<NotificationTask> notExec = notificationJobDelegate.executeSingle(
334                         (NotificationTask) task, executor);
335                 result = binder.getExecTO(notExec);
336                 break;
337 
338             case SCHEDULED:
339             case PULL:
340             case PUSH:
341             case MACRO:
342                 if (!((SchedTask) task).isActive()) {
343                     SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
344                     sce.getElements().add("Task " + task.getKey() + " is not active");
345                     throw sce;
346                 }
347 
348                 if (taskUtils.getType() == TaskType.MACRO) {
349                     securityChecks(IdRepoEntitlement.TASK_EXECUTE, ((MacroTask) task).getRealm().getFullPath());
350                 }
351 
352                 try {
353                     Map<String, Object> jobDataMap = jobManager.register(
354                             (SchedTask) task,
355                             startAt,
356                             executor);
357                     jobDataMap.putAll(additionalDataMap);
358 
359                     if (startAt == null) {
360                         scheduler.getScheduler().triggerJob(JobNamer.getJobKey(task), new JobDataMap(jobDataMap));
361                     }
362                 } catch (Exception e) {
363                     LOG.error("While executing task {}", task, e);
364 
365                     SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
366                     sce.getElements().add(e.getMessage());
367                     throw sce;
368                 }
369 
370                 result = new ExecTO();
371                 result.setJobType(JobType.TASK);
372                 result.setRefKey(task.getKey());
373                 result.setRefDesc(binder.buildRefDesc(task));
374                 result.setStart(OffsetDateTime.now());
375                 result.setExecutor(executor);
376                 result.setStatus("JOB_FIRED");
377                 result.setMessage("Job fired; waiting for results...");
378                 break;
379 
380             default:
381         }
382 
383         return result;
384     }
385 
386     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
387     @Override
388     public ExecTO execute(final ExecSpecs specs) {
389         Task<?> task = taskDAO.find(specs.getKey()).
390                 orElseThrow(() -> new NotFoundException("Task " + specs.getKey()));
391 
392         return doExecute(
393                 task,
394                 specs.getStartAt(),
395                 Map.of(JobManager.DRY_RUN_JOBDETAIL_KEY, specs.getDryRun()));
396     }
397 
398     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
399     public ExecTO execute(final ExecSpecs specs, final SyncopeForm macroTaskForm) {
400         MacroTask task = taskDAO.find(specs.getKey()).
401                 filter(MacroTask.class::isInstance).map(MacroTask.class::cast).
402                 orElseThrow(() -> new NotFoundException("MacroTask " + specs.getKey()));
403 
404         return doExecute(
405                 task,
406                 specs.getStartAt(),
407                 Map.of(JobManager.DRY_RUN_JOBDETAIL_KEY, specs.getDryRun(),
408                         MacroJobDelegate.MACRO_TASK_FORM_JOBDETAIL_KEY, macroTaskForm));
409     }
410 
411     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_DELETE + "')")
412     public <T extends TaskTO> T delete(final TaskType type, final String key) {
413         Task<?> task = taskDAO.find(type, key);
414         if (task == null) {
415             throw new NotFoundException("Task " + key);
416         }
417 
418         TaskUtils taskUtils = taskUtilsFactory.getInstance(task);
419         if (type != null && taskUtils.getType() != type) {
420             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidRequest);
421             sce.getElements().add("Found " + type + ", expected " + taskUtils.getType());
422             throw sce;
423         }
424 
425         if (taskUtils.getType() == TaskType.MACRO) {
426             securityChecks(IdRepoEntitlement.TASK_DELETE, ((MacroTask) task).getRealm().getFullPath());
427         }
428 
429         T taskToDelete = binder.getTaskTO(task, taskUtils, true);
430 
431         if (TaskType.SCHEDULED == taskUtils.getType()
432                 || TaskType.PULL == taskUtils.getType()
433                 || TaskType.PUSH == taskUtils.getType()
434                 || TaskType.MACRO == taskUtils.getType()) {
435 
436             jobManager.unregister(task);
437         }
438 
439         taskDAO.delete(task);
440         return taskToDelete;
441     }
442 
443     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_READ + "')")
444     @Override
445     public Pair<Integer, List<ExecTO>> listExecutions(
446             final String key,
447             final OffsetDateTime before,
448             final OffsetDateTime after,
449             final int page,
450             final int size,
451             final List<OrderByClause> orderByClauses) {
452 
453         Task<?> task = taskDAO.find(key).orElseThrow(() -> new NotFoundException("Task " + key));
454 
455         if (task instanceof MacroTask) {
456             securityChecks(IdRepoEntitlement.TASK_READ, ((MacroTask) task).getRealm().getFullPath());
457         }
458 
459         Integer count = taskExecDAO.count(task, before, after);
460 
461         List<ExecTO> result = taskExecDAO.findAll(task, before, after, page, size, orderByClauses).stream().
462                 map(exec -> binder.getExecTO(exec)).collect(Collectors.toList());
463 
464         return Pair.of(count, result);
465     }
466 
467     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_LIST + "')")
468     @Override
469     public List<ExecTO> listRecentExecutions(final int max) {
470         return taskExecDAO.findRecent(max).stream().
471                 map(exec -> {
472                     try {
473                         if (exec.getTask() instanceof MacroTask) {
474                             securityChecks(IdRepoEntitlement.TASK_DELETE,
475                                     ((MacroTask) exec.getTask()).getRealm().getFullPath());
476                         }
477 
478                         return binder.getExecTO(exec);
479                     } catch (DelegatedAdministrationException e) {
480                         LOG.error("Skip executions for command task", e);
481                         return null;
482                     }
483                 }).
484                 filter(Objects::nonNull).
485                 collect(Collectors.toList());
486     }
487 
488     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_DELETE + "')")
489     @Override
490     public ExecTO deleteExecution(final String execKey) {
491         TaskExec<?> exec = taskExecDAO.find(execKey).
492                 orElseThrow(() -> new NotFoundException("Task execution " + execKey));
493 
494         if (exec.getTask() instanceof MacroTask) {
495             securityChecks(IdRepoEntitlement.TASK_DELETE, ((MacroTask) exec.getTask()).getRealm().getFullPath());
496         }
497 
498         ExecTO executionToDelete = binder.getExecTO(exec);
499         taskExecDAO.delete(exec);
500         return executionToDelete;
501     }
502 
503     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_DELETE + "')")
504     @Override
505     public List<BatchResponseItem> deleteExecutions(
506             final String key,
507             final OffsetDateTime before,
508             final OffsetDateTime after) {
509 
510         Task<?> task = taskDAO.find(key).orElseThrow(() -> new NotFoundException("Task " + key));
511 
512         List<BatchResponseItem> batchResponseItems = new ArrayList<>();
513 
514         taskExecDAO.findAll(task, before, after, -1, -1, List.of()).forEach(exec -> {
515             BatchResponseItem item = new BatchResponseItem();
516             item.getHeaders().put(RESTHeaders.RESOURCE_KEY, List.of(exec.getKey()));
517             batchResponseItems.add(item);
518 
519             try {
520                 if (exec.getTask() instanceof MacroTask) {
521                     securityChecks(IdRepoEntitlement.TASK_DELETE,
522                             ((MacroTask) exec.getTask()).getRealm().getFullPath());
523                 }
524 
525                 taskExecDAO.delete(exec);
526                 item.setStatus(Response.Status.OK.getStatusCode());
527             } catch (Exception e) {
528                 LOG.error("Error deleting execution {} of task {}", exec.getKey(), key, e);
529                 item.setStatus(Response.Status.BAD_REQUEST.getStatusCode());
530                 item.setContent(ExceptionUtils2.getFullStackTrace(e));
531             }
532         });
533 
534         return batchResponseItems;
535     }
536 
537     @Override
538     protected Triple<JobType, String, String> getReference(final JobKey jobKey) {
539         String key = JobNamer.getTaskKeyFromJobName(jobKey.getName());
540 
541         Task<?> task = taskDAO.find(key).orElse(null);
542         return task == null || !(task instanceof SchedTask)
543                 ? null
544                 : Triple.of(JobType.TASK, key, binder.buildRefDesc(task));
545     }
546 
547     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_LIST + "')")
548     @Override
549     public List<JobTO> listJobs() {
550         return super.doListJobs(true);
551     }
552 
553     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_READ + "')")
554     @Override
555     public JobTO getJob(final String key) {
556         Task<?> task = taskDAO.find(key).orElseThrow(() -> new NotFoundException("Task " + key));
557 
558         if (task instanceof MacroTask) {
559             securityChecks(IdRepoEntitlement.TASK_READ, ((MacroTask) task).getRealm().getFullPath());
560         }
561 
562         JobTO jobTO = null;
563         try {
564             jobTO = getJobTO(JobNamer.getJobKey(task), false);
565         } catch (SchedulerException e) {
566             LOG.error("Problems while retrieving scheduled job {}", JobNamer.getJobKey(task), e);
567 
568             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
569             sce.getElements().add(e.getMessage());
570             throw sce;
571         }
572         if (jobTO == null) {
573             throw new NotFoundException("Job for task " + key);
574         }
575         return jobTO;
576     }
577 
578     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
579     @Override
580     public void actionJob(final String key, final JobAction action) {
581         Task<?> task = taskDAO.find(key).orElseThrow(() -> new NotFoundException("Task " + key));
582 
583         if (task instanceof MacroTask) {
584             securityChecks(IdRepoEntitlement.TASK_EXECUTE, ((MacroTask) task).getRealm().getFullPath());
585         }
586 
587         doActionJob(JobNamer.getJobKey(task), action);
588     }
589 
590     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_DELETE + "')")
591     public List<PropagationTaskTO> purgePropagations(
592             final OffsetDateTime since,
593             final List<ExecStatus> statuses,
594             final List<String> resources) {
595 
596         return taskDAO.purgePropagations(since, statuses, Optional.ofNullable(resources).
597                 map(r -> r.stream().map(resourceDAO::find).
598                 filter(Objects::nonNull).collect(Collectors.toList())).
599                 orElse(null));
600     }
601 
602     @Override
603     protected TaskTO resolveReference(final Method method, final Object... args)
604             throws UnresolvedReferenceException {
605 
606         String key = null;
607 
608         if (ArrayUtils.isNotEmpty(args)
609                 && !"deleteExecution".equals(method.getName()) && !"readExecution".equals(method.getName())) {
610 
611             for (int i = 0; key == null && i < args.length; i++) {
612                 if (args[i] instanceof String) {
613                     key = (String) args[i];
614                 } else if (args[i] instanceof TaskTO) {
615                     key = ((TaskTO) args[i]).getKey();
616                 }
617             }
618         }
619 
620         if (key != null) {
621             String taskKey = key;
622             try {
623                 Task<?> task = taskDAO.find(taskKey).orElseThrow(() -> new NotFoundException("Task " + taskKey));
624                 return binder.getTaskTO(task, taskUtilsFactory.getInstance(task), false);
625             } catch (Throwable ignore) {
626                 LOG.debug("Unresolved reference", ignore);
627                 throw new UnresolvedReferenceException(ignore);
628             }
629         }
630 
631         throw new UnresolvedReferenceException();
632     }
633 }