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.text.ParseException;
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Objects;
26  import java.util.Optional;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.stream.Collectors;
29  import java.util.stream.Stream;
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.Provision;
34  import org.apache.syncope.common.lib.types.AnyTypeKind;
35  import org.apache.syncope.common.lib.types.MatchType;
36  import org.apache.syncope.core.persistence.api.attrvalue.validation.ParsingValidationException;
37  import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
38  import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
39  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
40  import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
41  import org.apache.syncope.core.persistence.api.dao.RealmDAO;
42  import org.apache.syncope.core.persistence.api.dao.UserDAO;
43  import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
44  import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
45  import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
46  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
47  import org.apache.syncope.core.persistence.api.entity.Any;
48  import org.apache.syncope.core.persistence.api.entity.AnyType;
49  import org.apache.syncope.core.persistence.api.entity.AnyUtils;
50  import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
51  import org.apache.syncope.core.persistence.api.entity.DerSchema;
52  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
53  import org.apache.syncope.core.persistence.api.entity.Implementation;
54  import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue;
55  import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
56  import org.apache.syncope.core.persistence.api.entity.PlainSchema;
57  import org.apache.syncope.core.persistence.api.entity.Realm;
58  import org.apache.syncope.core.persistence.api.entity.VirSchema;
59  import org.apache.syncope.core.persistence.api.entity.policy.PullCorrelationRuleEntity;
60  import org.apache.syncope.core.provisioning.api.Connector;
61  import org.apache.syncope.core.provisioning.api.IntAttrName;
62  import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
63  import org.apache.syncope.core.provisioning.api.VirAttrHandler;
64  import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
65  import org.apache.syncope.core.provisioning.api.rules.PullCorrelationRule;
66  import org.apache.syncope.core.provisioning.api.rules.PullMatch;
67  import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
68  import org.apache.syncope.core.spring.implementation.ImplementationManager;
69  import org.identityconnectors.framework.common.objects.Attribute;
70  import org.identityconnectors.framework.common.objects.AttributeUtil;
71  import org.identityconnectors.framework.common.objects.ConnectorObject;
72  import org.identityconnectors.framework.common.objects.Name;
73  import org.identityconnectors.framework.common.objects.ObjectClass;
74  import org.identityconnectors.framework.common.objects.SearchResult;
75  import org.identityconnectors.framework.common.objects.SyncDelta;
76  import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
77  import org.identityconnectors.framework.common.objects.SyncDeltaType;
78  import org.identityconnectors.framework.common.objects.SyncToken;
79  import org.identityconnectors.framework.common.objects.Uid;
80  import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
81  import org.identityconnectors.framework.spi.SearchResultsHandler;
82  import org.slf4j.Logger;
83  import org.slf4j.LoggerFactory;
84  import org.springframework.transaction.annotation.Transactional;
85  import org.springframework.util.CollectionUtils;
86  
87  @Transactional(readOnly = true)
88  public class InboundMatcher {
89  
90      protected static final Logger LOG = LoggerFactory.getLogger(InboundMatcher.class);
91  
92      protected final UserDAO userDAO;
93  
94      protected final AnyObjectDAO anyObjectDAO;
95  
96      protected final GroupDAO groupDAO;
97  
98      protected final AnySearchDAO anySearchDAO;
99  
100     protected final RealmDAO realmDAO;
101 
102     protected final VirSchemaDAO virSchemaDAO;
103 
104     protected final ImplementationDAO implementationDAO;
105 
106     protected final VirAttrHandler virAttrHandler;
107 
108     protected final IntAttrNameParser intAttrNameParser;
109 
110     protected final AnyUtilsFactory anyUtilsFactory;
111 
112     protected final Map<String, PullCorrelationRule> perContextPullCorrelationRules = new ConcurrentHashMap<>();
113 
114     public InboundMatcher(
115             final UserDAO userDAO,
116             final AnyObjectDAO anyObjectDAO,
117             final GroupDAO groupDAO,
118             final AnySearchDAO anySearchDAO,
119             final RealmDAO realmDAO,
120             final VirSchemaDAO virSchemaDAO,
121             final ImplementationDAO implementationDAO,
122             final VirAttrHandler virAttrHandler,
123             final IntAttrNameParser intAttrNameParser,
124             final AnyUtilsFactory anyUtilsFactory) {
125 
126         this.userDAO = userDAO;
127         this.anyObjectDAO = anyObjectDAO;
128         this.groupDAO = groupDAO;
129         this.anySearchDAO = anySearchDAO;
130         this.realmDAO = realmDAO;
131         this.virSchemaDAO = virSchemaDAO;
132         this.implementationDAO = implementationDAO;
133         this.virAttrHandler = virAttrHandler;
134         this.intAttrNameParser = intAttrNameParser;
135         this.anyUtilsFactory = anyUtilsFactory;
136     }
137 
138     public Optional<PullMatch> match(
139             final AnyType anyType,
140             final String nameValue,
141             final ExternalResource resource,
142             final Connector connector) {
143 
144         Optional<Provision> provision = resource.getProvisionByAnyType(anyType.getKey());
145         if (provision.isEmpty()) {
146             return Optional.empty();
147         }
148 
149         Stream<Item> mapItems = Stream.concat(
150                 provision.get().getMapping().getItems().stream(),
151                 virSchemaDAO.find(resource.getKey(), anyType.getKey()).stream().map(VirSchema::asLinkingMappingItem));
152 
153         List<ConnectorObject> found = new ArrayList<>();
154 
155         try {
156             Name nameAttr = new Name(nameValue);
157             connector.search(
158                     new ObjectClass(provision.get().getObjectClass()),
159                     provision.get().isIgnoreCaseMatch()
160                     ? FilterBuilder.equalsIgnoreCase(nameAttr)
161                     : FilterBuilder.equalTo(nameAttr),
162                     new SearchResultsHandler() {
163 
164                 @Override
165                 public void handleResult(final SearchResult result) {
166                     // nothing to do
167                 }
168 
169                 @Override
170                 public boolean handle(final ConnectorObject connectorObject) {
171                     return found.add(connectorObject);
172                 }
173             }, MappingUtils.buildOperationOptions(mapItems));
174         } catch (Throwable t) {
175             LOG.warn("While searching for {} ...", nameValue, t);
176         }
177 
178         Optional<PullMatch> result = Optional.empty();
179 
180         if (found.isEmpty()) {
181             LOG.debug("No {} found on {} with {} {}", provision.get().getObjectClass(), resource, Name.NAME, nameValue);
182         } else {
183             if (found.size() > 1) {
184                 LOG.warn("More than one {} found on {} with {} {} - taking first only",
185                         provision.get().getObjectClass(), resource, Name.NAME, nameValue);
186             }
187 
188             ConnectorObject connObj = found.iterator().next();
189             try {
190                 List<PullMatch> matches = match(
191                         new SyncDeltaBuilder().
192                                 setToken(new SyncToken("")).
193                                 setDeltaType(SyncDeltaType.CREATE_OR_UPDATE).
194                                 setObject(connObj).
195                                 build(),
196                         resource,
197                         provision.get(),
198                         anyType.getKind());
199                 if (matches.isEmpty()) {
200                     LOG.debug("No matching {} found for {}, aborting", anyType.getKind(), connObj);
201                 } else {
202                     if (matches.size() > 1) {
203                         LOG.warn("More than one {} found {} - taking first only", anyType.getKind(), matches);
204                     }
205 
206                     result = matches.stream().filter(match -> match.getAny() != null).findFirst();
207                     result.ifPresent(pullMatch -> virAttrHandler.setValues(pullMatch.getAny(), connObj));
208                 }
209             } catch (IllegalArgumentException e) {
210                 LOG.warn(e.getMessage());
211             }
212         }
213 
214         return result;
215     }
216 
217     protected List<Implementation> getTransformers(final Item item) {
218         return item.getTransformers().stream().
219                 map(implementationDAO::find).
220                 filter(Objects::nonNull).
221                 collect(Collectors.toList());
222     }
223 
224     public List<PullMatch> matchByConnObjectKeyValue(
225             final Item connObjectKeyItem,
226             final String connObjectKeyValue,
227             final AnyTypeKind anyTypeKind,
228             final boolean ignoreCaseMatch,
229             final ExternalResource resource) {
230 
231         String finalConnObjectKeyValue = connObjectKeyValue;
232         for (ItemTransformer transformer
233                 : MappingUtils.getItemTransformers(connObjectKeyItem, getTransformers(connObjectKeyItem))) {
234 
235             List<Object> output = transformer.beforePull(
236                     connObjectKeyItem,
237                     null,
238                     List.of(finalConnObjectKeyValue));
239             if (!CollectionUtils.isEmpty(output)) {
240                 finalConnObjectKeyValue = output.get(0).toString();
241             }
242         }
243 
244         List<PullMatch> noMatchResult = List.of(PullCorrelationRule.NO_MATCH);
245 
246         IntAttrName intAttrName;
247         try {
248             intAttrName = intAttrNameParser.parse(connObjectKeyItem.getIntAttrName(), anyTypeKind);
249         } catch (ParseException e) {
250             LOG.error("Invalid intAttrName '{}' specified, ignoring", connObjectKeyItem.getIntAttrName(), e);
251             return noMatchResult;
252         }
253 
254         AnyUtils anyUtils = anyUtilsFactory.getInstance(anyTypeKind);
255 
256         List<Any<?>> anys = new ArrayList<>();
257 
258         if (intAttrName.getField() != null) {
259             switch (intAttrName.getField()) {
260                 case "key":
261                     Optional.ofNullable(anyUtils.dao().find(finalConnObjectKeyValue)).ifPresent(anys::add);
262                     break;
263 
264                 case "username":
265                     if (anyTypeKind == AnyTypeKind.USER && ignoreCaseMatch) {
266                         AnyCond cond = new AnyCond(AttrCond.Type.IEQ);
267                         cond.setSchema("username");
268                         cond.setExpression(finalConnObjectKeyValue);
269                         anys.addAll(anySearchDAO.search(SearchCond.getLeaf(cond), AnyTypeKind.USER));
270                     } else {
271                         Optional.ofNullable(userDAO.findByUsername(finalConnObjectKeyValue)).ifPresent(anys::add);
272                     }
273                     break;
274 
275                 case "name":
276                     if (anyTypeKind == AnyTypeKind.GROUP && ignoreCaseMatch) {
277                         AnyCond cond = new AnyCond(AttrCond.Type.IEQ);
278                         cond.setSchema("name");
279                         cond.setExpression(finalConnObjectKeyValue);
280                         anys.addAll(anySearchDAO.search(SearchCond.getLeaf(cond), AnyTypeKind.GROUP));
281                     } else {
282                         Optional.ofNullable(groupDAO.findByName(finalConnObjectKeyValue)).ifPresent(anys::add);
283                     }
284 
285                     if (anyTypeKind == AnyTypeKind.ANY_OBJECT && ignoreCaseMatch) {
286                         AnyCond cond = new AnyCond(AttrCond.Type.IEQ);
287                         cond.setSchema("name");
288                         cond.setExpression(finalConnObjectKeyValue);
289                         anys.addAll(anySearchDAO.search(SearchCond.getLeaf(cond), AnyTypeKind.ANY_OBJECT));
290                     } else {
291                         anys.addAll(anyObjectDAO.findByName(finalConnObjectKeyValue));
292                     }
293                     break;
294 
295                 default:
296             }
297         } else if (intAttrName.getSchemaType() != null) {
298             switch (intAttrName.getSchemaType()) {
299                 case PLAIN:
300                     PlainAttrValue value = intAttrName.getSchema().isUniqueConstraint()
301                             ? anyUtils.newPlainAttrUniqueValue()
302                             : anyUtils.newPlainAttrValue();
303                     try {
304                         value.parseValue((PlainSchema) intAttrName.getSchema(), finalConnObjectKeyValue);
305                     } catch (ParsingValidationException e) {
306                         LOG.error("While parsing provided {} {}", Uid.NAME, value, e);
307                         value.setStringValue(finalConnObjectKeyValue);
308                     }
309 
310                     if (intAttrName.getSchema().isUniqueConstraint()) {
311                         anyUtils.dao().findByPlainAttrUniqueValue((PlainSchema) intAttrName.getSchema(),
312                                 (PlainAttrUniqueValue) value, ignoreCaseMatch).
313                                 ifPresent(anys::add);
314                     } else {
315                         anys.addAll(anyUtils.dao().findByPlainAttrValue((PlainSchema) intAttrName.getSchema(),
316                                 value, ignoreCaseMatch));
317                     }
318                     break;
319 
320                 case DERIVED:
321                     anys.addAll(anyUtils.dao().findByDerAttrValue((DerSchema) intAttrName.getSchema(),
322                             finalConnObjectKeyValue, ignoreCaseMatch));
323                     break;
324 
325                 default:
326             }
327         }
328 
329         List<PullMatch> result = anys.stream().
330                 map(any -> new PullMatch(MatchType.ANY, any)).
331                 collect(Collectors.toList());
332 
333         if (resource != null) {
334             userDAO.findLinkedAccount(resource, finalConnObjectKeyValue).
335                     map(account -> new PullMatch(MatchType.LINKED_ACCOUNT, account)).
336                     ifPresent(result::add);
337         }
338 
339         return result.isEmpty() ? noMatchResult : result;
340     }
341 
342     protected List<PullMatch> matchByCorrelationRule(
343             final SyncDelta syncDelta,
344             final Provision provision,
345             final PullCorrelationRule rule,
346             final AnyTypeKind type) {
347 
348         List<PullMatch> result = new ArrayList<>();
349 
350         try {
351             result.addAll(anySearchDAO.search(rule.getSearchCond(syncDelta, provision), type).stream().
352                     map(any -> rule.matching(any, syncDelta, provision)).
353                     collect(Collectors.toList()));
354         } catch (Throwable t) {
355             LOG.error("While searching via {}", rule.getClass().getName(), t);
356         }
357 
358         if (result.isEmpty()) {
359             rule.unmatching(syncDelta, provision).ifPresent(result::add);
360         }
361 
362         return result;
363     }
364 
365     protected Optional<PullCorrelationRule> rule(final ExternalResource resource, final Provision provision) {
366         Optional<? extends PullCorrelationRuleEntity> correlationRule = resource.getPullPolicy() == null
367                 ? Optional.empty()
368                 : resource.getPullPolicy().getCorrelationRule(provision.getAnyType());
369 
370         Optional<PullCorrelationRule> rule = Optional.empty();
371         if (correlationRule.isPresent()) {
372             Implementation impl = correlationRule.get().getImplementation();
373             try {
374                 rule = ImplementationManager.buildPullCorrelationRule(
375                         impl,
376                         () -> perContextPullCorrelationRules.get(impl.getKey()),
377                         instance -> perContextPullCorrelationRules.put(impl.getKey(), instance));
378             } catch (Exception e) {
379                 LOG.error("While building {}", impl, e);
380             }
381         }
382 
383         return rule;
384     }
385 
386     /**
387      * Finds internal entities based on external attributes and mapping.
388      *
389      * @param syncDelta change operation, including external attributes
390      * @param resource external resource
391      * @param provision mapping
392      * @param anyTypeKind type kind
393      * @return list of matching users' / groups' / any objects' keys
394      */
395     public List<PullMatch> match(
396             final SyncDelta syncDelta,
397             final ExternalResource resource,
398             final Provision provision,
399             final AnyTypeKind anyTypeKind) {
400 
401         Optional<PullCorrelationRule> rule = rule(resource, provision);
402 
403         List<PullMatch> result = List.of();
404         try {
405             if (rule.isPresent()) {
406                 result = matchByCorrelationRule(syncDelta, provision, rule.get(), anyTypeKind);
407             } else {
408                 String connObjectKeyValue = null;
409 
410                 Optional<Item> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
411                 if (connObjectKeyItem.isPresent()) {
412                     Attribute connObjectKeyAttr = syncDelta.getObject().
413                             getAttributeByName(connObjectKeyItem.get().getExtAttrName());
414                     if (connObjectKeyAttr != null) {
415                         connObjectKeyValue = AttributeUtil.getStringValue(connObjectKeyAttr);
416                     }
417                     // fallback to __UID__
418                     if (connObjectKeyValue == null) {
419                         connObjectKeyValue = syncDelta.getUid().getUidValue();
420                     }
421                 }
422                 if (connObjectKeyValue == null) {
423                     result = List.of(PullCorrelationRule.NO_MATCH);
424                 } else {
425                     result = matchByConnObjectKeyValue(
426                             connObjectKeyItem.get(),
427                             connObjectKeyValue,
428                             anyTypeKind,
429                             provision.isIgnoreCaseMatch(),
430                             resource);
431                 }
432             }
433         } catch (RuntimeException e) {
434             LOG.error("Could not match {} with any existing {}", syncDelta, provision.getAnyType(), e);
435         }
436 
437         if (result.size() == 1 && result.get(0).getMatchTarget() == MatchType.ANY) {
438             virAttrHandler.setValues(result.get(0).getAny(), syncDelta.getObject());
439         }
440 
441         return result;
442     }
443 
444     /**
445      * Finds internal realms based on external attributes and mapping.
446      *
447      * @param syncDelta change operation, including external attributes
448      * @param orgUnit mapping
449      * @return list of matching realms' keys.
450      */
451     @Transactional(readOnly = true)
452     public List<Realm> match(final SyncDelta syncDelta, final OrgUnit orgUnit) {
453         String connObjectKey = null;
454 
455         Optional<Item> connObjectKeyItem = orgUnit.getConnObjectKeyItem();
456         if (connObjectKeyItem.isPresent()) {
457             Attribute connObjectKeyAttr = syncDelta.getObject().
458                     getAttributeByName(connObjectKeyItem.get().getExtAttrName());
459             if (connObjectKeyAttr != null) {
460                 connObjectKey = AttributeUtil.getStringValue(connObjectKeyAttr);
461             }
462         }
463         if (connObjectKey == null) {
464             return List.of();
465         }
466 
467         for (ItemTransformer transformer
468                 : MappingUtils.getItemTransformers(connObjectKeyItem.get(), getTransformers(connObjectKeyItem.get()))) {
469 
470             List<Object> output = transformer.beforePull(
471                     connObjectKeyItem.get(),
472                     null,
473                     List.of(connObjectKey));
474             if (!CollectionUtils.isEmpty(output)) {
475                 connObjectKey = output.get(0).toString();
476             }
477         }
478 
479         List<Realm> result = new ArrayList<>();
480 
481         Realm realm;
482         switch (connObjectKeyItem.get().getIntAttrName()) {
483             case "key":
484                 realm = realmDAO.find(connObjectKey);
485                 if (realm != null) {
486                     result.add(realm);
487                 }
488                 break;
489 
490             case "name":
491                 if (orgUnit.isIgnoreCaseMatch()) {
492                     result.addAll(realmDAO.findDescendants(SyncopeConstants.ROOT_REALM, connObjectKey, -1, -1));
493                 } else {
494                     result.addAll(realmDAO.findByName(connObjectKey).stream().collect(Collectors.toList()));
495                 }
496                 break;
497 
498             case "fullpath":
499                 realm = realmDAO.findByFullPath(connObjectKey);
500                 if (realm != null) {
501                     result.add(realm);
502                 }
503                 break;
504 
505             default:
506         }
507 
508         return result;
509     }
510 }