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.List;
24  import java.util.Map;
25  import java.util.Optional;
26  import java.util.Set;
27  import java.util.stream.Stream;
28  import org.apache.commons.lang3.exception.ExceptionUtils;
29  import org.apache.commons.lang3.tuple.Pair;
30  import org.apache.syncope.common.lib.SyncopeConstants;
31  import org.apache.syncope.common.lib.to.Item;
32  import org.apache.syncope.common.lib.to.OrgUnit;
33  import org.apache.syncope.common.lib.to.ProvisioningReport;
34  import org.apache.syncope.common.lib.to.RealmTO;
35  import org.apache.syncope.common.lib.types.AuditElements;
36  import org.apache.syncope.common.lib.types.AuditElements.Result;
37  import org.apache.syncope.common.lib.types.ExecStatus;
38  import org.apache.syncope.common.lib.types.MatchingRule;
39  import org.apache.syncope.common.lib.types.ResourceOperation;
40  import org.apache.syncope.common.lib.types.UnmatchingRule;
41  import org.apache.syncope.core.persistence.api.entity.Realm;
42  import org.apache.syncope.core.persistence.api.entity.task.PushTask;
43  import org.apache.syncope.core.provisioning.api.MappingManager;
44  import org.apache.syncope.core.provisioning.api.PropagationByResource;
45  import org.apache.syncope.core.provisioning.api.TimeoutException;
46  import org.apache.syncope.core.provisioning.api.event.AfterHandlingEvent;
47  import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
48  import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
49  import org.apache.syncope.core.provisioning.api.pushpull.IgnoreProvisionException;
50  import org.apache.syncope.core.provisioning.api.pushpull.PushActions;
51  import org.apache.syncope.core.provisioning.api.pushpull.RealmPushResultHandler;
52  import org.apache.syncope.core.provisioning.java.job.AfterHandlingJob;
53  import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
54  import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
55  import org.apache.syncope.core.spring.security.AuthContextUtils;
56  import org.identityconnectors.framework.common.objects.Attribute;
57  import org.identityconnectors.framework.common.objects.AttributeBuilder;
58  import org.identityconnectors.framework.common.objects.ConnectorObject;
59  import org.identityconnectors.framework.common.objects.ObjectClass;
60  import org.quartz.JobExecutionException;
61  import org.springframework.beans.factory.annotation.Autowired;
62  import org.springframework.scheduling.quartz.SchedulerFactoryBean;
63  import org.springframework.transaction.annotation.Propagation;
64  import org.springframework.transaction.annotation.Transactional;
65  
66  public class DefaultRealmPushResultHandler
67          extends AbstractRealmResultHandler<PushTask, PushActions>
68          implements RealmPushResultHandler {
69  
70      @Autowired
71      private MappingManager mappingManager;
72  
73      @Autowired
74      private SchedulerFactoryBean scheduler;
75  
76      @Transactional(propagation = Propagation.REQUIRES_NEW)
77      @Override
78      public boolean handle(final String realmKey) {
79          Realm realm = null;
80          try {
81              realm = realmDAO.find(realmKey);
82              doHandle(realm);
83              return true;
84          } catch (IgnoreProvisionException e) {
85              ProvisioningReport result = new ProvisioningReport();
86              result.setOperation(ResourceOperation.NONE);
87              result.setAnyType(realm == null ? null : SyncopeConstants.REALM_ANYTYPE);
88              result.setStatus(ProvisioningReport.Status.IGNORE);
89              result.setKey(realmKey);
90              profile.getResults().add(result);
91  
92              LOG.warn("Ignoring during push", e);
93              return true;
94          } catch (JobExecutionException e) {
95              LOG.error("Push failed", e);
96              return false;
97          }
98      }
99  
100     private static void reportPropagation(final ProvisioningReport result, final PropagationReporter reporter) {
101         if (!reporter.getStatuses().isEmpty()) {
102             result.setStatus(toProvisioningReportStatus(reporter.getStatuses().get(0).getStatus()));
103             result.setMessage(reporter.getStatuses().get(0).getFailureReason());
104         }
105     }
106 
107     private Realm update(final RealmTO realmTO, final ConnectorObject beforeObj, final ProvisioningReport result) {
108         Realm realm = realmDAO.findByFullPath(realmTO.getFullPath());
109 
110         Map<Pair<String, String>, Set<Attribute>> beforeAttrs = propagationManager.prepareAttrs(realm);
111 
112         PropagationByResource<String> propByRes = binder.update(realm, realmTO);
113         realm = realmDAO.save(realm);
114 
115         List<PropagationTaskInfo> taskInfos = propagationManager.setAttributeDeltas(
116                 propagationManager.createTasks(realm, propByRes, null),
117                 beforeAttrs,
118                 null);
119         if (!taskInfos.isEmpty()) {
120             taskInfos.get(0).setBeforeObj(Optional.ofNullable(beforeObj));
121             PropagationReporter reporter = new DefaultPropagationReporter();
122             taskExecutor.execute(taskInfos.get(0), reporter, securityProperties.getAdminUser());
123             reportPropagation(result, reporter);
124         }
125 
126         return realm;
127     }
128 
129     private void deprovision(final Realm realm, final ConnectorObject beforeObj, final ProvisioningReport result) {
130         List<String> noPropResources = new ArrayList<>(realm.getResourceKeys());
131         noPropResources.remove(profile.getTask().getResource().getKey());
132 
133         PropagationByResource<String> propByRes = new PropagationByResource<>();
134         propByRes.addAll(ResourceOperation.DELETE, realm.getResourceKeys());
135 
136         List<PropagationTaskInfo> taskInfos = propagationManager.createTasks(realm, propByRes, noPropResources);
137         if (!taskInfos.isEmpty()) {
138             taskInfos.get(0).setBeforeObj(Optional.ofNullable(beforeObj));
139             PropagationReporter reporter = new DefaultPropagationReporter();
140             taskExecutor.execute(taskInfos.get(0), reporter, securityProperties.getAdminUser());
141             reportPropagation(result, reporter);
142         }
143     }
144 
145     private void provision(final Realm realm, final ProvisioningReport result) {
146         List<String> noPropResources = new ArrayList<>(realm.getResourceKeys());
147         noPropResources.remove(profile.getTask().getResource().getKey());
148 
149         PropagationByResource<String> propByRes = new PropagationByResource<>();
150         propByRes.add(ResourceOperation.CREATE, profile.getTask().getResource().getKey());
151 
152         PropagationReporter reporter = taskExecutor.execute(
153                 propagationManager.createTasks(realm, propByRes, noPropResources),
154                 false, securityProperties.getAdminUser());
155         reportPropagation(result, reporter);
156     }
157 
158     private void link(final Realm realm, final boolean unlink, final ProvisioningReport result) {
159         RealmTO realmTO = binder.getRealmTO(realm, true);
160         if (unlink) {
161             realmTO.getResources().remove(profile.getTask().getResource().getKey());
162         } else {
163             realmTO.getResources().add(profile.getTask().getResource().getKey());
164         }
165 
166         update(realmTO, null, result);
167     }
168 
169     private void unassign(final Realm realm, final ConnectorObject beforeObj, final ProvisioningReport result) {
170         RealmTO realmTO = binder.getRealmTO(realm, true);
171         realmTO.getResources().remove(profile.getTask().getResource().getKey());
172 
173         deprovision(update(realmTO, beforeObj, result), beforeObj, result);
174     }
175 
176     private void assign(final Realm realm, final ProvisioningReport result) {
177         RealmTO realmTO = binder.getRealmTO(realm, true);
178         realmTO.getResources().add(profile.getTask().getResource().getKey());
179 
180         provision(update(realmTO, null, result), result);
181     }
182 
183     protected ConnectorObject getRemoteObject(
184             final ObjectClass objectClass,
185             final String connObjectKey,
186             final String connObjectKeyValue,
187             final boolean ignoreCaseMatch,
188             final Stream<Item> mapItems) {
189 
190         ConnectorObject obj = null;
191         try {
192             obj = profile.getConnector().getObject(
193                     objectClass,
194                     AttributeBuilder.build(connObjectKey, connObjectKeyValue),
195                     ignoreCaseMatch,
196                     MappingUtils.buildOperationOptions(mapItems));
197         } catch (TimeoutException toe) {
198             LOG.debug("Request timeout", toe);
199             throw toe;
200         } catch (RuntimeException ignore) {
201             LOG.debug("While resolving {}", connObjectKeyValue, ignore);
202         }
203 
204         return obj;
205     }
206 
207     private void doHandle(final Realm realm) throws JobExecutionException {
208         ProvisioningReport result = new ProvisioningReport();
209         profile.getResults().add(result);
210 
211         result.setKey(realm.getKey());
212         result.setAnyType(SyncopeConstants.REALM_ANYTYPE);
213         result.setName(realm.getFullPath());
214 
215         LOG.debug("Propagating Realm with key {} towards {}", realm.getKey(), profile.getTask().getResource());
216 
217         Object output = null;
218         Result resultStatus = null;
219 
220         // Try to read remote object BEFORE any actual operation
221         OrgUnit orgUnit = profile.getTask().getResource().getOrgUnit();
222         Optional<Item> connObjectKey = orgUnit.getConnObjectKeyItem();
223         Optional<String> connObjecKeyValue = mappingManager.getConnObjectKeyValue(realm, orgUnit);
224 
225         ConnectorObject beforeObj = null;
226         if (connObjectKey.isPresent() && connObjecKeyValue.isPresent()) {
227             beforeObj = getRemoteObject(
228                     new ObjectClass(orgUnit.getObjectClass()),
229                     connObjectKey.get().getExtAttrName(),
230                     connObjecKeyValue.get(),
231                     orgUnit.isIgnoreCaseMatch(),
232                     orgUnit.getItems().stream());
233         } else {
234             LOG.debug("OrgUnitItem {} or its value {} are null", connObjectKey, connObjecKeyValue);
235         }
236 
237         if (profile.isDryRun()) {
238             if (beforeObj == null) {
239                 result.setOperation(toResourceOperation(profile.getTask().getUnmatchingRule()));
240             } else {
241                 result.setOperation(toResourceOperation(profile.getTask().getMatchingRule()));
242             }
243             result.setStatus(ProvisioningReport.Status.SUCCESS);
244         } else {
245             String operation = beforeObj == null
246                     ? UnmatchingRule.toEventName(profile.getTask().getUnmatchingRule())
247                     : MatchingRule.toEventName(profile.getTask().getMatchingRule());
248 
249             boolean notificationsAvailable = notificationManager.notificationsAvailable(
250                     AuditElements.EventCategoryType.PUSH,
251                     SyncopeConstants.REALM_ANYTYPE.toLowerCase(),
252                     profile.getTask().getResource().getKey(),
253                     operation);
254             boolean auditRequested = auditManager.auditRequested(
255                     AuthContextUtils.getUsername(),
256                     AuditElements.EventCategoryType.PUSH,
257                     SyncopeConstants.REALM_ANYTYPE.toLowerCase(),
258                     profile.getTask().getResource().getKey(),
259                     operation);
260             try {
261                 if (beforeObj == null) {
262                     result.setOperation(toResourceOperation(profile.getTask().getUnmatchingRule()));
263 
264                     switch (profile.getTask().getUnmatchingRule()) {
265                         case ASSIGN:
266                             for (PushActions action : profile.getActions()) {
267                                 action.beforeAssign(profile, realm);
268                             }
269 
270                             if (!profile.getTask().isPerformCreate()) {
271                                 LOG.debug("PushTask not configured for create");
272                                 result.setStatus(ProvisioningReport.Status.IGNORE);
273                             } else {
274                                 assign(realm, result);
275                             }
276 
277                             break;
278 
279                         case PROVISION:
280                             for (PushActions action : profile.getActions()) {
281                                 action.beforeProvision(profile, realm);
282                             }
283 
284                             if (!profile.getTask().isPerformCreate()) {
285                                 LOG.debug("PushTask not configured for create");
286                                 result.setStatus(ProvisioningReport.Status.IGNORE);
287                             } else {
288                                 provision(realm, result);
289                             }
290 
291                             break;
292 
293                         case UNLINK:
294                             for (PushActions action : profile.getActions()) {
295                                 action.beforeUnlink(profile, realm);
296                             }
297 
298                             if (!profile.getTask().isPerformUpdate()) {
299                                 LOG.debug("PushTask not configured for update");
300                                 result.setStatus(ProvisioningReport.Status.IGNORE);
301                             } else {
302                                 link(realm, true, result);
303                             }
304 
305                             break;
306 
307                         case IGNORE:
308                             LOG.debug("Ignored any: {}", realm);
309                             result.setStatus(ProvisioningReport.Status.IGNORE);
310                             break;
311 
312                         default:
313                         // do nothing
314                     }
315                 } else {
316                     result.setOperation(toResourceOperation(profile.getTask().getMatchingRule()));
317 
318                     switch (profile.getTask().getMatchingRule()) {
319                         case UPDATE:
320                             for (PushActions action : profile.getActions()) {
321                                 action.beforeUpdate(profile, realm);
322                             }
323                             if (!profile.getTask().isPerformUpdate()) {
324                                 LOG.debug("PushTask not configured for update");
325                                 result.setStatus(ProvisioningReport.Status.IGNORE);
326                             } else {
327                                 update(binder.getRealmTO(realm, true), beforeObj, result);
328                             }
329 
330                             break;
331 
332                         case DEPROVISION:
333                             for (PushActions action : profile.getActions()) {
334                                 action.beforeDeprovision(profile, realm);
335                             }
336 
337                             if (!profile.getTask().isPerformDelete()) {
338                                 LOG.debug("PushTask not configured for delete");
339                                 result.setStatus(ProvisioningReport.Status.IGNORE);
340                             } else {
341                                 deprovision(realm, beforeObj, result);
342                             }
343 
344                             break;
345 
346                         case UNASSIGN:
347                             for (PushActions action : profile.getActions()) {
348                                 action.beforeUnassign(profile, realm);
349                             }
350 
351                             if (!profile.getTask().isPerformDelete()) {
352                                 LOG.debug("PushTask not configured for delete");
353                                 result.setStatus(ProvisioningReport.Status.IGNORE);
354                             } else {
355                                 unassign(realm, beforeObj, result);
356                             }
357 
358                             break;
359 
360                         case LINK:
361                             for (PushActions action : profile.getActions()) {
362                                 action.beforeLink(profile, realm);
363                             }
364 
365                             if (!profile.getTask().isPerformUpdate()) {
366                                 LOG.debug("PushTask not configured for update");
367                                 result.setStatus(ProvisioningReport.Status.IGNORE);
368                             } else {
369                                 link(realm, false, result);
370                             }
371 
372                             break;
373 
374                         case UNLINK:
375                             for (PushActions action : profile.getActions()) {
376                                 action.beforeUnlink(profile, realm);
377                             }
378 
379                             if (!profile.getTask().isPerformUpdate()) {
380                                 LOG.debug("PushTask not configured for update");
381                                 result.setStatus(ProvisioningReport.Status.IGNORE);
382                             } else {
383                                 link(realm, true, result);
384                             }
385 
386                             break;
387 
388                         case IGNORE:
389                             LOG.debug("Ignored any: {}", realm);
390                             result.setStatus(ProvisioningReport.Status.IGNORE);
391                             break;
392 
393                         default:
394                         // do nothing
395                     }
396                 }
397 
398                 for (PushActions action : profile.getActions()) {
399                     action.after(profile, realm, result);
400                 }
401 
402                 if (result.getStatus() == null) {
403                     result.setStatus(ProvisioningReport.Status.SUCCESS);
404                 }
405 
406                 if (notificationsAvailable || auditRequested) {
407                     resultStatus = AuditElements.Result.SUCCESS;
408                     if (connObjectKey.isPresent() && connObjecKeyValue.isPresent()) {
409                         output = getRemoteObject(
410                                 new ObjectClass(orgUnit.getObjectClass()),
411                                 connObjectKey.get().getExtAttrName(),
412                                 connObjecKeyValue.get(),
413                                 orgUnit.isIgnoreCaseMatch(),
414                                 orgUnit.getItems().stream());
415                     }
416                 }
417             } catch (IgnoreProvisionException e) {
418                 throw e;
419             } catch (Exception e) {
420                 result.setStatus(ProvisioningReport.Status.FAILURE);
421                 result.setMessage(ExceptionUtils.getRootCauseMessage(e));
422 
423                 if (notificationsAvailable || auditRequested) {
424                     resultStatus = AuditElements.Result.FAILURE;
425                     output = e;
426                 }
427 
428                 LOG.warn("Error pushing {} towards {}", realm, profile.getTask().getResource(), e);
429 
430                 for (PushActions action : profile.getActions()) {
431                     action.onError(profile, realm, result, e);
432                 }
433 
434                 throw new JobExecutionException(e);
435             } finally {
436                 if (notificationsAvailable || auditRequested) {
437                     Map<String, Object> jobMap = new HashMap<>();
438                     jobMap.put(AfterHandlingEvent.JOBMAP_KEY, new AfterHandlingEvent(
439                             AuthContextUtils.getWho(),
440                             AuditElements.EventCategoryType.PUSH,
441                             SyncopeConstants.REALM_ANYTYPE.toLowerCase(),
442                             profile.getTask().getResource().getKey(),
443                             operation,
444                             resultStatus,
445                             beforeObj,
446                             output,
447                             realm));
448                     AfterHandlingJob.schedule(scheduler, jobMap);
449                 }
450             }
451         }
452     }
453 
454     private static ResourceOperation toResourceOperation(final UnmatchingRule rule) {
455         switch (rule) {
456             case ASSIGN:
457             case PROVISION:
458                 return ResourceOperation.CREATE;
459             default:
460                 return ResourceOperation.NONE;
461         }
462     }
463 
464     private static ResourceOperation toResourceOperation(final MatchingRule rule) {
465         switch (rule) {
466             case UPDATE:
467                 return ResourceOperation.UPDATE;
468             case DEPROVISION:
469             case UNASSIGN:
470                 return ResourceOperation.DELETE;
471             default:
472                 return ResourceOperation.NONE;
473         }
474     }
475 
476     private static ProvisioningReport.Status toProvisioningReportStatus(final ExecStatus status) {
477         switch (status) {
478             case FAILURE:
479                 return ProvisioningReport.Status.FAILURE;
480 
481             case SUCCESS:
482                 return ProvisioningReport.Status.SUCCESS;
483 
484             case CREATED:
485             case NOT_ATTEMPTED:
486             default:
487                 return ProvisioningReport.Status.IGNORE;
488         }
489     }
490 }