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;
20  
21  import java.text.ParseException;
22  import java.time.temporal.TemporalAccessor;
23  import java.util.ArrayList;
24  import java.util.Base64;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Objects;
28  import java.util.Optional;
29  import java.util.Set;
30  import java.util.stream.Collectors;
31  import org.apache.commons.jexl3.JexlContext;
32  import org.apache.commons.jexl3.MapContext;
33  import org.apache.commons.lang3.BooleanUtils;
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.commons.lang3.reflect.FieldUtils;
36  import org.apache.commons.lang3.tuple.Pair;
37  import org.apache.syncope.common.lib.Attr;
38  import org.apache.syncope.common.lib.to.AnyObjectTO;
39  import org.apache.syncope.common.lib.to.AnyTO;
40  import org.apache.syncope.common.lib.to.GroupTO;
41  import org.apache.syncope.common.lib.to.GroupableRelatableTO;
42  import org.apache.syncope.common.lib.to.Item;
43  import org.apache.syncope.common.lib.to.Mapping;
44  import org.apache.syncope.common.lib.to.MembershipTO;
45  import org.apache.syncope.common.lib.to.OrgUnit;
46  import org.apache.syncope.common.lib.to.Provision;
47  import org.apache.syncope.common.lib.to.RealmTO;
48  import org.apache.syncope.common.lib.to.UserTO;
49  import org.apache.syncope.common.lib.types.AnyTypeKind;
50  import org.apache.syncope.common.lib.types.AttrSchemaType;
51  import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
52  import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
53  import org.apache.syncope.core.persistence.api.dao.ApplicationDAO;
54  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
55  import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
56  import org.apache.syncope.core.persistence.api.dao.RealmDAO;
57  import org.apache.syncope.core.persistence.api.dao.RelationshipTypeDAO;
58  import org.apache.syncope.core.persistence.api.dao.UserDAO;
59  import org.apache.syncope.core.persistence.api.entity.Any;
60  import org.apache.syncope.core.persistence.api.entity.AnyType;
61  import org.apache.syncope.core.persistence.api.entity.AnyUtils;
62  import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
63  import org.apache.syncope.core.persistence.api.entity.Application;
64  import org.apache.syncope.core.persistence.api.entity.Attributable;
65  import org.apache.syncope.core.persistence.api.entity.DerSchema;
66  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
67  import org.apache.syncope.core.persistence.api.entity.GroupableRelatable;
68  import org.apache.syncope.core.persistence.api.entity.Implementation;
69  import org.apache.syncope.core.persistence.api.entity.Membership;
70  import org.apache.syncope.core.persistence.api.entity.PlainAttr;
71  import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
72  import org.apache.syncope.core.persistence.api.entity.PlainSchema;
73  import org.apache.syncope.core.persistence.api.entity.Realm;
74  import org.apache.syncope.core.persistence.api.entity.Relationship;
75  import org.apache.syncope.core.persistence.api.entity.RelationshipType;
76  import org.apache.syncope.core.persistence.api.entity.VirSchema;
77  import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
78  import org.apache.syncope.core.persistence.api.entity.group.Group;
79  import org.apache.syncope.core.persistence.api.entity.user.Account;
80  import org.apache.syncope.core.persistence.api.entity.user.LAPlainAttr;
81  import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
82  import org.apache.syncope.core.persistence.api.entity.user.User;
83  import org.apache.syncope.core.provisioning.api.AccountGetter;
84  import org.apache.syncope.core.provisioning.api.DerAttrHandler;
85  import org.apache.syncope.core.provisioning.api.IntAttrName;
86  import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
87  import org.apache.syncope.core.provisioning.api.MappingManager;
88  import org.apache.syncope.core.provisioning.api.PlainAttrGetter;
89  import org.apache.syncope.core.provisioning.api.VirAttrHandler;
90  import org.apache.syncope.core.provisioning.api.cache.VirAttrCache;
91  import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheKey;
92  import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
93  import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
94  import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
95  import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
96  import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
97  import org.apache.syncope.core.spring.security.Encryptor;
98  import org.identityconnectors.framework.common.FrameworkUtil;
99  import org.identityconnectors.framework.common.objects.Attribute;
100 import org.identityconnectors.framework.common.objects.AttributeBuilder;
101 import org.identityconnectors.framework.common.objects.AttributeUtil;
102 import org.identityconnectors.framework.common.objects.Name;
103 import org.identityconnectors.framework.common.objects.OperationalAttributes;
104 import org.identityconnectors.framework.common.objects.Uid;
105 import org.slf4j.Logger;
106 import org.slf4j.LoggerFactory;
107 import org.springframework.transaction.annotation.Transactional;
108 
109 public class DefaultMappingManager implements MappingManager {
110 
111     protected static final Logger LOG = LoggerFactory.getLogger(MappingManager.class);
112 
113     protected static final Encryptor ENCRYPTOR = Encryptor.getInstance();
114 
115     protected final AnyTypeDAO anyTypeDAO;
116 
117     protected final UserDAO userDAO;
118 
119     protected final AnyObjectDAO anyObjectDAO;
120 
121     protected final GroupDAO groupDAO;
122 
123     protected final RelationshipTypeDAO relationshipTypeDAO;
124 
125     protected final RealmDAO realmDAO;
126 
127     protected final ApplicationDAO applicationDAO;
128 
129     protected final ImplementationDAO implementationDAO;
130 
131     protected final DerAttrHandler derAttrHandler;
132 
133     protected final VirAttrHandler virAttrHandler;
134 
135     protected final VirAttrCache virAttrCache;
136 
137     protected final AnyUtilsFactory anyUtilsFactory;
138 
139     protected final IntAttrNameParser intAttrNameParser;
140 
141     public DefaultMappingManager(
142             final AnyTypeDAO anyTypeDAO,
143             final UserDAO userDAO,
144             final AnyObjectDAO anyObjectDAO,
145             final GroupDAO groupDAO,
146             final RelationshipTypeDAO relationshipTypeDAO,
147             final RealmDAO realmDAO,
148             final ApplicationDAO applicationDAO,
149             final ImplementationDAO implementationDAO,
150             final DerAttrHandler derAttrHandler,
151             final VirAttrHandler virAttrHandler,
152             final VirAttrCache virAttrCache,
153             final AnyUtilsFactory anyUtilsFactory,
154             final IntAttrNameParser intAttrNameParser) {
155 
156         this.anyTypeDAO = anyTypeDAO;
157         this.userDAO = userDAO;
158         this.anyObjectDAO = anyObjectDAO;
159         this.groupDAO = groupDAO;
160         this.relationshipTypeDAO = relationshipTypeDAO;
161         this.realmDAO = realmDAO;
162         this.applicationDAO = applicationDAO;
163         this.implementationDAO = implementationDAO;
164         this.derAttrHandler = derAttrHandler;
165         this.virAttrHandler = virAttrHandler;
166         this.virAttrCache = virAttrCache;
167         this.anyUtilsFactory = anyUtilsFactory;
168         this.intAttrNameParser = intAttrNameParser;
169     }
170 
171     protected List<Implementation> getTransformers(final Item item) {
172         return item.getTransformers().stream().
173                 map(implementationDAO::find).
174                 filter(Objects::nonNull).
175                 collect(Collectors.toList());
176     }
177 
178     protected String processPreparedAttr(final Pair<String, Attribute> preparedAttr, final Set<Attribute> attributes) {
179         String connObjectKey = null;
180 
181         if (preparedAttr != null) {
182             if (preparedAttr.getLeft() != null) {
183                 connObjectKey = preparedAttr.getLeft();
184             }
185 
186             if (preparedAttr.getRight() != null) {
187                 Attribute alreadyAdded = AttributeUtil.find(preparedAttr.getRight().getName(), attributes);
188 
189                 if (alreadyAdded == null) {
190                     attributes.add(preparedAttr.getRight());
191                 } else {
192                     attributes.remove(alreadyAdded);
193 
194                     Set<Object> values = new HashSet<>();
195                     if (alreadyAdded.getValue() != null && !alreadyAdded.getValue().isEmpty()) {
196                         values.addAll(alreadyAdded.getValue());
197                     }
198 
199                     if (preparedAttr.getRight().getValue() != null) {
200                         values.addAll(preparedAttr.getRight().getValue());
201                     }
202 
203                     attributes.add(AttributeBuilder.build(preparedAttr.getRight().getName(), values));
204                 }
205             }
206         }
207 
208         return connObjectKey;
209     }
210 
211     protected static Name getName(final String evalConnObjectLink, final String connObjectKey) {
212         // If connObjectLink evaluates to an empty string, just use the provided connObjectKey as Name(),
213         // otherwise evaluated connObjectLink expression is taken as Name().
214         Name name;
215         if (StringUtils.isBlank(evalConnObjectLink)) {
216             // add connObjectKey as __NAME__ attribute ...
217             LOG.debug("Add connObjectKey [{}] as {}", connObjectKey, Name.NAME);
218             name = new Name(connObjectKey);
219         } else {
220             LOG.debug("Add connObjectLink [{}] as {}", evalConnObjectLink, Name.NAME);
221             name = new Name(evalConnObjectLink);
222 
223             // connObjectKey not propagated: it will be used to set the value for __UID__ attribute
224             LOG.debug("connObjectKey will be used just as {} attribute", Uid.NAME);
225         }
226 
227         return name;
228     }
229 
230     /**
231      * Build __NAME__ for propagation.
232      * First look if there is a defined connObjectLink for the given resource (and in
233      * this case evaluate as JEXL); otherwise, take given connObjectKey.
234      *
235      * @param any given any object
236      * @param provision external resource
237      * @param connObjectKey connector object key
238      * @return the value to be propagated as __NAME__
239      */
240     protected Name evaluateNAME(final Any<?> any, final Provision provision, final String connObjectKey) {
241         if (StringUtils.isBlank(connObjectKey)) {
242             // LOG error but avoid to throw exception: leave it to the external resource
243             LOG.warn("Missing ConnObjectKey value for {}: ", any.getType().getKey());
244         }
245 
246         // Evaluate connObjectKey expression
247         String connObjectLink = provision.getMapping() == null
248                 ? null
249                 : provision.getMapping().getConnObjectLink();
250         String evalConnObjectLink = null;
251         if (StringUtils.isNotBlank(connObjectLink)) {
252             JexlContext jexlContext = new MapContext();
253             JexlUtils.addFieldsToContext(any, jexlContext);
254             JexlUtils.addPlainAttrsToContext(any.getPlainAttrs(), jexlContext);
255             JexlUtils.addDerAttrsToContext(any, derAttrHandler, jexlContext);
256             evalConnObjectLink = JexlUtils.evaluate(connObjectLink, jexlContext).toString();
257         }
258 
259         return getName(evalConnObjectLink, connObjectKey);
260     }
261 
262     /**
263      * Build __NAME__ for propagation.
264      * First look if there is a defined connObjectLink for the given resource (and in
265      * this case evaluate as JEXL); otherwise, take given connObjectKey.
266      *
267      * @param realm given any object
268      * @param orgUnit external resource
269      * @param connObjectKey connector object key
270      * @return the value to be propagated as __NAME__
271      */
272     protected Name evaluateNAME(final Realm realm, final OrgUnit orgUnit, final String connObjectKey) {
273         if (StringUtils.isBlank(connObjectKey)) {
274             // LOG error but avoid to throw exception: leave it to the external resource
275             LOG.warn("Missing ConnObjectKey value for Realms");
276         }
277 
278         // Evaluate connObjectKey expression
279         String connObjectLink = orgUnit.getConnObjectLink();
280         String evalConnObjectLink = null;
281         if (StringUtils.isNotBlank(connObjectLink)) {
282             JexlContext jexlContext = new MapContext();
283             JexlUtils.addFieldsToContext(realm, jexlContext);
284             evalConnObjectLink = JexlUtils.evaluate(connObjectLink, jexlContext).toString();
285         }
286 
287         return getName(evalConnObjectLink, connObjectKey);
288     }
289 
290     @Transactional(readOnly = true)
291     @Override
292     public Pair<String, Set<Attribute>> prepareAttrsFromAny(
293             final Any<?> any,
294             final String password,
295             final boolean changePwd,
296             final Boolean enable,
297             final ExternalResource resource,
298             final Provision provision) {
299 
300         LOG.debug("Preparing resource attributes for {} with provision {} for attributes {}",
301                 any, provision, any.getPlainAttrs());
302 
303         Set<Attribute> attributes = new HashSet<>();
304         String[] connObjectKeyValue = new String[1];
305 
306         MappingUtils.getPropagationItems(provision.getMapping().getItems().stream()).forEach(mapItem -> {
307             LOG.debug("Processing expression '{}'", mapItem.getIntAttrName());
308 
309             try {
310                 String processedConnObjectKeyValue = processPreparedAttr(
311                         prepareAttr(
312                                 resource,
313                                 provision,
314                                 mapItem,
315                                 any,
316                                 password,
317                                 AccountGetter.DEFAULT,
318                                 AccountGetter.DEFAULT,
319                                 PlainAttrGetter.DEFAULT),
320                         attributes);
321                 if (processedConnObjectKeyValue != null) {
322                     connObjectKeyValue[0] = processedConnObjectKeyValue;
323                 }
324             } catch (Exception e) {
325                 LOG.error("Expression '{}' processing failed", mapItem.getIntAttrName(), e);
326             }
327         });
328 
329         MappingUtils.getConnObjectKeyItem(provision).ifPresent(connObjectKeyItem -> {
330             Attribute connObjectKeyAttr = AttributeUtil.find(connObjectKeyItem.getExtAttrName(), attributes);
331             if (connObjectKeyAttr != null) {
332                 attributes.remove(connObjectKeyAttr);
333                 attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue[0]));
334             }
335             Name name = evaluateNAME(any, provision, connObjectKeyValue[0]);
336             attributes.add(name);
337             if (connObjectKeyAttr == null
338                     && connObjectKeyValue[0] != null && !connObjectKeyValue[0].equals(name.getNameValue())) {
339 
340                 attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue[0]));
341             }
342         });
343 
344         if (enable != null) {
345             attributes.add(AttributeBuilder.buildEnabled(enable));
346         }
347         if (!changePwd) {
348             Attribute pwdAttr = AttributeUtil.find(OperationalAttributes.PASSWORD_NAME, attributes);
349             if (pwdAttr != null) {
350                 attributes.remove(pwdAttr);
351             }
352         }
353 
354         return Pair.of(connObjectKeyValue[0], attributes);
355     }
356 
357     @Transactional(readOnly = true)
358     @Override
359     public Set<Attribute> prepareAttrsFromLinkedAccount(
360             final User user,
361             final LinkedAccount account,
362             final String password,
363             final boolean changePwd,
364             final Provision provision) {
365 
366         LOG.debug("Preparing resource attributes for linked account {} of user {} with provision {} "
367                 + "for user attributes {} with override {}",
368                 account, user, provision, user.getPlainAttrs(), account.getPlainAttrs());
369 
370         Set<Attribute> attributes = new HashSet<>();
371 
372         MappingUtils.getPropagationItems(provision.getMapping().getItems().stream()).forEach(mapItem -> {
373             LOG.debug("Processing expression '{}'", mapItem.getIntAttrName());
374 
375             try {
376                 processPreparedAttr(
377                         prepareAttr(
378                                 account.getResource(),
379                                 provision,
380                                 mapItem,
381                                 user,
382                                 password,
383                                 acct -> account.getUsername() == null ? AccountGetter.DEFAULT.apply(acct) : account,
384                                 acct -> account.getPassword() == null ? AccountGetter.DEFAULT.apply(acct) : account,
385                                 (attributable, schema) -> {
386                                     PlainAttr<?> result = null;
387                                     if (attributable instanceof User) {
388                                         Optional<? extends LAPlainAttr> accountAttr = account.getPlainAttr(schema);
389                                         if (accountAttr.isPresent()) {
390                                             result = accountAttr.get();
391                                         }
392                                     }
393                                     if (result == null) {
394                                         result = PlainAttrGetter.DEFAULT.apply(attributable, schema);
395                                     }
396                                     return result;
397                                 }),
398                         attributes);
399             } catch (Exception e) {
400                 LOG.error("Expression '{}' processing failed", mapItem.getIntAttrName(), e);
401             }
402         });
403 
404         String connObjectKey = account.getConnObjectKeyValue();
405         MappingUtils.getConnObjectKeyItem(provision).ifPresent(connObjectKeyItem -> {
406             Attribute connObjectKeyExtAttr = AttributeUtil.find(connObjectKeyItem.getExtAttrName(), attributes);
407             if (connObjectKeyExtAttr != null) {
408                 attributes.remove(connObjectKeyExtAttr);
409                 attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKey));
410             }
411             Name name = evaluateNAME(user, provision, connObjectKey);
412             attributes.add(name);
413             if (!connObjectKey.equals(name.getNameValue()) && connObjectKeyExtAttr == null) {
414                 attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKey));
415             }
416         });
417 
418         if (account.isSuspended() != null) {
419             attributes.add(AttributeBuilder.buildEnabled(BooleanUtils.negate(account.isSuspended())));
420         }
421         if (!changePwd) {
422             Attribute pwdAttr = AttributeUtil.find(OperationalAttributes.PASSWORD_NAME, attributes);
423             if (pwdAttr != null) {
424                 attributes.remove(pwdAttr);
425             }
426         }
427 
428         return attributes;
429     }
430 
431     protected String getIntValue(final Realm realm, final Item orgUnitItem) {
432         String value = null;
433         switch (orgUnitItem.getIntAttrName()) {
434             case "key":
435                 value = realm.getKey();
436                 break;
437 
438             case "name":
439                 value = realm.getName();
440                 break;
441 
442             case "fullpath":
443                 value = realm.getFullPath();
444                 break;
445 
446             default:
447         }
448 
449         return value;
450     }
451 
452     @Override
453     public Pair<String, Set<Attribute>> prepareAttrsFromRealm(final Realm realm, final OrgUnit orgUnit) {
454         LOG.debug("Preparing resource attributes for {} with orgUnit {}", realm, orgUnit);
455 
456         Set<Attribute> attributes = new HashSet<>();
457         String[] connObjectKeyValue = new String[1];
458 
459         MappingUtils.getPropagationItems(orgUnit.getItems().stream()).forEach(orgUnitItem -> {
460             LOG.debug("Processing expression '{}'", orgUnitItem.getIntAttrName());
461 
462             String value = getIntValue(realm, orgUnitItem);
463 
464             if (orgUnitItem.isConnObjectKey()) {
465                 connObjectKeyValue[0] = value;
466             }
467 
468             Attribute alreadyAdded = AttributeUtil.find(orgUnitItem.getExtAttrName(), attributes);
469             if (alreadyAdded == null) {
470                 if (value == null) {
471                     attributes.add(AttributeBuilder.build(orgUnitItem.getExtAttrName()));
472                 } else {
473                     attributes.add(AttributeBuilder.build(orgUnitItem.getExtAttrName(), value));
474                 }
475             } else if (value != null) {
476                 attributes.remove(alreadyAdded);
477 
478                 Set<Object> values = new HashSet<>();
479                 if (alreadyAdded.getValue() != null && !alreadyAdded.getValue().isEmpty()) {
480                     values.addAll(alreadyAdded.getValue());
481                 }
482                 values.add(value);
483 
484                 attributes.add(AttributeBuilder.build(orgUnitItem.getExtAttrName(), values));
485             }
486         });
487 
488         Optional<Item> connObjectKeyItem = orgUnit.getConnObjectKeyItem();
489         if (connObjectKeyItem.isPresent()) {
490             Attribute connObjectKeyAttr = AttributeUtil.find(connObjectKeyItem.get().getExtAttrName(), attributes);
491             if (connObjectKeyAttr != null) {
492                 attributes.remove(connObjectKeyAttr);
493                 attributes.add(AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKeyValue[0]));
494             }
495             attributes.add(evaluateNAME(realm, orgUnit, connObjectKeyValue[0]));
496         }
497 
498         return Pair.of(connObjectKeyValue[0], attributes);
499     }
500 
501     protected String decodePassword(final Account account) {
502         try {
503             return ENCRYPTOR.decode(account.getPassword(), account.getCipherAlgorithm());
504         } catch (Exception e) {
505             LOG.error("Could not decode password for {}", account, e);
506             return null;
507         }
508     }
509 
510     protected String getPasswordAttrValue(final Account account, final String defaultValue) {
511         String passwordAttrValue;
512         if (account instanceof LinkedAccount) {
513             if (account.getPassword() != null) {
514                 passwordAttrValue = decodePassword(account);
515             } else {
516                 passwordAttrValue = defaultValue;
517             }
518         } else {
519             if (StringUtils.isNotBlank(defaultValue)) {
520                 passwordAttrValue = defaultValue;
521             } else if (account.canDecodeSecrets()) {
522                 passwordAttrValue = decodePassword(account);
523             } else {
524                 passwordAttrValue = null;
525             }
526         }
527 
528         return passwordAttrValue;
529     }
530 
531     @Override
532     public Pair<String, Attribute> prepareAttr(
533             final ExternalResource resource,
534             final Provision provision,
535             final Item item,
536             final Any<?> any,
537             final String password,
538             final AccountGetter usernameAccountGetter,
539             final AccountGetter passwordAccountGetter,
540             final PlainAttrGetter plainAttrGetter) {
541 
542         IntAttrName intAttrName;
543         try {
544             intAttrName = intAttrNameParser.parse(item.getIntAttrName(), any.getType().getKind());
545         } catch (ParseException e) {
546             LOG.error("Invalid intAttrName '{}' specified, ignoring", item.getIntAttrName(), e);
547             return null;
548         }
549 
550         AttrSchemaType schemaType = intAttrName.getSchema() instanceof PlainSchema
551                 ? intAttrName.getSchema().getType()
552                 : AttrSchemaType.String;
553         boolean readOnlyVirSchema = intAttrName.getSchema() instanceof VirSchema
554                 ? intAttrName.getSchema().isReadonly()
555                 : false;
556 
557         Pair<AttrSchemaType, List<PlainAttrValue>> intValues = getIntValues(
558                 resource, provision, item, intAttrName, schemaType, any, usernameAccountGetter, plainAttrGetter);
559         schemaType = intValues.getLeft();
560         List<PlainAttrValue> values = intValues.getRight();
561 
562         LOG.debug("Define mapping for: "
563                 + "\n* ExtAttrName " + item.getExtAttrName()
564                 + "\n* is connObjectKey " + item.isConnObjectKey()
565                 + "\n* is password " + item.isPassword()
566                 + "\n* mandatory condition " + item.getMandatoryCondition()
567                 + "\n* Schema " + intAttrName.getSchema()
568                 + "\n* ClassType " + schemaType.getType().getName()
569                 + "\n* AttrSchemaType " + schemaType
570                 + "\n* Values " + values);
571 
572         Pair<String, Attribute> result;
573         if (readOnlyVirSchema) {
574             result = null;
575         } else {
576             List<Object> objValues = new ArrayList<>();
577 
578             for (PlainAttrValue value : values) {
579                 if (FrameworkUtil.isSupportedAttributeType(schemaType.getType())) {
580                     objValues.add(value.getValue());
581                 } else {
582                     PlainSchema plainSchema = intAttrName.getSchema() instanceof PlainSchema
583                             ? (PlainSchema) intAttrName.getSchema()
584                             : null;
585                     if (plainSchema == null || plainSchema.getType() != schemaType) {
586                         objValues.add(value.getValueAsString(schemaType));
587                     } else {
588                         objValues.add(value.getValueAsString(plainSchema));
589                     }
590                 }
591             }
592 
593             if (item.isConnObjectKey()) {
594                 result = Pair.of(objValues.isEmpty() ? null : objValues.iterator().next().toString(), null);
595             } else if (item.isPassword() && any instanceof User) {
596                 String passwordAttrValue = getPasswordAttrValue(passwordAccountGetter.apply((User) any), password);
597                 if (passwordAttrValue == null) {
598                     result = null;
599                 } else {
600                     result = Pair.of(null, AttributeBuilder.buildPassword(passwordAttrValue.toCharArray()));
601                 }
602             } else {
603                 result = Pair.of(null, objValues.isEmpty()
604                         ? AttributeBuilder.build(item.getExtAttrName())
605                         : AttributeBuilder.build(item.getExtAttrName(), objValues));
606             }
607         }
608 
609         return result;
610     }
611 
612     @Transactional(readOnly = true)
613     @SuppressWarnings("unchecked")
614     @Override
615     public Pair<AttrSchemaType, List<PlainAttrValue>> getIntValues(
616             final ExternalResource resource,
617             final Provision provision,
618             final Item mapItem,
619             final IntAttrName intAttrName,
620             final AttrSchemaType schemaType,
621             final Any<?> any,
622             final AccountGetter usernameAccountGetter,
623             final PlainAttrGetter plainAttrGetter) {
624 
625         LOG.debug("Get internal values for {} as '{}' on {}", any, mapItem.getIntAttrName(), resource);
626 
627         List<Any<?>> references = new ArrayList<>();
628         Membership<?> membership = null;
629         if (intAttrName.getEnclosingGroup() == null
630                 && intAttrName.getRelatedAnyObject() == null
631                 && intAttrName.getRelationshipAnyType() == null
632                 && intAttrName.getRelationshipType() == null
633                 && intAttrName.getRelatedUser() == null) {
634             references.add(any);
635         }
636         if (any instanceof GroupableRelatable) {
637             GroupableRelatable<?, ?, ?, ?, ?> groupableRelatable = (GroupableRelatable<?, ?, ?, ?, ?>) any;
638 
639             if (intAttrName.getEnclosingGroup() != null) {
640                 Group group = groupDAO.findByName(intAttrName.getEnclosingGroup());
641                 if (group == null
642                         || any instanceof User
643                                 ? !userDAO.findAllGroupKeys((User) any).contains(group.getKey())
644                                 : any instanceof AnyObject
645                                         ? !anyObjectDAO.findAllGroupKeys((AnyObject) any).contains(group.getKey())
646                                         : false) {
647 
648                     LOG.warn("No (dyn) membership for {} in {}, ignoring",
649                             intAttrName.getEnclosingGroup(), groupableRelatable);
650                 } else {
651                     references.add(group);
652                 }
653             } else if (intAttrName.getRelatedUser() != null) {
654                 User user = userDAO.findByUsername(intAttrName.getRelatedUser());
655                 if (user == null || user.getRelationships(groupableRelatable.getKey()).isEmpty()) {
656                     LOG.warn("No relationship for {} in {}, ignoring",
657                             intAttrName.getRelatedUser(), groupableRelatable);
658                 } else if (groupableRelatable.getType().getKind() == AnyTypeKind.USER) {
659                     LOG.warn("Users cannot have relationship with other users, ignoring");
660                 } else {
661                     references.add(user);
662                 }
663             } else if (intAttrName.getRelatedAnyObject() != null) {
664                 AnyObject anyObject = anyObjectDAO.find(intAttrName.getRelatedAnyObject());
665                 if (anyObject == null || groupableRelatable.getRelationships(anyObject.getKey()).isEmpty()) {
666                     LOG.warn("No relationship for {} in {}, ignoring",
667                             intAttrName.getRelatedAnyObject(), groupableRelatable);
668                 } else {
669                     references.add(anyObject);
670                 }
671             } else if (intAttrName.getRelationshipAnyType() != null && intAttrName.getRelationshipType() != null) {
672                 RelationshipType relationshipType = relationshipTypeDAO.find(intAttrName.getRelationshipType());
673                 AnyType anyType = anyTypeDAO.find(intAttrName.getRelationshipAnyType());
674                 if (relationshipType == null || groupableRelatable.getRelationships(relationshipType).isEmpty()) {
675                     LOG.warn("No relationship for type {} in {}, ignoring",
676                             intAttrName.getRelationshipType(), groupableRelatable);
677                 } else if (anyType == null) {
678                     LOG.warn("No anyType {}, ignoring", intAttrName.getRelationshipAnyType());
679                 } else {
680                     references.addAll(groupableRelatable.getRelationships(relationshipType).stream().
681                             filter(relationship -> anyType.equals(relationship.getRightEnd().getType())).
682                             map(Relationship::getRightEnd).
683                             collect(Collectors.toList()));
684                 }
685             } else if (intAttrName.getMembershipOfGroup() != null) {
686                 Group group = groupDAO.findByName(intAttrName.getMembershipOfGroup());
687                 membership = groupableRelatable.getMembership(group.getKey()).orElse(null);
688             }
689         }
690         if (references.isEmpty()) {
691             LOG.warn("Could not determine the reference instance for {}", mapItem.getIntAttrName());
692             return Pair.of(schemaType, List.of());
693         }
694 
695         List<PlainAttrValue> values = new ArrayList<>();
696         boolean transform = true;
697 
698         for (Any<?> ref : references) {
699             AnyUtils anyUtils = anyUtilsFactory.getInstance(ref);
700             if (intAttrName.getField() != null) {
701                 PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
702 
703                 switch (intAttrName.getField()) {
704                     case "key":
705                         attrValue.setStringValue(ref.getKey());
706                         values.add(attrValue);
707                         break;
708 
709                     case "username":
710                         if (ref instanceof Account) {
711                             attrValue.setStringValue(usernameAccountGetter.apply((Account) ref).getUsername());
712                             values.add(attrValue);
713                         }
714                         break;
715 
716                     case "realm":
717                         attrValue.setStringValue(ref.getRealm().getFullPath());
718                         values.add(attrValue);
719                         break;
720 
721                     case "password":
722                         // ignore
723                         break;
724 
725                     case "userOwner":
726                     case "groupOwner":
727                         Mapping uMappingTO = provision.getAnyType().equals(AnyTypeKind.USER.name())
728                                 ? provision.getMapping()
729                                 : null;
730                         Mapping gMappingTO = provision.getAnyType().equals(AnyTypeKind.GROUP.name())
731                                 ? provision.getMapping()
732                                 : null;
733 
734                         if (ref instanceof Group) {
735                             Group group = (Group) ref;
736                             String groupOwnerValue = null;
737                             if (group.getUserOwner() != null && uMappingTO != null) {
738                                 groupOwnerValue = getGroupOwnerValue(resource, provision, group.getUserOwner());
739                             }
740                             if (group.getGroupOwner() != null && gMappingTO != null) {
741                                 groupOwnerValue = getGroupOwnerValue(resource, provision, group.getGroupOwner());
742                             }
743 
744                             if (StringUtils.isNotBlank(groupOwnerValue)) {
745                                 attrValue.setStringValue(groupOwnerValue);
746                                 values.add(attrValue);
747                             }
748                         }
749                         break;
750 
751                     case "suspended":
752                         if (ref instanceof User) {
753                             attrValue.setBooleanValue(((User) ref).isSuspended());
754                             values.add(attrValue);
755                         }
756                         break;
757 
758                     case "mustChangePassword":
759                         if (ref instanceof User) {
760                             attrValue.setBooleanValue(((User) ref).isMustChangePassword());
761                             values.add(attrValue);
762                         }
763                         break;
764 
765                     default:
766                         try {
767                         Object fieldValue = FieldUtils.readField(ref, intAttrName.getField(), true);
768                         if (fieldValue instanceof TemporalAccessor) {
769                             // needed because ConnId does not natively supports the Date type
770                             attrValue.setStringValue(FormatUtils.format((TemporalAccessor) fieldValue));
771                         } else if (Boolean.TYPE.isInstance(fieldValue)) {
772                             attrValue.setBooleanValue((Boolean) fieldValue);
773                         } else if (Double.TYPE.isInstance(fieldValue) || Float.TYPE.isInstance(fieldValue)) {
774                             attrValue.setDoubleValue((Double) fieldValue);
775                         } else if (Long.TYPE.isInstance(fieldValue) || Integer.TYPE.isInstance(fieldValue)) {
776                             attrValue.setLongValue((Long) fieldValue);
777                         } else {
778                             attrValue.setStringValue(fieldValue.toString());
779                         }
780                         values.add(attrValue);
781                     } catch (Exception e) {
782                         LOG.error("Could not read value of '{}' from {}", intAttrName.getField(), ref, e);
783                     }
784                 }
785             } else if (intAttrName.getSchemaType() != null) {
786                 switch (intAttrName.getSchemaType()) {
787                     case PLAIN:
788                         PlainAttr<?> attr;
789                         if (membership == null) {
790                             attr = plainAttrGetter.apply((Attributable) ref, intAttrName.getSchema().getKey());
791                         } else {
792                             attr = ((GroupableRelatable<?, ?, ?, ?, ?>) ref).getPlainAttr(
793                                     intAttrName.getSchema().getKey(), membership).orElse(null);
794                         }
795                         if (attr != null) {
796                             if (attr.getUniqueValue() != null) {
797                                 values.add(anyUtils.clonePlainAttrValue(attr.getUniqueValue()));
798                             } else if (attr.getValues() != null) {
799                                 attr.getValues().forEach(value -> values.add(anyUtils.clonePlainAttrValue(value)));
800                             }
801                         }
802                         break;
803 
804                     case DERIVED:
805                         DerSchema derSchema = (DerSchema) intAttrName.getSchema();
806                         String derValue = membership == null
807                                 ? derAttrHandler.getValue(ref, derSchema)
808                                 : derAttrHandler.getValue(ref, membership, derSchema);
809                         if (derValue != null) {
810                             PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
811                             attrValue.setStringValue(derValue);
812                             values.add(attrValue);
813                         }
814                         break;
815 
816                     case VIRTUAL:
817                         // virtual attributes don't get transformed
818                         transform = false;
819 
820                         VirAttrCacheKey cacheKey = new VirAttrCacheKey(
821                                 ref.getType().getKey(), ref.getKey(), intAttrName.getSchema().getKey());
822                         virAttrCache.expire(cacheKey);
823                         LOG.debug("Evicted from cache: {}", cacheKey);
824 
825                         VirSchema virSchema = (VirSchema) intAttrName.getSchema();
826                         List<String> virValues = membership == null
827                                 ? virAttrHandler.getValues(ref, virSchema)
828                                 : virAttrHandler.getValues(ref, membership, virSchema);
829                         virValues.forEach(virValue -> {
830                             PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
831                             attrValue.setStringValue(virValue);
832                             values.add(attrValue);
833                         });
834                         break;
835 
836                     default:
837                 }
838             } else if (intAttrName.getPrivilegesOfApplication() != null && ref instanceof User) {
839                 Application application = applicationDAO.find(intAttrName.getPrivilegesOfApplication());
840                 if (application == null) {
841                     LOG.warn("Invalid application: {}", intAttrName.getPrivilegesOfApplication());
842                 } else {
843                     userDAO.findAllRoles((User) ref).stream().
844                             flatMap(role -> role.getPrivileges(application).stream()).
845                             forEach(privilege -> {
846                                 PlainAttrValue attrValue = anyUtils.newPlainAttrValue();
847                                 attrValue.setStringValue(privilege.getKey());
848                                 values.add(attrValue);
849                             });
850                 }
851             }
852         }
853 
854         LOG.debug("Internal values: {}", values);
855 
856         Pair<AttrSchemaType, List<PlainAttrValue>> transformed = Pair.of(schemaType, values);
857         if (transform) {
858             for (ItemTransformer transformer : MappingUtils.getItemTransformers(mapItem, getTransformers(mapItem))) {
859                 transformed = transformer.beforePropagation(
860                         mapItem, any, transformed.getLeft(), transformed.getRight());
861             }
862             LOG.debug("Transformed values: {}", values);
863         } else {
864             LOG.debug("No transformation occurred");
865         }
866 
867         return transformed;
868     }
869 
870     protected String getGroupOwnerValue(
871             final ExternalResource resource,
872             final Provision provision,
873             final Any<?> any) {
874 
875         Optional<Item> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
876 
877         Pair<String, Attribute> preparedAttr = null;
878         if (connObjectKeyItem.isPresent()) {
879             preparedAttr = prepareAttr(
880                     resource,
881                     provision,
882                     connObjectKeyItem.get(),
883                     any,
884                     null,
885                     AccountGetter.DEFAULT,
886                     AccountGetter.DEFAULT,
887                     PlainAttrGetter.DEFAULT);
888         }
889 
890         return Optional.ofNullable(preparedAttr).
891                 map(attr -> evaluateNAME(any, provision, attr.getKey()).getNameValue()).orElse(null);
892     }
893 
894     @Transactional(readOnly = true)
895     @Override
896     public Optional<String> getConnObjectKeyValue(
897             final Any<?> any,
898             final ExternalResource resource,
899             final Provision provision) {
900 
901         Optional<Item> connObjectKeyItem = provision.getMapping().getConnObjectKeyItem();
902         if (connObjectKeyItem.isEmpty()) {
903             LOG.error("Unable to locate conn object key item for {}", any.getType().getKey());
904             return Optional.empty();
905         }
906         Item mapItem = connObjectKeyItem.get();
907         Pair<AttrSchemaType, List<PlainAttrValue>> intValues;
908         try {
909             intValues = getIntValues(
910                     resource,
911                     provision,
912                     mapItem,
913                     intAttrNameParser.parse(mapItem.getIntAttrName(), any.getType().getKind()),
914                     AttrSchemaType.String,
915                     any,
916                     AccountGetter.DEFAULT,
917                     PlainAttrGetter.DEFAULT);
918         } catch (ParseException e) {
919             LOG.error("Invalid intAttrName '{}' specified, ignoring", mapItem.getIntAttrName(), e);
920             intValues = Pair.of(AttrSchemaType.String, List.of());
921         }
922         return Optional.ofNullable(intValues.getRight().isEmpty()
923                 ? null
924                 : intValues.getRight().get(0).getValueAsString());
925     }
926 
927     @Transactional(readOnly = true)
928     @Override
929     public Optional<String> getConnObjectKeyValue(final Realm realm, final OrgUnit orgUnit) {
930         Optional<Item> connObjectKeyItem = orgUnit.getConnObjectKeyItem();
931         if (connObjectKeyItem.isEmpty()) {
932             LOG.error("Unable to locate conn object key item for Realms");
933             return Optional.empty();
934         }
935         return Optional.ofNullable(getIntValue(realm, connObjectKeyItem.get()));
936     }
937 
938     @Transactional(readOnly = true)
939     @Override
940     public void setIntValues(final Item mapItem, final Attribute attr, final AnyTO anyTO) {
941         List<Object> values = null;
942         if (attr != null) {
943             values = attr.getValue();
944             for (ItemTransformer transformer : MappingUtils.getItemTransformers(mapItem, getTransformers(mapItem))) {
945                 values = transformer.beforePull(mapItem, anyTO, values);
946             }
947         }
948         values = Optional.ofNullable(values).orElse(List.of());
949 
950         IntAttrName intAttrName;
951         try {
952             intAttrName = intAttrNameParser.parse(mapItem.getIntAttrName(), AnyTypeKind.fromTOClass(anyTO.getClass()));
953         } catch (ParseException e) {
954             LOG.error("Invalid intAttrName '{}' specified, ignoring", mapItem.getIntAttrName(), e);
955             return;
956         }
957 
958         if (intAttrName.getField() != null) {
959             switch (intAttrName.getField()) {
960                 case "password":
961                     if (anyTO instanceof UserTO && !values.isEmpty()) {
962                         ((UserTO) anyTO).setPassword(ConnObjectUtils.getPassword(values.get(0)));
963                     }
964                     break;
965 
966                 case "username":
967                     if (anyTO instanceof UserTO) {
968                         ((UserTO) anyTO).setUsername(values.isEmpty() || values.get(0) == null
969                                 ? null
970                                 : values.get(0).toString());
971                     }
972                     break;
973 
974                 case "name":
975                     if (anyTO instanceof GroupTO) {
976                         ((GroupTO) anyTO).setName(values.isEmpty() || values.get(0) == null
977                                 ? null
978                                 : values.get(0).toString());
979                     } else if (anyTO instanceof AnyObjectTO) {
980                         ((AnyObjectTO) anyTO).setName(values.isEmpty() || values.get(0) == null
981                                 ? null
982                                 : values.get(0).toString());
983                     }
984                     break;
985 
986                 case "mustChangePassword":
987                     if (anyTO instanceof UserTO && !values.isEmpty() && values.get(0) != null) {
988                         ((UserTO) anyTO).setMustChangePassword(BooleanUtils.toBoolean(values.get(0).toString()));
989                     }
990                     break;
991 
992                 case "userOwner":
993                 case "groupOwner":
994                     if (anyTO instanceof GroupTO && attr != null) {
995                         // using a special attribute (with schema "", that will be ignored) for carrying the
996                         // GroupOwnerSchema value
997                         Attr attrTO = new Attr();
998                         attrTO.setSchema(StringUtils.EMPTY);
999                         if (values.isEmpty() || values.get(0) == null) {
1000                             attrTO.getValues().add(StringUtils.EMPTY);
1001                         } else {
1002                             attrTO.getValues().add(values.get(0).toString());
1003                         }
1004 
1005                         ((GroupTO) anyTO).getPlainAttrs().add(attrTO);
1006                     }
1007                     break;
1008 
1009                 default:
1010             }
1011         } else if (intAttrName.getSchemaType() != null && attr != null) {
1012             GroupableRelatableTO groupableTO;
1013             Group group;
1014             if (anyTO instanceof GroupableRelatableTO && intAttrName.getMembershipOfGroup() != null) {
1015                 groupableTO = (GroupableRelatableTO) anyTO;
1016                 group = groupDAO.findByName(intAttrName.getMembershipOfGroup());
1017             } else {
1018                 groupableTO = null;
1019                 group = null;
1020             }
1021 
1022             switch (intAttrName.getSchemaType()) {
1023                 case PLAIN:
1024                     Attr attrTO = new Attr();
1025                     attrTO.setSchema(intAttrName.getSchema().getKey());
1026 
1027                     PlainSchema schema = (PlainSchema) intAttrName.getSchema();
1028 
1029                     for (Object value : values) {
1030                         AttrSchemaType schemaType = schema == null ? AttrSchemaType.String : schema.getType();
1031                         if (value != null) {
1032                             if (schemaType == AttrSchemaType.Binary) {
1033                                 attrTO.getValues().add(Base64.getEncoder().encodeToString((byte[]) value));
1034                             } else {
1035                                 attrTO.getValues().add(value.toString());
1036                             }
1037                         }
1038                     }
1039 
1040                     if (groupableTO == null || group == null) {
1041                         anyTO.getPlainAttrs().add(attrTO);
1042                     } else {
1043                         MembershipTO membership = groupableTO.getMembership(group.getKey()).orElseGet(() -> {
1044                             MembershipTO newMemb = new MembershipTO.Builder(group.getKey()).build();
1045                             groupableTO.getMemberships().add(newMemb);
1046                             return newMemb;
1047                         });
1048                         membership.getPlainAttrs().add(attrTO);
1049                     }
1050                     break;
1051 
1052                 case DERIVED:
1053                     attrTO = new Attr();
1054                     attrTO.setSchema(intAttrName.getSchema().getKey());
1055 
1056                     if (groupableTO == null || group == null) {
1057                         anyTO.getDerAttrs().add(attrTO);
1058                     } else {
1059                         MembershipTO membership = groupableTO.getMembership(group.getKey()).orElseGet(() -> {
1060                             MembershipTO newMemb = new MembershipTO.Builder(group.getKey()).build();
1061                             groupableTO.getMemberships().add(newMemb);
1062                             return newMemb;
1063                         });
1064                         membership.getDerAttrs().add(attrTO);
1065                     }
1066                     break;
1067 
1068                 case VIRTUAL:
1069                     attrTO = new Attr();
1070                     attrTO.setSchema(intAttrName.getSchema().getKey());
1071 
1072                     // virtual attributes don't get transformed, iterate over original attr.getValue()
1073                     if (attr.getValue() != null && !attr.getValue().isEmpty()) {
1074                         attr.getValue().stream().
1075                                 filter(Objects::nonNull).
1076                                 forEachOrdered(value -> attrTO.getValues().add(value.toString()));
1077                     }
1078 
1079                     if (groupableTO == null || group == null) {
1080                         anyTO.getVirAttrs().add(attrTO);
1081                     } else {
1082                         MembershipTO membership = groupableTO.getMembership(group.getKey()).orElseGet(() -> {
1083                             MembershipTO newMemb = new MembershipTO.Builder(group.getKey()).build();
1084                             groupableTO.getMemberships().add(newMemb);
1085                             return newMemb;
1086                         });
1087                         membership.getVirAttrs().add(attrTO);
1088                     }
1089                     break;
1090 
1091                 default:
1092             }
1093         }
1094     }
1095 
1096     @Override
1097     public void setIntValues(final Item item, final Attribute attr, final RealmTO realmTO) {
1098         List<Object> values = null;
1099         if (attr != null) {
1100             values = attr.getValue();
1101             for (ItemTransformer transformer : MappingUtils.getItemTransformers(item, getTransformers(item))) {
1102                 values = transformer.beforePull(item, realmTO, values);
1103             }
1104         }
1105 
1106         if (values != null && !values.isEmpty() && values.get(0) != null) {
1107             switch (item.getIntAttrName()) {
1108                 case "name":
1109                     realmTO.setName(values.get(0).toString());
1110                     break;
1111 
1112                 case "fullpath":
1113                     String parentFullPath = StringUtils.substringBeforeLast(values.get(0).toString(), "/");
1114                     Realm parent = realmDAO.findByFullPath(parentFullPath);
1115                     if (parent == null) {
1116                         LOG.warn("Could not find Realm with path {}, ignoring", parentFullPath);
1117                     } else {
1118                         realmTO.setParent(parent.getFullPath());
1119                     }
1120                     break;
1121 
1122                 default:
1123             }
1124         }
1125     }
1126 
1127     @Transactional(readOnly = true)
1128     @Override
1129     public boolean hasMustChangePassword(final Provision provision) {
1130         return provision != null && provision.getMapping() != null
1131                 && provision.getMapping().getItems().stream().
1132                         anyMatch(mappingItem -> "mustChangePassword".equals(mappingItem.getIntAttrName()));
1133     }
1134 }