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.provisioning.java.job;
20  
21  import java.time.format.DateTimeParseException;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Optional;
27  import java.util.Set;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.ExecutionException;
30  import java.util.concurrent.Future;
31  import java.util.concurrent.atomic.AtomicReference;
32  import java.util.stream.Collectors;
33  import javax.annotation.Resource;
34  import javax.validation.ConstraintViolation;
35  import javax.validation.ValidationException;
36  import javax.validation.Validator;
37  import org.apache.commons.jexl3.JexlContext;
38  import org.apache.commons.jexl3.MapContext;
39  import org.apache.commons.lang3.BooleanUtils;
40  import org.apache.commons.lang3.StringUtils;
41  import org.apache.commons.lang3.math.NumberUtils;
42  import org.apache.commons.lang3.tuple.Pair;
43  import org.apache.syncope.common.lib.command.CommandArgs;
44  import org.apache.syncope.common.lib.form.FormProperty;
45  import org.apache.syncope.common.lib.form.SyncopeForm;
46  import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
47  import org.apache.syncope.core.persistence.api.entity.task.FormPropertyDef;
48  import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
49  import org.apache.syncope.core.persistence.api.entity.task.MacroTaskCommand;
50  import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
51  import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
52  import org.apache.syncope.core.provisioning.api.macro.Command;
53  import org.apache.syncope.core.provisioning.api.macro.MacroActions;
54  import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
55  import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
56  import org.apache.syncope.core.spring.implementation.ImplementationManager;
57  import org.quartz.JobExecutionContext;
58  import org.quartz.JobExecutionException;
59  import org.springframework.aop.support.AopUtils;
60  import org.springframework.beans.factory.annotation.Autowired;
61  import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
62  import org.springframework.security.concurrent.DelegatingSecurityContextCallable;
63  import org.springframework.util.ReflectionUtils;
64  
65  public class MacroJobDelegate extends AbstractSchedTaskJobDelegate<MacroTask> {
66  
67      public static final String MACRO_TASK_FORM_JOBDETAIL_KEY = "macroTaskForm";
68  
69      @Autowired
70      protected ImplementationDAO implementationDAO;
71  
72      @Autowired
73      protected Validator validator;
74  
75      @Resource(name = "batchExecutor")
76      protected ThreadPoolTaskExecutor executor;
77  
78      protected final Map<String, MacroActions> perContextActions = new ConcurrentHashMap<>();
79  
80      protected final Map<String, Command<?>> perContextCommands = new ConcurrentHashMap<>();
81  
82      protected Optional<JexlContext> check(
83              final SyncopeForm macroTaskForm,
84              final Optional<MacroActions> actions,
85              final StringBuilder output) throws JobExecutionException {
86  
87          if (macroTaskForm == null) {
88              return Optional.empty();
89          }
90  
91          // check if there is any required property with no value provided
92          Set<String> missingFormProperties = task.getFormPropertyDefs().stream().
93                  filter(FormPropertyDef::isRequired).
94                  map(fpd -> Pair.of(
95                  fpd.getKey(),
96                  macroTaskForm.getProperty(fpd.getKey()).map(p -> p.getValue() != null))).
97                  filter(pair -> pair.getRight().isEmpty()).
98                  map(Pair::getLeft).
99                  collect(Collectors.toSet());
100         if (!missingFormProperties.isEmpty()) {
101             throw new JobExecutionException("Required form properties missing: " + missingFormProperties);
102         }
103 
104         // build the JEXL context where variables are mapped to property values, built according to the defined type
105         Map<String, Object> vars = new HashMap<>();
106         for (FormPropertyDef fpd : task.getFormPropertyDefs()) {
107             String value = macroTaskForm.getProperty(fpd.getKey()).map(FormProperty::getValue).orElse(null);
108             if (value == null) {
109                 continue;
110             }
111 
112             switch (fpd.getType()) {
113                 case String:
114                     if (Optional.ofNullable(fpd.getStringRegEx()).
115                             map(pattern -> !pattern.matcher(value).matches()).
116                             orElse(false)) {
117 
118                         throw new JobExecutionException("RegEx not matching for " + fpd.getKey() + ": " + value);
119                     }
120 
121                     vars.put(fpd.getKey(), value);
122                     break;
123 
124                 case Password:
125                     vars.put(fpd.getKey(), value);
126                     break;
127 
128                 case Boolean:
129                     vars.put(fpd.getKey(), BooleanUtils.toBoolean(value));
130                     break;
131 
132                 case Date:
133                     try {
134                         vars.put(fpd.getKey(), StringUtils.isBlank(fpd.getDatePattern())
135                                 ? FormatUtils.parseDate(value)
136                                 : FormatUtils.parseDate(value, fpd.getDatePattern()));
137                     } catch (DateTimeParseException e) {
138                         throw new JobExecutionException("Unparseable date " + fpd.getKey() + ": " + value, e);
139                     }
140                     break;
141 
142                 case Long:
143                     vars.put(fpd.getKey(), NumberUtils.toLong(value));
144                     break;
145 
146                 case Enum:
147                     if (!fpd.getEnumValues().containsKey(value)) {
148                         throw new JobExecutionException("Not allowed for " + fpd.getKey() + ": " + value);
149                     }
150 
151                     vars.put(fpd.getKey(), value);
152                     break;
153 
154                 case Dropdown:
155                     if (!fpd.isDropdownFreeForm()) {
156                         List<String> values = fpd.isDropdownSingleSelection()
157                                 ? List.of(value)
158                                 : List.of(value.split(";"));
159 
160                         if (!actions.map(a -> a.getDropdownValues(fpd.getKey()).keySet()).
161                                 orElse(Set.of()).containsAll(values)) {
162 
163                             throw new JobExecutionException("Not allowed for " + fpd.getKey() + ": " + values);
164                         }
165                     }
166 
167                     vars.put(fpd.getKey(), value);
168                     break;
169 
170                 default:
171             }
172         }
173 
174         // if validator is defined, validate the provided form
175         try {
176             actions.ifPresent(a -> a.validate(macroTaskForm, vars));
177         } catch (ValidationException e) {
178             throw new JobExecutionException("Invalid form submitted for task " + task.getKey(), e);
179         }
180 
181         output.append("Form parameter values: ").append(vars).append("\n\n");
182 
183         return vars.isEmpty() ? Optional.empty() : Optional.of(new MapContext(vars));
184     }
185 
186     protected String run(
187             final List<Pair<Command<CommandArgs>, CommandArgs>> commands,
188             final Optional<MacroActions> actions,
189             final StringBuilder output,
190             final boolean dryRun)
191             throws JobExecutionException {
192 
193         Future<AtomicReference<Pair<String, Throwable>>> future = executor.submit(
194                 new DelegatingSecurityContextCallable<>(() -> {
195 
196                     AtomicReference<Pair<String, Throwable>> error = new AtomicReference<>();
197 
198                     for (int i = 0; i < commands.size() && error.get() == null; i++) {
199                         Pair<Command<CommandArgs>, CommandArgs> command = commands.get(i);
200 
201                         try {
202                             String args = POJOHelper.serialize(command.getRight());
203                             output.append("Command[").append(command.getLeft().getClass().getName()).append("]: ").
204                                     append(args).append("\n");
205 
206                             if (!dryRun) {
207                                 actions.ifPresent(a -> a.beforeCommand(command.getLeft(), command.getRight()));
208 
209                                 String cmdOut = command.getLeft().run(command.getRight());
210 
211                                 actions.ifPresent(a -> a.afterCommand(command.getLeft(), command.getRight(), cmdOut));
212 
213                                 output.append(cmdOut);
214                             }
215                         } catch (Throwable t) {
216                             if (task.isContinueOnError()) {
217                                 output.append("Continuing on error: <").append(t.getMessage()).append('>');
218 
219                                 LOG.error("While running {} with args {}, continuing on error",
220                                         command.getLeft().getClass().getName(), command.getRight(), t);
221                             } else {
222                                 error.set(Pair.of(AopUtils.getTargetClass(command.getLeft()).getName(), t));
223                             }
224                         }
225                         output.append("\n\n");
226                     }
227 
228                     return error;
229                 }));
230 
231         try {
232             AtomicReference<Pair<String, Throwable>> error = future.get();
233             if (error.get() != null) {
234                 throw new JobExecutionException("While running " + error.get().getLeft(), error.get().getRight());
235             }
236         } catch (ExecutionException | InterruptedException e) {
237             throw new JobExecutionException("While waiting for macro commands completion", e);
238         }
239 
240         output.append("COMPLETED");
241 
242         return actions.filter(a -> !dryRun).map(a -> a.afterAll(output)).orElse(output).toString();
243     }
244 
245     @SuppressWarnings("unchecked")
246     @Override
247     protected String doExecute(final boolean dryRun, final String executor, final JobExecutionContext context)
248             throws JobExecutionException {
249 
250         Optional<MacroActions> actions;
251         if (task.getMacroActions() == null) {
252             actions = Optional.empty();
253         } else {
254             try {
255                 actions = Optional.of(ImplementationManager.build(
256                         task.getMacroActions(),
257                         () -> perContextActions.get(task.getMacroActions().getKey()),
258                         instance -> perContextActions.put(task.getMacroActions().getKey(), instance)));
259             } catch (Exception e) {
260                 throw new JobExecutionException("Could not build " + task.getMacroActions().getKey(), e);
261             }
262         }
263 
264         StringBuilder output = new StringBuilder();
265 
266         SyncopeForm macroTaskForm = (SyncopeForm) context.getMergedJobDataMap().get(MACRO_TASK_FORM_JOBDETAIL_KEY);
267         Optional<JexlContext> jexlContext = check(macroTaskForm, actions, output);
268 
269         if (!dryRun) {
270             actions.ifPresent(MacroActions::beforeAll);
271         }
272 
273         List<Pair<Command<CommandArgs>, CommandArgs>> commands = new ArrayList<>();
274         for (MacroTaskCommand command : task.getCommands()) {
275             Command<CommandArgs> runnable;
276             try {
277                 runnable = (Command<CommandArgs>) ImplementationManager.build(
278                         command.getCommand(),
279                         () -> perContextCommands.get(command.getCommand().getKey()),
280                         instance -> perContextCommands.put(command.getCommand().getKey(), instance));
281             } catch (Exception e) {
282                 throw new JobExecutionException("Could not build " + command.getCommand().getKey(), e);
283             }
284 
285             CommandArgs args;
286             if (command.getArgs() == null) {
287                 try {
288                     args = ImplementationManager.emptyArgs(command.getCommand());
289                 } catch (Exception e) {
290                     throw new JobExecutionException("While getting empty args from " + command.getKey(), e);
291                 }
292             } else {
293                 args = command.getArgs();
294 
295                 jexlContext.ifPresent(ctx -> ReflectionUtils.doWithFields(
296                         args.getClass(),
297                         field -> {
298                             if (String.class.equals(field.getType())) {
299                                 field.setAccessible(true);
300                                 Object value = field.get(args);
301                                 if (value instanceof String) {
302                                     field.set(args, JexlUtils.evaluateTemplate((String) value, ctx));
303                                 }
304                             }
305                         },
306                         field -> !field.isSynthetic()));
307 
308                 Set<ConstraintViolation<Object>> violations = validator.validate(args);
309                 if (!violations.isEmpty()) {
310                     LOG.error("While validating {}: {}", args, violations);
311 
312                     throw new JobExecutionException(
313                             "While running " + command.getKey(),
314                             new IllegalArgumentException(args.getClass().getName()));
315                 }
316             }
317 
318             commands.add(Pair.of(runnable, args));
319         }
320 
321         return run(commands, actions, output, dryRun);
322     }
323 
324     @Override
325     protected boolean hasToBeRegistered(final TaskExec<?> execution) {
326         return task.isSaveExecs();
327     }
328 }