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.pushpull;
20  
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Optional;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  import org.apache.commons.lang3.BooleanUtils;
30  import org.apache.commons.lang3.exception.ExceptionUtils;
31  import org.apache.syncope.common.lib.request.AnyUR;
32  import org.apache.syncope.common.lib.request.StringPatchItem;
33  import org.apache.syncope.common.lib.to.AnyTO;
34  import org.apache.syncope.common.lib.to.Provision;
35  import org.apache.syncope.common.lib.to.ProvisioningReport;
36  import org.apache.syncope.common.lib.types.AuditElements;
37  import org.apache.syncope.common.lib.types.AuditElements.Result;
38  import org.apache.syncope.common.lib.types.ExecStatus;
39  import org.apache.syncope.common.lib.types.MatchingRule;
40  import org.apache.syncope.common.lib.types.PatchOperation;
41  import org.apache.syncope.common.lib.types.ResourceOperation;
42  import org.apache.syncope.common.lib.types.UnmatchingRule;
43  import org.apache.syncope.core.persistence.api.entity.Any;
44  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
45  import org.apache.syncope.core.persistence.api.entity.task.PushTask;
46  import org.apache.syncope.core.persistence.api.entity.user.User;
47  import org.apache.syncope.core.provisioning.api.AuditManager;
48  import org.apache.syncope.core.provisioning.api.MappingManager;
49  import org.apache.syncope.core.provisioning.api.PropagationByResource;
50  import org.apache.syncope.core.provisioning.api.event.AfterHandlingEvent;
51  import org.apache.syncope.core.provisioning.api.notification.NotificationManager;
52  import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
53  import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
54  import org.apache.syncope.core.provisioning.api.pushpull.IgnoreProvisionException;
55  import org.apache.syncope.core.provisioning.api.pushpull.PushActions;
56  import org.apache.syncope.core.provisioning.api.pushpull.SyncopePushResultHandler;
57  import org.apache.syncope.core.provisioning.java.job.AfterHandlingJob;
58  import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
59  import org.apache.syncope.core.spring.security.AuthContextUtils;
60  import org.identityconnectors.framework.common.objects.ConnectorObject;
61  import org.quartz.JobExecutionException;
62  import org.springframework.beans.factory.annotation.Autowired;
63  import org.springframework.scheduling.quartz.SchedulerFactoryBean;
64  import org.springframework.transaction.annotation.Propagation;
65  import org.springframework.transaction.annotation.Transactional;
66  
67  public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHandler<PushTask, PushActions>
68          implements SyncopePushResultHandler {
69  
70      @Autowired
71      protected OutboundMatcher outboundMatcher;
72  
73      /**
74       * Notification Manager.
75       */
76      @Autowired
77      protected NotificationManager notificationManager;
78  
79      /**
80       * Audit Manager.
81       */
82      @Autowired
83      protected AuditManager auditManager;
84  
85      @Autowired
86      protected MappingManager mappingManager;
87  
88      @Autowired
89      protected SchedulerFactoryBean scheduler;
90  
91      protected abstract String getName(Any<?> any);
92  
93      protected void update(
94              final Any<?> any,
95              final Boolean enable,
96              final ConnectorObject beforeObj,
97              final ProvisioningReport result) {
98  
99          boolean changepwd = any instanceof User;
100         List<String> ownedResources = getAnyUtils().getAllResources(any).stream().
101                 map(ExternalResource::getKey).collect(Collectors.toList());
102 
103         List<String> noPropResources = new ArrayList<>(ownedResources);
104         noPropResources.remove(profile.getTask().getResource().getKey());
105 
106         PropagationByResource<String> propByRes = new PropagationByResource<>();
107         propByRes.add(ResourceOperation.UPDATE, profile.getTask().getResource().getKey());
108         propByRes.addOldConnObjectKey(profile.getTask().getResource().getKey(), beforeObj.getUid().getUidValue());
109 
110         List<PropagationTaskInfo> taskInfos = propagationManager.getUpdateTasks(
111                 any.getType().getKind(),
112                 any.getKey(),
113                 changepwd,
114                 enable,
115                 propByRes,
116                 null,
117                 null,
118                 noPropResources);
119         if (!taskInfos.isEmpty()) {
120             taskInfos.get(0).setBeforeObj(Optional.of(beforeObj));
121             PropagationReporter reporter = new DefaultPropagationReporter();
122             taskExecutor.execute(taskInfos.get(0), reporter, securityProperties.getAdminUser());
123             reportPropagation(result, reporter);
124         }
125     }
126 
127     protected void deprovision(final Any<?> any, final ConnectorObject beforeObj, final ProvisioningReport result) {
128         AnyTO before = getAnyTO(any);
129 
130         List<String> noPropResources = new ArrayList<>(before.getResources());
131         noPropResources.remove(profile.getTask().getResource().getKey());
132 
133         PropagationByResource<String> propByRes = new PropagationByResource<>();
134         propByRes.add(ResourceOperation.DELETE, profile.getTask().getResource().getKey());
135         propByRes.addOldConnObjectKey(profile.getTask().getResource().getKey(), beforeObj.getUid().getUidValue());
136 
137         List<PropagationTaskInfo> taskInfos = propagationManager.getDeleteTasks(
138                 any.getType().getKind(),
139                 any.getKey(),
140                 propByRes,
141                 null,
142                 noPropResources);
143         if (!taskInfos.isEmpty()) {
144             taskInfos.get(0).setBeforeObj(Optional.of(beforeObj));
145             PropagationReporter reporter = new DefaultPropagationReporter();
146             taskExecutor.execute(taskInfos.get(0), reporter, securityProperties.getAdminUser());
147             reportPropagation(result, reporter);
148         }
149     }
150 
151     protected void provision(final Any<?> any, final Boolean enable, final ProvisioningReport result) {
152         AnyTO before = getAnyTO(any);
153 
154         List<String> noPropResources = new ArrayList<>(before.getResources());
155         noPropResources.remove(profile.getTask().getResource().getKey());
156 
157         PropagationByResource<String> propByRes = new PropagationByResource<>();
158         propByRes.add(ResourceOperation.CREATE, profile.getTask().getResource().getKey());
159 
160         List<PropagationTaskInfo> taskInfos = propagationManager.getCreateTasks(
161                 any.getType().getKind(),
162                 any.getKey(),
163                 enable,
164                 propByRes,
165                 before.getVirAttrs(),
166                 noPropResources);
167         if (!taskInfos.isEmpty()) {
168             taskInfos.get(0).setBeforeObj(Optional.empty());
169             PropagationReporter reporter = new DefaultPropagationReporter();
170             taskExecutor.execute(taskInfos.get(0), reporter, securityProperties.getAdminUser());
171             reportPropagation(result, reporter);
172         }
173     }
174 
175     protected void link(final Any<?> any, final boolean unlink, final ProvisioningReport result) {
176         AnyUR req = getAnyUtils().newAnyUR(any.getKey());
177         req.getResources().add(new StringPatchItem.Builder().
178                 operation(unlink ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE).
179                 value(profile.getTask().getResource().getKey()).build());
180 
181         update(req);
182 
183         result.setStatus(ProvisioningReport.Status.SUCCESS);
184     }
185 
186     protected void unassign(final Any<?> any, final ConnectorObject beforeObj, final ProvisioningReport result) {
187         AnyUR req = getAnyUtils().newAnyUR(any.getKey());
188         req.getResources().add(new StringPatchItem.Builder().
189                 operation(PatchOperation.DELETE).
190                 value(profile.getTask().getResource().getKey()).build());
191 
192         update(req);
193 
194         deprovision(any, beforeObj, result);
195     }
196 
197     protected void assign(final Any<?> any, final Boolean enabled, final ProvisioningReport result) {
198         AnyUR req = getAnyUtils().newAnyUR(any.getKey());
199         req.getResources().add(new StringPatchItem.Builder().
200                 operation(PatchOperation.ADD_REPLACE).
201                 value(profile.getTask().getResource().getKey()).build());
202 
203         update(req);
204 
205         provision(any, enabled, result);
206     }
207 
208     @Transactional(propagation = Propagation.REQUIRES_NEW)
209     @Override
210     public boolean handle(final String anyKey) {
211         Any<?> any = null;
212         try {
213             any = getAnyUtils().dao().authFind(anyKey);
214 
215             Provision provision = profile.getTask().getResource().
216                     getProvisionByAnyType(any.getType().getKey()).orElse(null);
217             if (provision == null) {
218                 throw new JobExecutionException("No provision found on " + profile.getTask().getResource() + " for "
219                         + any.getType().getKey());
220             }
221 
222             doHandle(any, provision);
223             return true;
224         } catch (IgnoreProvisionException e) {
225             ProvisioningReport ignoreResult = profile.getResults().stream().
226                     filter(report -> anyKey.equalsIgnoreCase(report.getKey())).
227                     findFirst().
228                     orElse(null);
229             if (ignoreResult == null) {
230                 ignoreResult = new ProvisioningReport();
231                 ignoreResult.setKey(anyKey);
232                 ignoreResult.setAnyType(Optional.ofNullable(any).map(any1 -> any1.getType().getKey()).orElse(null));
233 
234                 profile.getResults().add(ignoreResult);
235             }
236 
237             ignoreResult.setOperation(ResourceOperation.NONE);
238             ignoreResult.setStatus(ProvisioningReport.Status.IGNORE);
239             ignoreResult.setMessage(e.getMessage());
240 
241             LOG.warn("Ignoring during push", e);
242             return true;
243         } catch (JobExecutionException e) {
244             LOG.error("Push failed", e);
245             return false;
246         }
247     }
248 
249     protected void doHandle(final Any<?> any, final Provision provision) throws JobExecutionException {
250         ProvisioningReport result = new ProvisioningReport();
251         profile.getResults().add(result);
252 
253         result.setKey(any.getKey());
254         result.setAnyType(any.getType().getKey());
255         result.setName(getName(any));
256 
257         LOG.debug("Pushing {} with key {} towards {}",
258                 any.getType().getKind(), any.getKey(), profile.getTask().getResource());
259 
260         // Try to read remote object BEFORE any actual operation
261         Set<String> moreAttrsToGet = new HashSet<>();
262         profile.getActions().forEach(action -> moreAttrsToGet.addAll(action.moreAttrsToGet(profile, any)));
263         List<ConnectorObject> connObjs = outboundMatcher.match(
264                 profile.getConnector(),
265                 any,
266                 profile.getTask().getResource(),
267                 provision,
268                 Optional.of(moreAttrsToGet.toArray(String[]::new)));
269         LOG.debug("Match(es) found for {} as {}: {}", any, provision.getObjectClass(), connObjs);
270 
271         if (connObjs.size() > 1) {
272             switch (profile.getConflictResolutionAction()) {
273                 case IGNORE:
274                     throw new IgnoreProvisionException("More than one match found for "
275                             + any.getKey() + ": " + connObjs);
276 
277                 case FIRSTMATCH:
278                     connObjs = connObjs.subList(0, 1);
279                     break;
280 
281                 case LASTMATCH:
282                     connObjs = connObjs.subList(connObjs.size() - 1, connObjs.size());
283                     break;
284 
285                 default:
286             }
287         }
288         ConnectorObject beforeObj = connObjs.isEmpty() ? null : connObjs.get(0);
289 
290         if (profile.isDryRun()) {
291             if (beforeObj == null) {
292                 result.setOperation(toResourceOperation(profile.getTask().getUnmatchingRule()));
293             } else {
294                 result.setOperation(toResourceOperation(profile.getTask().getMatchingRule()));
295             }
296             result.setStatus(ProvisioningReport.Status.SUCCESS);
297         } else {
298             String operation = beforeObj == null
299                     ? UnmatchingRule.toEventName(profile.getTask().getUnmatchingRule())
300                     : MatchingRule.toEventName(profile.getTask().getMatchingRule());
301 
302             boolean notificationsAvailable = notificationManager.notificationsAvailable(
303                     AuditElements.EventCategoryType.PUSH,
304                     any.getType().getKind().name(),
305                     profile.getTask().getResource().getKey(),
306                     operation);
307             boolean auditRequested = auditManager.auditRequested(
308                     AuthContextUtils.getUsername(),
309                     AuditElements.EventCategoryType.PUSH,
310                     any.getType().getKind().name(),
311                     profile.getTask().getResource().getKey(),
312                     operation);
313 
314             Object output = null;
315             Result resultStatus = null;
316 
317             Boolean enable = any instanceof User && profile.getTask().isSyncStatus()
318                     ? BooleanUtils.negate(((User) any).isSuspended())
319                     : null;
320             try {
321                 if (beforeObj == null) {
322                     result.setOperation(toResourceOperation(profile.getTask().getUnmatchingRule()));
323 
324                     switch (profile.getTask().getUnmatchingRule()) {
325                         case ASSIGN:
326                             for (PushActions action : profile.getActions()) {
327                                 action.beforeAssign(profile, any);
328                             }
329 
330                             if (!profile.getTask().isPerformCreate()) {
331                                 LOG.debug("PushTask not configured for create");
332                                 result.setStatus(ProvisioningReport.Status.IGNORE);
333                             } else {
334                                 assign(any, enable, result);
335                             }
336                             break;
337 
338                         case PROVISION:
339                             for (PushActions action : profile.getActions()) {
340                                 action.beforeProvision(profile, any);
341                             }
342 
343                             if (!profile.getTask().isPerformCreate()) {
344                                 LOG.debug("PushTask not configured for create");
345                                 result.setStatus(ProvisioningReport.Status.IGNORE);
346                             } else {
347                                 provision(any, enable, result);
348                             }
349                             break;
350 
351                         case UNLINK:
352                             for (PushActions action : profile.getActions()) {
353                                 action.beforeUnlink(profile, any);
354                             }
355 
356                             if (!profile.getTask().isPerformUpdate()) {
357                                 LOG.debug("PushTask not configured for update");
358                                 result.setStatus(ProvisioningReport.Status.IGNORE);
359                             } else {
360                                 link(any, true, result);
361                             }
362                             break;
363 
364                         case IGNORE:
365                             LOG.debug("Ignored any: {}", any);
366                             result.setStatus(ProvisioningReport.Status.IGNORE);
367                             break;
368 
369                         default:
370                         // do nothing
371                     }
372                 } else {
373                     result.setOperation(toResourceOperation(profile.getTask().getMatchingRule()));
374 
375                     switch (profile.getTask().getMatchingRule()) {
376                         case UPDATE:
377                             for (PushActions action : profile.getActions()) {
378                                 action.beforeUpdate(profile, any);
379                             }
380                             if (!profile.getTask().isPerformUpdate()) {
381                                 LOG.debug("PushTask not configured for update");
382                                 result.setStatus(ProvisioningReport.Status.IGNORE);
383                             } else {
384                                 update(any, enable, beforeObj, result);
385                             }
386                             break;
387 
388                         case DEPROVISION:
389                             for (PushActions action : profile.getActions()) {
390                                 action.beforeDeprovision(profile, any);
391                             }
392 
393                             if (!profile.getTask().isPerformDelete()) {
394                                 LOG.debug("PushTask not configured for delete");
395                                 result.setStatus(ProvisioningReport.Status.IGNORE);
396                             } else {
397                                 deprovision(any, beforeObj, result);
398                             }
399                             break;
400 
401                         case UNASSIGN:
402                             for (PushActions action : profile.getActions()) {
403                                 action.beforeUnassign(profile, any);
404                             }
405 
406                             if (!profile.getTask().isPerformDelete()) {
407                                 LOG.debug("PushTask not configured for delete");
408                                 result.setStatus(ProvisioningReport.Status.IGNORE);
409                             } else {
410                                 unassign(any, beforeObj, result);
411                             }
412                             break;
413 
414                         case LINK:
415                             for (PushActions action : profile.getActions()) {
416                                 action.beforeLink(profile, any);
417                             }
418 
419                             if (!profile.getTask().isPerformUpdate()) {
420                                 LOG.debug("PushTask not configured for update");
421                                 result.setStatus(ProvisioningReport.Status.IGNORE);
422                             } else {
423                                 link(any, false, result);
424                             }
425                             break;
426 
427                         case UNLINK:
428                             for (PushActions action : profile.getActions()) {
429                                 action.beforeUnlink(profile, any);
430                             }
431 
432                             if (!profile.getTask().isPerformUpdate()) {
433                                 LOG.debug("PushTask not configured for update");
434                                 result.setStatus(ProvisioningReport.Status.IGNORE);
435                             } else {
436                                 link(any, true, result);
437                             }
438 
439                             break;
440 
441                         case IGNORE:
442                             LOG.debug("Ignored any: {}", any);
443                             result.setStatus(ProvisioningReport.Status.IGNORE);
444                             break;
445 
446                         default:
447                         // do nothing
448                     }
449                 }
450 
451                 for (PushActions action : profile.getActions()) {
452                     action.after(profile, any, result);
453                 }
454 
455                 if (result.getStatus() == null) {
456                     result.setStatus(ProvisioningReport.Status.SUCCESS);
457                 }
458 
459                 if (notificationsAvailable || auditRequested) {
460                     resultStatus = AuditElements.Result.SUCCESS;
461                     output = outboundMatcher.match(
462                             profile.getConnector(),
463                             any,
464                             profile.getTask().getResource(),
465                             provision,
466                             Optional.of(moreAttrsToGet.toArray(String[]::new)));
467                 }
468             } catch (IgnoreProvisionException e) {
469                 throw e;
470             } catch (Exception e) {
471                 result.setStatus(ProvisioningReport.Status.FAILURE);
472                 result.setMessage(ExceptionUtils.getRootCauseMessage(e));
473 
474                 if (notificationsAvailable || auditRequested) {
475                     resultStatus = AuditElements.Result.FAILURE;
476                     output = e;
477                 }
478 
479                 LOG.warn("Error pushing {} towards {}", any, profile.getTask().getResource(), e);
480 
481                 for (PushActions action : profile.getActions()) {
482                     action.onError(profile, any, result, e);
483                 }
484 
485                 throw new JobExecutionException(e);
486             } finally {
487                 if (notificationsAvailable || auditRequested) {
488                     Map<String, Object> jobMap = new HashMap<>();
489                     jobMap.put(AfterHandlingEvent.JOBMAP_KEY, new AfterHandlingEvent(
490                             AuthContextUtils.getWho(),
491                             AuditElements.EventCategoryType.PUSH,
492                             any.getType().getKind().name(),
493                             profile.getTask().getResource().getKey(),
494                             operation,
495                             resultStatus,
496                             beforeObj,
497                             output,
498                             any));
499                     AfterHandlingJob.schedule(scheduler, jobMap);
500                 }
501             }
502         }
503     }
504 
505     protected static void reportPropagation(final ProvisioningReport result, final PropagationReporter reporter) {
506         if (!reporter.getStatuses().isEmpty()) {
507             result.setStatus(toProvisioningReportStatus(reporter.getStatuses().get(0).getStatus()));
508             result.setMessage(reporter.getStatuses().get(0).getFailureReason());
509         }
510     }
511 
512     protected static ResourceOperation toResourceOperation(final UnmatchingRule rule) {
513         switch (rule) {
514             case ASSIGN:
515             case PROVISION:
516                 return ResourceOperation.CREATE;
517             default:
518                 return ResourceOperation.NONE;
519         }
520     }
521 
522     protected static ResourceOperation toResourceOperation(final MatchingRule rule) {
523         switch (rule) {
524             case UPDATE:
525                 return ResourceOperation.UPDATE;
526             case DEPROVISION:
527             case UNASSIGN:
528                 return ResourceOperation.DELETE;
529             default:
530                 return ResourceOperation.NONE;
531         }
532     }
533 
534     protected static ProvisioningReport.Status toProvisioningReportStatus(final ExecStatus status) {
535         switch (status) {
536             case FAILURE:
537                 return ProvisioningReport.Status.FAILURE;
538 
539             case SUCCESS:
540                 return ProvisioningReport.Status.SUCCESS;
541 
542             case CREATED:
543             case NOT_ATTEMPTED:
544             default:
545                 return ProvisioningReport.Status.IGNORE;
546         }
547     }
548 }