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.flowable.impl;
20  
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Objects;
26  import java.util.Optional;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  import org.apache.commons.lang3.tuple.Pair;
30  import org.apache.syncope.common.lib.request.UserCR;
31  import org.apache.syncope.common.lib.request.UserUR;
32  import org.apache.syncope.common.lib.to.WorkflowTask;
33  import org.apache.syncope.common.lib.to.WorkflowTaskExecInput;
34  import org.apache.syncope.common.lib.types.ResourceOperation;
35  import org.apache.syncope.core.flowable.api.UserRequestHandler;
36  import org.apache.syncope.core.flowable.api.WorkflowTaskManager;
37  import org.apache.syncope.core.flowable.support.DomainProcessEngine;
38  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
39  import org.apache.syncope.core.persistence.api.dao.RealmDAO;
40  import org.apache.syncope.core.persistence.api.dao.UserDAO;
41  import org.apache.syncope.core.persistence.api.entity.EntityFactory;
42  import org.apache.syncope.core.persistence.api.entity.user.User;
43  import org.apache.syncope.core.provisioning.api.PropagationByResource;
44  import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
45  import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
46  import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent;
47  import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
48  import org.apache.syncope.core.spring.security.AuthContextUtils;
49  import org.apache.syncope.core.spring.security.SecurityProperties;
50  import org.apache.syncope.core.workflow.api.WorkflowException;
51  import org.apache.syncope.core.workflow.java.AbstractUserWorkflowAdapter;
52  import org.flowable.bpmn.model.FlowElement;
53  import org.flowable.bpmn.model.Gateway;
54  import org.flowable.bpmn.model.Process;
55  import org.flowable.bpmn.model.SequenceFlow;
56  import org.flowable.common.engine.api.FlowableException;
57  import org.flowable.engine.runtime.ProcessInstance;
58  import org.flowable.task.api.Task;
59  import org.identityconnectors.framework.common.objects.SyncDeltaType;
60  import org.springframework.beans.BeanUtils;
61  import org.springframework.context.ApplicationEventPublisher;
62  
63  public class FlowableUserWorkflowAdapter extends AbstractUserWorkflowAdapter implements WorkflowTaskManager {
64  
65      protected final DomainProcessEngine engine;
66  
67      protected final UserRequestHandler userRequestHandler;
68  
69      public FlowableUserWorkflowAdapter(
70              final UserDataBinder dataBinder,
71              final UserDAO userDAO,
72              final RealmDAO realmDAO,
73              final GroupDAO groupDAO,
74              final EntityFactory entityFactory,
75              final SecurityProperties securityProperties,
76              final RuleEnforcer ruleEnforcer,
77              final DomainProcessEngine engine,
78              final UserRequestHandler userRequestHandler,
79              final ApplicationEventPublisher publisher) {
80  
81          super(dataBinder, userDAO, realmDAO, groupDAO, entityFactory, securityProperties, ruleEnforcer, publisher);
82          this.engine = engine;
83          this.userRequestHandler = userRequestHandler;
84      }
85  
86      @Override
87      public String getPrefix() {
88          return "ACT_";
89      }
90  
91      @Override
92      public <T> T getVariable(final String executionId, final String variableName, final Class<T> variableClass) {
93          return engine.getRuntimeService().getVariable(executionId, variableName, variableClass);
94      }
95  
96      @Override
97      public void setVariable(final String executionId, final String variableName, final Object value) {
98          engine.getRuntimeService().setVariable(executionId, variableName, value);
99      }
100 
101     protected User lazyLoad(final User user) {
102         // using BeanUtils to access all user's properties and trigger lazy loading - we are about to
103         // serialize a User instance for availability within workflow tasks, and this breaks transactions
104         BeanUtils.copyProperties(user, entityFactory.newEntity(User.class));
105         return user;
106     }
107 
108     @Override
109     protected UserWorkflowResult<Pair<String, Boolean>> doCreate(
110             final UserCR userCR,
111             final boolean disablePwdPolicyCheck,
112             final Boolean enabled,
113             final String creator,
114             final String context) {
115 
116         Map<String, Object> variables = new HashMap<>();
117         variables.put(FlowableRuntimeUtils.WF_EXECUTOR, AuthContextUtils.getUsername());
118         variables.put(FlowableRuntimeUtils.USER_CR, userCR);
119         variables.put(FlowableRuntimeUtils.ENABLED, enabled);
120 
121         ProcessInstance procInst = null;
122         try {
123             procInst = engine.getRuntimeService().
124                     startProcessInstanceByKey(FlowableRuntimeUtils.WF_PROCESS_ID, variables);
125         } catch (FlowableException e) {
126             FlowableRuntimeUtils.throwException(
127                     e, "While starting " + FlowableRuntimeUtils.WF_PROCESS_ID + " instance");
128         }
129 
130         engine.getRuntimeService().removeVariable(
131                 Objects.requireNonNull(procInst).getProcessInstanceId(), FlowableRuntimeUtils.WF_EXECUTOR);
132         engine.getRuntimeService().removeVariable(
133                 procInst.getProcessInstanceId(), FlowableRuntimeUtils.USER_CR);
134         engine.getRuntimeService().removeVariable(
135                 procInst.getProcessInstanceId(), FlowableRuntimeUtils.USER_TO);
136 
137         User user = engine.getRuntimeService().
138                 getVariable(procInst.getProcessInstanceId(), FlowableRuntimeUtils.USER, User.class);
139         engine.getRuntimeService().removeVariable(
140                 procInst.getProcessInstanceId(), FlowableRuntimeUtils.USER);
141 
142         Boolean updatedEnabled = engine.getRuntimeService().
143                 getVariable(procInst.getProcessInstanceId(), FlowableRuntimeUtils.ENABLED, Boolean.class);
144         engine.getRuntimeService().removeVariable(
145                 procInst.getProcessInstanceId(), FlowableRuntimeUtils.ENABLED);
146         if (updatedEnabled != null) {
147             user.setSuspended(!updatedEnabled);
148         }
149 
150         metadata(user, creator, context);
151         FlowableRuntimeUtils.updateStatus(engine, procInst.getProcessInstanceId(), user);
152         User created = userDAO.save(user);
153 
154         publisher.publishEvent(
155                 new EntityLifecycleEvent<>(this, SyncDeltaType.CREATE, created, AuthContextUtils.getDomain()));
156 
157         engine.getRuntimeService().updateBusinessKey(
158                 procInst.getProcessInstanceId(), FlowableRuntimeUtils.getWFProcBusinessKey(created.getKey()));
159 
160         Boolean propagateEnable = engine.getRuntimeService().getVariable(
161                 procInst.getProcessInstanceId(), FlowableRuntimeUtils.PROPAGATE_ENABLE, Boolean.class);
162         engine.getRuntimeService().removeVariable(
163                 procInst.getProcessInstanceId(), FlowableRuntimeUtils.PROPAGATE_ENABLE);
164         if (propagateEnable == null) {
165             propagateEnable = enabled;
166         }
167 
168         PropagationByResource<String> propByRes = new PropagationByResource<>();
169         propByRes.set(ResourceOperation.CREATE, userDAO.findAllResourceKeys(created.getKey()));
170 
171         PropagationByResource<Pair<String, String>> propByLinkedAccount = new PropagationByResource<>();
172         user.getLinkedAccounts().forEach(account -> propByLinkedAccount.add(
173                 ResourceOperation.CREATE,
174                 Pair.of(account.getResource().getKey(), account.getConnObjectKeyValue())));
175 
176         FlowableRuntimeUtils.saveForFormSubmit(
177                 engine,
178                 procInst.getProcessInstanceId(),
179                 dataBinder.getUserTO(created, true),
180                 userCR.getPassword(),
181                 enabled,
182                 propByRes,
183                 propByLinkedAccount);
184 
185         Set<String> tasks = FlowableRuntimeUtils.getPerformedTasks(engine, procInst.getProcessInstanceId());
186 
187         return new UserWorkflowResult<>(
188                 Pair.of(created.getKey(), propagateEnable),
189                 propByRes,
190                 propByLinkedAccount,
191                 tasks);
192     }
193 
194     protected Set<String> doExecuteNextTask(
195             final String procInstID,
196             final User user,
197             final Map<String, Object> moreVariables) {
198 
199         Set<String> preTasks = FlowableRuntimeUtils.getPerformedTasks(engine, procInstID);
200 
201         Map<String, Object> variables = new HashMap<>();
202         variables.put(FlowableRuntimeUtils.WF_EXECUTOR, AuthContextUtils.getUsername());
203         variables.put(FlowableRuntimeUtils.USER, lazyLoad(user));
204 
205         if (moreVariables != null && !moreVariables.isEmpty()) {
206             variables.putAll(moreVariables);
207         }
208 
209         List<Task> tasks = engine.getTaskService().createTaskQuery().processInstanceId(procInstID).list();
210         String task = null;
211         if (tasks.size() == 1) {
212             try {
213                 engine.getTaskService().complete(tasks.get(0).getId(), variables);
214                 task = tasks.get(0).getTaskDefinitionKey();
215             } catch (FlowableException e) {
216                 FlowableRuntimeUtils.throwException(
217                         e, "While completing task '" + tasks.get(0).getName() + "' for " + user);
218             }
219         } else {
220             LOG.warn("Expected a single task, found {}", tasks.size());
221         }
222 
223         Set<String> postTasks = FlowableRuntimeUtils.getPerformedTasks(engine, procInstID);
224         postTasks.removeAll(preTasks);
225         if (task != null) {
226             postTasks.add(task);
227         }
228 
229         return postTasks;
230     }
231 
232     @Override
233     protected UserWorkflowResult<String> doActivate(
234             final User user, final String token, final String updater, final String context) {
235 
236         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());
237 
238         Map<String, Object> variables = new HashMap<>(2);
239         variables.put(FlowableRuntimeUtils.TOKEN, token);
240         variables.put(FlowableRuntimeUtils.TASK, "activate");
241 
242         Set<String> tasks = doExecuteNextTask(procInstID, user, variables);
243 
244         metadata(user, updater, context);
245         FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
246         User updated = userDAO.save(user);
247 
248         publisher.publishEvent(
249                 new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));
250 
251         variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key));
252         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
253         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
254 
255         return new UserWorkflowResult<>(updated.getKey(), null, null, tasks);
256     }
257 
258     @Override
259     protected UserWorkflowResult<Pair<UserUR, Boolean>> doUpdate(
260             final User user, final UserUR userUR, final String updater, final String context) {
261 
262         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());
263 
264         // save some existing variable values for later processing, after actual update is made 
265         UserUR beforeUpdate = engine.getRuntimeService().
266                 getVariable(procInstID, FlowableRuntimeUtils.USER_UR, UserUR.class);
267         @SuppressWarnings("unchecked")
268         PropagationByResource<String> propByResBeforeUpdate = engine.getRuntimeService().getVariable(
269                 procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
270         @SuppressWarnings("unchecked")
271         PropagationByResource<Pair<String, String>> propByLinkedAccountBeforeUpdate = engine.getRuntimeService().
272                 getVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
273 
274         // whether the initial status is a form task
275         boolean inFormTask = FlowableRuntimeUtils.getFormTask(engine, procInstID) != null;
276 
277         Map<String, Object> variables = new HashMap<>(2);
278         variables.put(FlowableRuntimeUtils.USER_UR, userUR);
279         variables.put(FlowableRuntimeUtils.TASK, "update");
280 
281         Set<String> tasks = doExecuteNextTask(procInstID, user, variables);
282 
283         metadata(user, updater, context);
284         FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
285         User updated = userDAO.save(user);
286 
287         publisher.publishEvent(
288                 new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));
289 
290         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
291         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
292         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK);
293 
294         // if the original status was a form task, restore the patch as before the process started
295         if (inFormTask) {
296             if (beforeUpdate == null) {
297                 engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER_UR);
298             } else {
299                 engine.getRuntimeService().setVariable(procInstID, FlowableRuntimeUtils.USER_UR, beforeUpdate);
300             }
301         }
302 
303         // whether the after status is a form task
304         inFormTask = FlowableRuntimeUtils.getFormTask(engine, procInstID) != null;
305         if (!inFormTask) {
306             engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER_UR);
307         }
308 
309         @SuppressWarnings("unchecked")
310         PropagationByResource<String> propByRes = engine.getRuntimeService().getVariable(
311                 procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
312         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
313 
314         @SuppressWarnings("unchecked")
315         PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
316                 procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
317         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);
318 
319         FlowableRuntimeUtils.saveForFormSubmit(
320                 engine,
321                 procInstID,
322                 dataBinder.getUserTO(updated, true),
323                 userUR.getPassword() == null ? null : userUR.getPassword().getValue(),
324                 null,
325                 Optional.ofNullable(propByResBeforeUpdate).orElse(propByRes),
326                 Optional.ofNullable(propByLinkedAccountBeforeUpdate).orElse(propByLinkedAccount));
327 
328         Boolean propagateEnable = engine.getRuntimeService().getVariable(
329                 procInstID, FlowableRuntimeUtils.PROPAGATE_ENABLE, Boolean.class);
330         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROPAGATE_ENABLE);
331 
332         return new UserWorkflowResult<>(Pair.of(userUR, propagateEnable), propByRes, propByLinkedAccount, tasks);
333     }
334 
335     @Override
336     protected UserWorkflowResult<String> doSuspend(final User user, final String updater, final String context) {
337         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());
338 
339         Set<String> performedTasks =
340                 doExecuteNextTask(procInstID, user, Map.of(FlowableRuntimeUtils.TASK, "suspend"));
341 
342         metadata(user, updater, context);
343         FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
344         User updated = userDAO.save(user);
345 
346         publisher.publishEvent(
347                 new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));
348 
349         @SuppressWarnings("unchecked")
350         PropagationByResource<String> propByRes = engine.getRuntimeService().getVariable(
351                 procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
352         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
353 
354         @SuppressWarnings("unchecked")
355         PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
356                 procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
357         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);
358 
359         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK);
360         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
361         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
362         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
363         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);
364 
365         return new UserWorkflowResult<>(updated.getKey(), propByRes, propByLinkedAccount, performedTasks);
366     }
367 
368     @Override
369     protected UserWorkflowResult<String> doReactivate(final User user, final String updater, final String context) {
370         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());
371 
372         Set<String> performedTasks =
373                 doExecuteNextTask(procInstID, user, Map.of(FlowableRuntimeUtils.TASK, "reactivate"));
374 
375         metadata(user, updater, context);
376         FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
377         User updated = userDAO.save(user);
378 
379         publisher.publishEvent(
380                 new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));
381 
382         @SuppressWarnings("unchecked")
383         PropagationByResource<String> propByRes = engine.getRuntimeService().getVariable(
384                 procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
385         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
386 
387         @SuppressWarnings("unchecked")
388         PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
389                 procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
390         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);
391 
392         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK);
393         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
394         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
395         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
396         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);
397 
398         return new UserWorkflowResult<>(updated.getKey(), propByRes, propByLinkedAccount, performedTasks);
399     }
400 
401     @Override
402     protected void doRequestPasswordReset(final User user, final String updater, final String context) {
403         Map<String, Object> variables = new HashMap<>(3);
404         variables.put(FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true));
405         variables.put(FlowableRuntimeUtils.TASK, "requestPasswordReset");
406         variables.put(FlowableRuntimeUtils.EVENT, "requestPasswordReset");
407 
408         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());
409 
410         doExecuteNextTask(procInstID, user, variables);
411 
412         metadata(user, updater, context);
413         User updated = userDAO.save(user);
414 
415         publisher.publishEvent(
416                 new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));
417 
418         variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key));
419         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
420         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
421     }
422 
423     @Override
424     protected UserWorkflowResult<Pair<UserUR, Boolean>> doConfirmPasswordReset(
425             final User user, final String token, final String password, final String updater, final String context) {
426 
427         Map<String, Object> variables = new HashMap<>(5);
428         variables.put(FlowableRuntimeUtils.TOKEN, token);
429         variables.put(FlowableRuntimeUtils.PASSWORD, password);
430         variables.put(FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true));
431         variables.put(FlowableRuntimeUtils.TASK, "confirmPasswordReset");
432         variables.put(FlowableRuntimeUtils.EVENT, "confirmPasswordReset");
433 
434         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());
435 
436         Set<String> tasks = doExecuteNextTask(procInstID, user, variables);
437 
438         metadata(user, updater, context);
439         User updated = userDAO.save(user);
440 
441         publisher.publishEvent(
442                 new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));
443 
444         variables.keySet().forEach(key -> engine.getRuntimeService().removeVariable(procInstID, key));
445         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
446         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
447 
448         @SuppressWarnings("unchecked")
449         PropagationByResource<String> propByRes = engine.getRuntimeService().getVariable(
450                 procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
451         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE);
452         @SuppressWarnings("unchecked")
453         PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
454                 procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
455         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT);
456         UserUR updatedReq = engine.getRuntimeService().getVariable(
457                 procInstID, FlowableRuntimeUtils.USER_UR, UserUR.class);
458         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER_UR);
459         Boolean propagateEnable = engine.getRuntimeService().getVariable(
460                 procInstID, FlowableRuntimeUtils.PROPAGATE_ENABLE, Boolean.class);
461         engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.PROPAGATE_ENABLE);
462 
463         return new UserWorkflowResult<>(Pair.of(updatedReq, propagateEnable), propByRes, propByLinkedAccount, tasks);
464     }
465 
466     @Override
467     protected void doDelete(final User user, final String eraser, final String context) {
468         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());
469 
470         doExecuteNextTask(procInstID, user, Map.of(FlowableRuntimeUtils.TASK, "delete"));
471 
472         PropagationByResource<String> propByRes = new PropagationByResource<>();
473         propByRes.set(ResourceOperation.DELETE, userDAO.findAllResourceKeys(user.getKey()));
474 
475         PropagationByResource<Pair<String, String>> propByLinkedAccount = new PropagationByResource<>();
476         user.getLinkedAccounts().forEach(account -> propByLinkedAccount.add(
477                 ResourceOperation.DELETE,
478                 Pair.of(account.getResource().getKey(), account.getConnObjectKeyValue())));
479 
480         if (engine.getRuntimeService().createProcessInstanceQuery().
481                 processInstanceId(procInstID).active().list().isEmpty()) {
482 
483             userDAO.delete(user.getKey());
484 
485             publisher.publishEvent(
486                     new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain()));
487 
488             if (!engine.getHistoryService().createHistoricProcessInstanceQuery().
489                     processInstanceId(procInstID).list().isEmpty()) {
490 
491                 engine.getHistoryService().deleteHistoricProcessInstance(procInstID);
492             }
493         } else {
494             FlowableRuntimeUtils.saveForFormSubmit(
495                     engine,
496                     procInstID,
497                     dataBinder.getUserTO(user, true),
498                     null,
499                     null,
500                     propByRes,
501                     propByLinkedAccount);
502 
503             FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
504             metadata(user, eraser, context);
505             User updated = userDAO.save(user);
506 
507             publisher.publishEvent(
508                     new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, updated, AuthContextUtils.getDomain()));
509 
510             engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.TASK);
511             engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.USER);
512             engine.getRuntimeService().removeVariable(procInstID, FlowableRuntimeUtils.WF_EXECUTOR);
513         }
514     }
515 
516     @Override
517     public UserWorkflowResult<String> executeNextTask(final WorkflowTaskExecInput workflowTaskExecInput) {
518         User user = userDAO.authFind(workflowTaskExecInput.getUserKey());
519 
520         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, user.getKey());
521 
522         Map<String, Object> variables = new HashMap<>();
523         variables.put(FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true));
524         variables.putAll(workflowTaskExecInput.getVariables());
525 
526         Set<String> performedTasks = doExecuteNextTask(procInstID, user, variables);
527         FlowableRuntimeUtils.updateStatus(engine, procInstID, user);
528         user = userDAO.save(user);
529 
530         publisher.publishEvent(
531                 new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain()));
532 
533         engine.getRuntimeService().setVariable(
534                 procInstID, FlowableRuntimeUtils.USER_TO, dataBinder.getUserTO(user, true));
535 
536         if (engine.getRuntimeService().createProcessInstanceQuery().
537                 processInstanceId(procInstID).active().list().isEmpty()) {
538 
539             userDAO.delete(user.getKey());
540 
541             publisher.publishEvent(
542                     new EntityLifecycleEvent<>(this, SyncDeltaType.DELETE, user, AuthContextUtils.getDomain()));
543 
544             if (!engine.getHistoryService().createHistoricProcessInstanceQuery().
545                     processInstanceId(procInstID).list().isEmpty()) {
546 
547                 engine.getHistoryService().deleteHistoricProcessInstance(procInstID);
548             }
549         } else {
550             @SuppressWarnings("unchecked")
551             PropagationByResource<String> propByRes = engine.getRuntimeService().
552                     getVariable(procInstID, FlowableRuntimeUtils.PROP_BY_RESOURCE, PropagationByResource.class);
553             @SuppressWarnings("unchecked")
554             PropagationByResource<Pair<String, String>> propByLinkedAccount = engine.getRuntimeService().getVariable(
555                     procInstID, FlowableRuntimeUtils.PROP_BY_LINKEDACCOUNT, PropagationByResource.class);
556 
557             FlowableRuntimeUtils.saveForFormSubmit(
558                     engine,
559                     procInstID,
560                     dataBinder.getUserTO(user, true),
561                     null,
562                     null,
563                     propByRes,
564                     propByLinkedAccount);
565         }
566 
567         return new UserWorkflowResult<>(user.getKey(), null, null, performedTasks);
568     }
569 
570     protected static void navigateAvailableTasks(final FlowElement flow, final List<String> availableTasks) {
571         if (flow instanceof Gateway) {
572             ((Gateway) flow).getOutgoingFlows().forEach(subflow -> navigateAvailableTasks(subflow, availableTasks));
573         } else if (flow instanceof SequenceFlow) {
574             navigateAvailableTasks(((SequenceFlow) flow).getTargetFlowElement(), availableTasks);
575         } else if (flow instanceof org.flowable.bpmn.model.Task) {
576             availableTasks.add(flow.getId());
577         } else {
578             LOG.debug("Unexpected flow found: {}", flow);
579         }
580     }
581 
582     @Override
583     public List<WorkflowTask> getAvailableTasks(final String userKey) {
584         String procInstID = FlowableRuntimeUtils.getWFProcInstID(engine, userKey);
585 
586         List<String> availableTasks = new ArrayList<>();
587         try {
588             Task currentTask = engine.getTaskService().createTaskQuery().processInstanceId(procInstID).singleResult();
589 
590             Process process = engine.getRepositoryService().
591                     getBpmnModel(FlowableRuntimeUtils.getLatestProcDefByKey(
592                             engine, FlowableRuntimeUtils.WF_PROCESS_ID).getId()).getProcesses().get(0);
593             process.getFlowElements().stream().
594                     filter(SequenceFlow.class::isInstance).
595                     map(SequenceFlow.class::cast).
596                     filter(flow -> flow.getSourceRef().equals(currentTask.getTaskDefinitionKey())).
597                     forEach(flow -> navigateAvailableTasks(flow.getTargetFlowElement(), availableTasks));
598         } catch (FlowableException e) {
599             throw new WorkflowException(
600                     "While reading available tasks for workflow instance " + procInstID, e);
601         }
602 
603         return availableTasks.stream().map(input -> {
604             WorkflowTask workflowTaskTO = new WorkflowTask();
605             workflowTaskTO.setName(input);
606             return workflowTaskTO;
607         }).collect(Collectors.toList());
608     }
609 }