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.logic;
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.Collectors;
28  import org.apache.commons.jexl3.MapContext;
29  import org.apache.commons.lang3.BooleanUtils;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.commons.lang3.tuple.Pair;
32  import org.apache.syncope.common.lib.AnyOperations;
33  import org.apache.syncope.common.lib.Attr;
34  import org.apache.syncope.common.lib.EntityTOUtils;
35  import org.apache.syncope.common.lib.SyncopeConstants;
36  import org.apache.syncope.common.lib.request.AttrPatch;
37  import org.apache.syncope.common.lib.request.GroupCR;
38  import org.apache.syncope.common.lib.request.GroupUR;
39  import org.apache.syncope.common.lib.request.PasswordPatch;
40  import org.apache.syncope.common.lib.request.StatusR;
41  import org.apache.syncope.common.lib.request.StringReplacePatchItem;
42  import org.apache.syncope.common.lib.request.UserCR;
43  import org.apache.syncope.common.lib.request.UserUR;
44  import org.apache.syncope.common.lib.scim.SCIMComplexConf;
45  import org.apache.syncope.common.lib.scim.SCIMConf;
46  import org.apache.syncope.common.lib.scim.SCIMEnterpriseUserConf;
47  import org.apache.syncope.common.lib.scim.SCIMManagerConf;
48  import org.apache.syncope.common.lib.scim.SCIMUserAddressConf;
49  import org.apache.syncope.common.lib.to.GroupTO;
50  import org.apache.syncope.common.lib.to.MembershipTO;
51  import org.apache.syncope.common.lib.to.UserTO;
52  import org.apache.syncope.common.lib.types.PatchOperation;
53  import org.apache.syncope.common.lib.types.StatusRType;
54  import org.apache.syncope.core.logic.scim.SCIMConfManager;
55  import org.apache.syncope.core.persistence.api.dao.AnyDAO;
56  import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
57  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
58  import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
59  import org.apache.syncope.core.spring.security.AuthDataAccessor;
60  import org.apache.syncope.ext.scimv2.api.BadRequestException;
61  import org.apache.syncope.ext.scimv2.api.data.Group;
62  import org.apache.syncope.ext.scimv2.api.data.Member;
63  import org.apache.syncope.ext.scimv2.api.data.Meta;
64  import org.apache.syncope.ext.scimv2.api.data.SCIMComplexValue;
65  import org.apache.syncope.ext.scimv2.api.data.SCIMEnterpriseInfo;
66  import org.apache.syncope.ext.scimv2.api.data.SCIMGroup;
67  import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOperation;
68  import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
69  import org.apache.syncope.ext.scimv2.api.data.SCIMUserAddress;
70  import org.apache.syncope.ext.scimv2.api.data.SCIMUserManager;
71  import org.apache.syncope.ext.scimv2.api.data.SCIMUserName;
72  import org.apache.syncope.ext.scimv2.api.data.Value;
73  import org.apache.syncope.ext.scimv2.api.type.ErrorType;
74  import org.apache.syncope.ext.scimv2.api.type.Function;
75  import org.apache.syncope.ext.scimv2.api.type.PatchOp;
76  import org.apache.syncope.ext.scimv2.api.type.Resource;
77  import org.slf4j.Logger;
78  import org.slf4j.LoggerFactory;
79  import org.springframework.util.CollectionUtils;
80  
81  public class SCIMDataBinder {
82  
83      protected static final Logger LOG = LoggerFactory.getLogger(SCIMDataBinder.class);
84  
85      protected static final List<String> USER_SCHEMAS = List.of(Resource.User.schema());
86  
87      protected static final List<String> ENTERPRISE_USER_SCHEMAS =
88              List.of(Resource.User.schema(), Resource.EnterpriseUser.schema());
89  
90      protected static final List<String> GROUP_SCHEMAS = List.of(Resource.Group.schema());
91  
92      /**
93       * Translates the given SCIM filter into the equivalent JEXL expression.
94       *
95       * @param filter SCIM filter according to https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2
96       * @return translated JEXL expression; see https://commons.apache.org/proper/commons-jexl/reference/syntax.html
97       * */
98      public static String filter2JexlExpression(final String filter) {
99          String jexlExpression = filter.
100                 replace(" co ", " =~ ").
101                 replace(" sw ", " =^ ").
102                 replace(" ew ", " =$ ");
103 
104         boolean endsWithPR = jexlExpression.endsWith(" pr");
105         int pr = endsWithPR ? jexlExpression.indexOf(" pr") : jexlExpression.indexOf(" pr ");
106         while (pr != -1) {
107             String before = jexlExpression.substring(0, pr);
108             int start = before.indexOf(' ') == -1 ? 0 : jexlExpression.substring(0, pr).lastIndexOf(' ', pr) + 1;
109             String literal = jexlExpression.substring(start, pr);
110 
111             endsWithPR = jexlExpression.endsWith(" pr");
112             jexlExpression = jexlExpression.replace(
113                     literal + " pr" + (endsWithPR ? "" : " "),
114                     "not(empty(" + literal + "))" + (endsWithPR ? "" : " "));
115 
116             pr = endsWithPR ? jexlExpression.indexOf(" pr") : jexlExpression.indexOf(" pr ");
117         }
118 
119         return jexlExpression;
120     }
121 
122     protected final SCIMConfManager confManager;
123 
124     protected final UserLogic userLogic;
125 
126     protected final AuthDataAccessor authDataAccessor;
127 
128     public SCIMDataBinder(
129             final SCIMConfManager confManager,
130             final UserLogic userLogic,
131             final AuthDataAccessor authDataAccessor) {
132 
133         this.confManager = confManager;
134         this.userLogic = userLogic;
135         this.authDataAccessor = authDataAccessor;
136     }
137 
138     protected <E extends Enum<?>> void fill(
139             final Map<String, Attr> attrs,
140             final List<SCIMComplexConf<E>> confs,
141             final List<SCIMComplexValue> values) {
142 
143         confs.forEach(conf -> {
144             SCIMComplexValue value = new SCIMComplexValue();
145 
146             if (conf.getValue() != null && attrs.containsKey(conf.getValue())) {
147                 value.setValue(attrs.get(conf.getValue()).getValues().get(0));
148             }
149             if (conf.getDisplay() != null && attrs.containsKey(conf.getDisplay())) {
150                 value.setDisplay(attrs.get(conf.getDisplay()).getValues().get(0));
151             }
152             if (conf.getType() != null) {
153                 value.setType(conf.getType().name());
154             }
155 
156             value.setPrimary(conf.isPrimary());
157 
158             if (!value.isEmpty()) {
159                 values.add(value);
160             }
161         });
162     }
163 
164     protected boolean output(
165             final List<String> attributes,
166             final List<String> excludedAttributes,
167             final String schema) {
168 
169         return (attributes.isEmpty() || attributes.contains(schema))
170                 && (excludedAttributes.isEmpty() || !excludedAttributes.contains(schema));
171     }
172 
173     protected <T> T output(
174             final List<String> attributes,
175             final List<String> excludedAttributes,
176             final String schema,
177             final T value) {
178 
179         return output(attributes, excludedAttributes, schema)
180                 ? value
181                 : null;
182     }
183 
184     public SCIMUser toSCIMUser(
185             final UserTO userTO,
186             final String location,
187             final List<String> attributes,
188             final List<String> excludedAttributes) {
189 
190         SCIMConf conf = confManager.get();
191 
192         List<String> schemas = new ArrayList<>();
193         schemas.add(Resource.User.schema());
194         if (conf.getEnterpriseUserConf() != null) {
195             schemas.add(Resource.EnterpriseUser.schema());
196         }
197 
198         SCIMUser user = new SCIMUser(
199                 userTO.getKey(),
200                 schemas,
201                 new Meta(
202                         Resource.User,
203                         userTO.getCreationDate(),
204                         Optional.ofNullable(userTO.getLastChangeDate()).orElse(userTO.getCreationDate()),
205                         userTO.getETagValue(),
206                         location),
207                 output(attributes, excludedAttributes, "userName", userTO.getUsername()),
208                 !userTO.isSuspended());
209 
210         Map<String, Attr> attrs = new HashMap<>();
211         attrs.putAll(EntityTOUtils.buildAttrMap(userTO.getPlainAttrs()));
212         attrs.putAll(EntityTOUtils.buildAttrMap(userTO.getDerAttrs()));
213         attrs.putAll(EntityTOUtils.buildAttrMap(userTO.getVirAttrs()));
214         attrs.put("username", new Attr.Builder("username").value(userTO.getUsername()).build());
215 
216         if (conf.getUserConf() != null) {
217             if (output(attributes, excludedAttributes, "externalId")
218                     && conf.getUserConf().getExternalId() != null
219                     && attrs.containsKey(conf.getUserConf().getExternalId())) {
220 
221                 user.setExternalId(attrs.get(conf.getUserConf().getExternalId()).getValues().get(0));
222             }
223 
224             if (output(attributes, excludedAttributes, "name") && conf.getUserConf().getName() != null) {
225                 SCIMUserName name = new SCIMUserName();
226 
227                 if (conf.getUserConf().getName().getFamilyName() != null
228                         && attrs.containsKey(conf.getUserConf().getName().getFamilyName())) {
229 
230                     name.setFamilyName(attrs.get(conf.getUserConf().getName().getFamilyName()).getValues().get(0));
231                 }
232                 if (conf.getUserConf().getName().getFormatted() != null
233                         && attrs.containsKey(conf.getUserConf().getName().getFormatted())) {
234 
235                     name.setFormatted(attrs.get(conf.getUserConf().getName().getFormatted()).getValues().get(0));
236                 }
237                 if (conf.getUserConf().getName().getGivenName() != null
238                         && attrs.containsKey(conf.getUserConf().getName().getGivenName())) {
239 
240                     name.setGivenName(attrs.get(conf.getUserConf().getName().getGivenName()).getValues().get(0));
241                 }
242                 if (conf.getUserConf().getName().getHonorificPrefix() != null
243                         && attrs.containsKey(conf.getUserConf().getName().getHonorificPrefix())) {
244 
245                     name.setHonorificPrefix(
246                             attrs.get(conf.getUserConf().getName().getHonorificPrefix()).getValues().get(0));
247                 }
248                 if (conf.getUserConf().getName().getHonorificSuffix() != null
249                         && attrs.containsKey(conf.getUserConf().getName().getHonorificSuffix())) {
250 
251                     name.setHonorificSuffix(
252                             attrs.get(conf.getUserConf().getName().getHonorificSuffix()).getValues().get(0));
253                 }
254                 if (conf.getUserConf().getName().getMiddleName() != null
255                         && attrs.containsKey(conf.getUserConf().getName().getMiddleName())) {
256 
257                     name.setMiddleName(attrs.get(conf.getUserConf().getName().getMiddleName()).getValues().get(0));
258                 }
259 
260                 if (!name.isEmpty()) {
261                     user.setName(name);
262                 }
263             }
264 
265             if (output(attributes, excludedAttributes, "displayName")
266                     && conf.getUserConf().getDisplayName() != null
267                     && attrs.containsKey(conf.getUserConf().getDisplayName())) {
268 
269                 user.setDisplayName(attrs.get(conf.getUserConf().getDisplayName()).getValues().get(0));
270             }
271             if (output(attributes, excludedAttributes, "nickName")
272                     && conf.getUserConf().getNickName() != null
273                     && attrs.containsKey(conf.getUserConf().getNickName())) {
274 
275                 user.setNickName(attrs.get(conf.getUserConf().getNickName()).getValues().get(0));
276             }
277             if (output(attributes, excludedAttributes, "profileUrl")
278                     && conf.getUserConf().getProfileUrl() != null
279                     && attrs.containsKey(conf.getUserConf().getProfileUrl())) {
280 
281                 user.setProfileUrl(attrs.get(conf.getUserConf().getProfileUrl()).getValues().get(0));
282             }
283             if (output(attributes, excludedAttributes, "title")
284                     && conf.getUserConf().getTitle() != null
285                     && attrs.containsKey(conf.getUserConf().getTitle())) {
286 
287                 user.setTitle(attrs.get(conf.getUserConf().getTitle()).getValues().get(0));
288             }
289             if (output(attributes, excludedAttributes, "userType")
290                     && conf.getUserConf().getUserType() != null
291                     && attrs.containsKey(conf.getUserConf().getUserType())) {
292 
293                 user.setUserType(attrs.get(conf.getUserConf().getUserType()).getValues().get(0));
294             }
295             if (output(attributes, excludedAttributes, "preferredLanguage")
296                     && conf.getUserConf().getPreferredLanguage() != null
297                     && attrs.containsKey(conf.getUserConf().getPreferredLanguage())) {
298 
299                 user.setPreferredLanguage(attrs.get(conf.getUserConf().getPreferredLanguage()).getValues().get(0));
300             }
301             if (output(attributes, excludedAttributes, "locale")
302                     && conf.getUserConf().getLocale() != null
303                     && attrs.containsKey(conf.getUserConf().getLocale())) {
304 
305                 user.setLocale(attrs.get(conf.getUserConf().getLocale()).getValues().get(0));
306             }
307             if (output(attributes, excludedAttributes, "timezone")
308                     && conf.getUserConf().getTimezone() != null
309                     && attrs.containsKey(conf.getUserConf().getTimezone())) {
310 
311                 user.setTimezone(attrs.get(conf.getUserConf().getTimezone()).getValues().get(0));
312             }
313 
314             if (output(attributes, excludedAttributes, "emails")) {
315                 fill(attrs, conf.getUserConf().getEmails(), user.getEmails());
316             }
317             if (output(attributes, excludedAttributes, "phoneNumbers")) {
318                 fill(attrs, conf.getUserConf().getPhoneNumbers(), user.getPhoneNumbers());
319             }
320             if (output(attributes, excludedAttributes, "ims")) {
321                 fill(attrs, conf.getUserConf().getIms(), user.getIms());
322             }
323             if (output(attributes, excludedAttributes, "photos")) {
324                 fill(attrs, conf.getUserConf().getPhotos(), user.getPhotos());
325             }
326             if (output(attributes, excludedAttributes, "addresses")) {
327                 conf.getUserConf().getAddresses().forEach(addressConf -> {
328                     SCIMUserAddress address = new SCIMUserAddress();
329 
330                     if (addressConf.getFormatted() != null && attrs.containsKey(addressConf.getFormatted())) {
331                         address.setFormatted(attrs.get(addressConf.getFormatted()).getValues().get(0));
332                     }
333                     if (addressConf.getStreetAddress() != null && attrs.containsKey(addressConf.getStreetAddress())) {
334                         address.setStreetAddress(attrs.get(addressConf.getStreetAddress()).getValues().get(0));
335                     }
336                     if (addressConf.getLocality() != null && attrs.containsKey(addressConf.getLocality())) {
337                         address.setLocality(attrs.get(addressConf.getLocality()).getValues().get(0));
338                     }
339                     if (addressConf.getRegion() != null && attrs.containsKey(addressConf.getRegion())) {
340                         address.setRegion(attrs.get(addressConf.getRegion()).getValues().get(0));
341                     }
342                     if (addressConf.getCountry() != null && attrs.containsKey(addressConf.getCountry())) {
343                         address.setCountry(attrs.get(addressConf.getCountry()).getValues().get(0));
344                     }
345                     if (addressConf.getType() != null) {
346                         address.setType(addressConf.getType().name());
347                     }
348                     if (addressConf.isPrimary()) {
349                         address.setPrimary(true);
350                     }
351 
352                     if (!address.isEmpty()) {
353                         user.getAddresses().add(address);
354                     }
355                 });
356             }
357             if (output(attributes, excludedAttributes, "x509Certificates")) {
358                 conf.getUserConf().getX509Certificates().stream().filter(attrs::containsKey).
359                         forEach(cert -> user.getX509Certificates().add(new Value(attrs.get(cert).getValues().get(0))));
360             }
361         }
362 
363         if (conf.getEnterpriseUserConf() != null) {
364             SCIMEnterpriseInfo enterpriseInfo = new SCIMEnterpriseInfo();
365 
366             if (output(attributes, excludedAttributes, "employeeNumber")
367                     && conf.getEnterpriseUserConf().getEmployeeNumber() != null
368                     && attrs.containsKey(conf.getEnterpriseUserConf().getEmployeeNumber())) {
369 
370                 enterpriseInfo.setEmployeeNumber(
371                         attrs.get(conf.getEnterpriseUserConf().getEmployeeNumber()).getValues().get(0));
372             }
373             if (output(attributes, excludedAttributes, "costCenter")
374                     && conf.getEnterpriseUserConf().getCostCenter() != null
375                     && attrs.containsKey(conf.getEnterpriseUserConf().getCostCenter())) {
376 
377                 enterpriseInfo.setCostCenter(
378                         attrs.get(conf.getEnterpriseUserConf().getCostCenter()).getValues().get(0));
379             }
380             if (output(attributes, excludedAttributes, "organization")
381                     && conf.getEnterpriseUserConf().getOrganization() != null
382                     && attrs.containsKey(conf.getEnterpriseUserConf().getOrganization())) {
383 
384                 enterpriseInfo.setOrganization(
385                         attrs.get(conf.getEnterpriseUserConf().getOrganization()).getValues().get(0));
386             }
387             if (output(attributes, excludedAttributes, "division")
388                     && conf.getEnterpriseUserConf().getDivision() != null
389                     && attrs.containsKey(conf.getEnterpriseUserConf().getDivision())) {
390 
391                 enterpriseInfo.setDivision(
392                         attrs.get(conf.getEnterpriseUserConf().getDivision()).getValues().get(0));
393             }
394             if (output(attributes, excludedAttributes, "department")
395                     && conf.getEnterpriseUserConf().getDepartment() != null
396                     && attrs.containsKey(conf.getEnterpriseUserConf().getDepartment())) {
397 
398                 enterpriseInfo.setDepartment(
399                         attrs.get(conf.getEnterpriseUserConf().getDepartment()).getValues().get(0));
400             }
401             if (output(attributes, excludedAttributes, "manager")
402                     && conf.getEnterpriseUserConf().getManager() != null) {
403 
404                 SCIMUserManager manager = new SCIMUserManager();
405 
406                 if (conf.getEnterpriseUserConf().getManager().getKey() != null
407                         && attrs.containsKey(conf.getEnterpriseUserConf().getManager().getKey())) {
408 
409                     try {
410                         UserTO userManager = userLogic.read(attrs.get(
411                                 conf.getEnterpriseUserConf().getManager().getKey()).getValues().get(0));
412                         manager.setValue(userManager.getKey());
413                         manager.setRef(
414                                 StringUtils.substringBefore(location, "/Users") + "/Users/" + userManager.getKey());
415 
416                         if (conf.getEnterpriseUserConf().getManager().getDisplayName() != null) {
417                             Attr displayName = userManager.getPlainAttr(
418                                     conf.getEnterpriseUserConf().getManager().getDisplayName()).orElse(null);
419                             if (displayName == null) {
420                                 displayName = userManager.getDerAttr(
421                                         conf.getEnterpriseUserConf().getManager().getDisplayName()).orElse(null);
422                             }
423                             if (displayName == null) {
424                                 displayName = userManager.getVirAttr(
425                                         conf.getEnterpriseUserConf().getManager().getDisplayName()).orElse(null);
426                             }
427                             if (displayName != null) {
428                                 manager.setDisplayName(displayName.getValues().get(0));
429                             }
430                         }
431                     } catch (Exception e) {
432                         LOG.error("Could not read user {}", conf.getEnterpriseUserConf().getManager().getKey(), e);
433                     }
434                 }
435 
436                 if (!manager.isEmpty()) {
437                     enterpriseInfo.setManager(manager);
438                 }
439             }
440 
441             if (!enterpriseInfo.isEmpty()) {
442                 user.setEnterpriseInfo(enterpriseInfo);
443             }
444         }
445 
446         if (output(attributes, excludedAttributes, "groups")) {
447             userTO.getMemberships().forEach(membership -> user.getGroups().add(new Group(
448                     membership.getGroupKey(),
449                     StringUtils.substringBefore(location, "/Users") + "/Groups/" + membership.getGroupKey(),
450                     membership.getGroupName(),
451                     Function.direct)));
452             userTO.getDynMemberships().forEach(membership -> user.getGroups().add(new Group(
453                     membership.getGroupKey(),
454                     StringUtils.substringBefore(location, "/Users") + "/Groups/" + membership.getGroupKey(),
455                     membership.getGroupName(),
456                     Function.indirect)));
457         }
458 
459         if (output(attributes, excludedAttributes, "entitlements")) {
460             authDataAccessor.getAuthorities(userTO.getUsername(), null).forEach(authority -> user.getEntitlements().
461                     add(new Value(authority.getAuthority() + " on Realm(s) " + authority.getRealms())));
462         }
463 
464         if (output(attributes, excludedAttributes, "roles")) {
465             userTO.getRoles().forEach(role -> user.getRoles().add(new Value(role)));
466         }
467 
468         return user;
469     }
470 
471     protected void setAttribute(
472             final UserTO userTO,
473             final String schema,
474             final String value) {
475 
476         if (schema == null || value == null) {
477             return;
478         }
479 
480         switch (schema) {
481             case "username":
482                 userTO.setUsername(value);
483                 break;
484 
485             default:
486                 userTO.getPlainAttrs().add(new Attr.Builder(schema).value(value).build());
487         }
488     }
489 
490     protected <E extends Enum<?>> void setAttribute(
491             final Set<Attr> attrs,
492             final List<SCIMComplexConf<E>> confs,
493             final List<SCIMComplexValue> values) {
494 
495         values.stream().filter(value -> value.getType() != null).forEach(value -> confs.stream().
496                 filter(object -> value.getType().equals(object.getType().name())).findFirst().
497                 ifPresent(conf -> attrs.add(
498                 new Attr.Builder(conf.getValue()).value(value.getValue()).build())));
499     }
500 
501     public UserTO toUserTO(final SCIMUser user, final boolean checkSchemas) {
502         if (checkSchemas
503                 && !USER_SCHEMAS.equals(user.getSchemas())
504                 && !ENTERPRISE_USER_SCHEMAS.equals(user.getSchemas())) {
505 
506             throw new BadRequestException(ErrorType.invalidValue);
507         }
508 
509         UserTO userTO = new UserTO();
510         userTO.setRealm(SyncopeConstants.ROOT_REALM);
511         userTO.setKey(user.getId());
512         userTO.setPassword(user.getPassword());
513         userTO.setUsername(user.getUserName());
514 
515         SCIMConf conf = confManager.get();
516 
517         if (conf.getUserConf() != null) {
518             setAttribute(
519                     userTO,
520                     conf.getUserConf().getExternalId(),
521                     user.getExternalId());
522 
523             if (conf.getUserConf().getName() != null && user.getName() != null) {
524                 setAttribute(
525                         userTO,
526                         conf.getUserConf().getName().getFamilyName(),
527                         user.getName().getFamilyName());
528 
529                 setAttribute(
530                         userTO,
531                         conf.getUserConf().getName().getFormatted(),
532                         user.getName().getFormatted());
533 
534                 setAttribute(
535                         userTO,
536                         conf.getUserConf().getName().getGivenName(),
537                         user.getName().getGivenName());
538 
539                 setAttribute(
540                         userTO,
541                         conf.getUserConf().getName().getHonorificPrefix(),
542                         user.getName().getHonorificPrefix());
543 
544                 setAttribute(
545                         userTO,
546                         conf.getUserConf().getName().getHonorificSuffix(),
547                         user.getName().getHonorificSuffix());
548 
549                 setAttribute(
550                         userTO,
551                         conf.getUserConf().getName().getMiddleName(),
552                         user.getName().getMiddleName());
553             }
554 
555             setAttribute(
556                     userTO,
557                     conf.getUserConf().getDisplayName(),
558                     user.getDisplayName());
559 
560             setAttribute(
561                     userTO,
562                     conf.getUserConf().getNickName(),
563                     user.getNickName());
564 
565             setAttribute(
566                     userTO,
567                     conf.getUserConf().getProfileUrl(),
568                     user.getProfileUrl());
569 
570             setAttribute(
571                     userTO,
572                     conf.getUserConf().getTitle(),
573                     user.getTitle());
574 
575             setAttribute(
576                     userTO,
577                     conf.getUserConf().getUserType(),
578                     user.getUserType());
579 
580             setAttribute(
581                     userTO,
582                     conf.getUserConf().getPreferredLanguage(),
583                     user.getPreferredLanguage());
584 
585             setAttribute(
586                     userTO,
587                     conf.getUserConf().getLocale(),
588                     user.getLocale());
589 
590             setAttribute(
591                     userTO,
592                     conf.getUserConf().getTimezone(),
593                     user.getTimezone());
594 
595             setAttribute(userTO.getPlainAttrs(), conf.getUserConf().getEmails(), user.getEmails());
596             setAttribute(userTO.getPlainAttrs(), conf.getUserConf().getPhoneNumbers(), user.getPhoneNumbers());
597             setAttribute(userTO.getPlainAttrs(), conf.getUserConf().getIms(), user.getIms());
598             setAttribute(userTO.getPlainAttrs(), conf.getUserConf().getPhotos(), user.getPhotos());
599 
600             user.getAddresses().stream().filter(address -> address.getType() != null).
601                     forEach(address -> conf.getUserConf().getAddresses().stream().
602                     filter(object -> address.getType().equals(object.getType().name())).findFirst().
603                     ifPresent(addressConf -> {
604                         setAttribute(
605                                 userTO,
606                                 addressConf.getFormatted(),
607                                 address.getFormatted());
608 
609                         setAttribute(
610                                 userTO,
611                                 addressConf.getStreetAddress(),
612                                 address.getStreetAddress());
613 
614                         setAttribute(
615                                 userTO,
616                                 addressConf.getLocality(),
617                                 address.getLocality());
618 
619                         setAttribute(
620                                 userTO,
621                                 addressConf.getRegion(),
622                                 address.getRegion());
623 
624                         setAttribute(
625                                 userTO,
626                                 addressConf.getPostalCode(),
627                                 address.getPostalCode());
628 
629                         setAttribute(
630                                 userTO,
631                                 addressConf.getCountry(),
632                                 address.getCountry());
633                     }));
634 
635             for (int i = 0; i < user.getX509Certificates().size(); i++) {
636                 Value certificate = user.getX509Certificates().get(i);
637                 if (conf.getUserConf().getX509Certificates().size() > i) {
638                     setAttribute(
639                             userTO,
640                             conf.getUserConf().getX509Certificates().get(i),
641                             certificate.getValue());
642                 }
643             }
644         }
645 
646         if (conf.getEnterpriseUserConf() != null && user.getEnterpriseInfo() != null) {
647             setAttribute(
648                     userTO,
649                     conf.getEnterpriseUserConf().getEmployeeNumber(),
650                     user.getEnterpriseInfo().getEmployeeNumber());
651 
652             setAttribute(
653                     userTO,
654                     conf.getEnterpriseUserConf().getCostCenter(),
655                     user.getEnterpriseInfo().getCostCenter());
656 
657             setAttribute(
658                     userTO,
659                     conf.getEnterpriseUserConf().getOrganization(),
660                     user.getEnterpriseInfo().getOrganization());
661 
662             setAttribute(
663                     userTO,
664                     conf.getEnterpriseUserConf().getDivision(),
665                     user.getEnterpriseInfo().getDivision());
666 
667             setAttribute(
668                     userTO,
669                     conf.getEnterpriseUserConf().getDepartment(),
670                     user.getEnterpriseInfo().getDepartment());
671 
672             setAttribute(
673                     userTO,
674                     Optional.ofNullable(conf.getEnterpriseUserConf().getManager()).
675                             map(SCIMManagerConf::getKey).orElse(null),
676                     Optional.ofNullable(user.getEnterpriseInfo().getManager()).
677                             map(SCIMUserManager::getValue).orElse(null));
678         }
679 
680         userTO.getMemberships().addAll(user.getGroups().stream().
681                 map(group -> new MembershipTO.Builder(group.getValue()).build()).
682                 collect(Collectors.toList()));
683 
684         userTO.getRoles().addAll(user.getRoles().stream().
685                 map(Value::getValue).
686                 collect(Collectors.toList()));
687 
688         return userTO;
689     }
690 
691     public UserCR toUserCR(final SCIMUser user) {
692         UserTO userTO = toUserTO(user, true);
693         UserCR userCR = new UserCR();
694         EntityTOUtils.toAnyCR(userTO, userCR);
695         return userCR;
696     }
697 
698     protected void setAttribute(final Set<AttrPatch> attrs, final String schema, final SCIMPatchOperation op) {
699         Optional.ofNullable(schema).ifPresent(a -> {
700             Attr.Builder attr = new Attr.Builder(a);
701             if (!CollectionUtils.isEmpty(op.getValue())) {
702                 attr.value(op.getValue().get(0).toString());
703             }
704 
705             attrs.add(new AttrPatch.Builder(attr.build()).
706                     operation(op.getOp() == PatchOp.remove ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE).
707                     build());
708         });
709     }
710 
711     protected <E extends Enum<?>> void setAttribute(
712             final Set<AttrPatch> attrs,
713             final List<SCIMComplexConf<E>> confs,
714             final SCIMPatchOperation op) {
715 
716         confs.stream().
717                 filter(conf -> BooleanUtils.toBoolean(JexlUtils.evaluate(
718                 filter2JexlExpression(op.getPath().getFilter()),
719                 new MapContext(Map.of("type", conf.getType().name()))).toString())).findFirst().
720                 ifPresent(conf -> {
721                     if (op.getPath().getSub() == null || "display".equals(op.getPath().getSub())) {
722                         setAttribute(attrs, conf.getDisplay(), op);
723                     }
724                     if (op.getPath().getSub() == null || "value".equals(op.getPath().getSub())) {
725                         setAttribute(attrs, conf.getValue(), op);
726                     }
727                 });
728     }
729 
730     protected <E extends Enum<?>> void setAttribute(
731             final Set<AttrPatch> attrs,
732             final List<SCIMComplexConf<E>> confs,
733             final List<SCIMComplexValue> values,
734             final PatchOp patchOp) {
735 
736         values.stream().
737                 filter(value -> value.getType() != null).forEach(value -> confs.stream().
738                 filter(conf -> value.getType().equals(conf.getType().name())).findFirst().
739                 ifPresent(conf -> attrs.add(new AttrPatch.Builder(
740                 new Attr.Builder(conf.getValue()).value(value.getValue()).build()).
741                 operation(patchOp == PatchOp.remove ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE).
742                 build())));
743     }
744 
745     protected void setAttribute(
746             final Set<AttrPatch> attrs,
747             final SCIMUserAddressConf conf,
748             final SCIMPatchOperation op) {
749 
750         if (op.getPath().getSub() == null || "formatted".equals(op.getPath().getSub())) {
751             setAttribute(attrs, conf.getFormatted(), op);
752         }
753         if (op.getPath().getSub() == null || "streetAddress".equals(op.getPath().getSub())) {
754             setAttribute(attrs, conf.getStreetAddress(), op);
755         }
756         if (op.getPath().getSub() == null || "locality".equals(op.getPath().getSub())) {
757             setAttribute(attrs, conf.getLocality(), op);
758         }
759         if (op.getPath().getSub() == null || "region".equals(op.getPath().getSub())) {
760             setAttribute(attrs, conf.getRegion(), op);
761         }
762         if (op.getPath().getSub() == null || "postalCode".equals(op.getPath().getSub())) {
763             setAttribute(attrs, conf.getPostalCode(), op);
764         }
765         if (op.getPath().getSub() == null || "country".equals(op.getPath().getSub())) {
766             setAttribute(attrs, conf.getCountry(), op);
767         }
768     }
769 
770     public Pair<UserUR, StatusR> toUserUpdate(final UserTO before, final SCIMPatchOperation op) {
771         StatusR statusR = null;
772 
773         if (op.getPath() == null && op.getOp() != PatchOp.remove
774                 && !CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
775 
776             SCIMUser after = (SCIMUser) op.getValue().get(0);
777 
778             if (after.getActive() != null && before.isSuspended() == after.isActive()) {
779                 statusR = new StatusR.Builder(
780                         before.getKey(),
781                         after.isActive() ? StatusRType.REACTIVATE : StatusRType.SUSPEND).
782                         build();
783             }
784 
785             UserTO updated = toUserTO(after, false);
786             updated.setKey(before.getKey());
787             return Pair.of(AnyOperations.diff(updated, before, true), statusR);
788         }
789 
790         UserUR userUR = new UserUR.Builder(before.getKey()).build();
791 
792         SCIMConf conf = confManager.get();
793         if (conf == null) {
794             return Pair.of(userUR, statusR);
795         }
796 
797         switch (op.getPath().getAttribute()) {
798             case "externalId":
799                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getExternalId(), op);
800                 break;
801 
802             case "userName":
803                 if (op.getOp() != PatchOp.remove && !CollectionUtils.isEmpty(op.getValue())) {
804                     userUR.setUsername(new StringReplacePatchItem.Builder().
805                             value(op.getValue().get(0).toString()).build());
806                 }
807                 break;
808 
809             case "password":
810                 if (op.getOp() != PatchOp.remove && !CollectionUtils.isEmpty(op.getValue())) {
811                     userUR.setPassword(new PasswordPatch.Builder().
812                             value(op.getValue().get(0).toString()).build());
813                 }
814                 break;
815 
816             case "active":
817                 if (!CollectionUtils.isEmpty(op.getValue())) {
818 
819                     // Workaround for Microsoft Entra being not SCIM compliant on PATCH requests
820                     if (op.getValue().get(0) instanceof String) {
821                         String a = (String) op.getValue().get(0);
822                         op.setValue(List.of(BooleanUtils.toBoolean(a)));
823                     }
824 
825                     statusR = new StatusR.Builder(
826                             before.getKey(),
827                             (boolean) op.getValue().get(0) ? StatusRType.REACTIVATE : StatusRType.SUSPEND).
828                             build();
829                 }
830                 break;
831 
832             case "name":
833                 if (conf.getUserConf().getName() != null) {
834                     if (op.getPath().getSub() == null || "familyName".equals(op.getPath().getSub())) {
835                         setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getName().getFamilyName(), op);
836                     }
837                     if (op.getPath().getSub() == null || "formatted".equals(op.getPath().getSub())) {
838                         setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getName().getFormatted(), op);
839                     }
840                     if (op.getPath().getSub() == null || "givenName".equals(op.getPath().getSub())) {
841                         setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getName().getGivenName(), op);
842                     }
843                     if (op.getPath().getSub() == null || "honorificPrefix".equals(op.getPath().getSub())) {
844                         setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getName().getHonorificPrefix(), op);
845                     }
846                     if (op.getPath().getSub() == null || "honorificSuffix".equals(op.getPath().getSub())) {
847                         setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getName().getHonorificSuffix(), op);
848                     }
849                     if (op.getPath().getSub() == null || "middleName".equals(op.getPath().getSub())) {
850                         setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getName().getMiddleName(), op);
851                     }
852                 }
853                 break;
854 
855             case "displayName":
856                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getDisplayName(), op);
857                 break;
858 
859             case "nickName":
860                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getNickName(), op);
861                 break;
862 
863             case "profileUrl":
864                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getProfileUrl(), op);
865                 break;
866 
867             case "title":
868                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getTitle(), op);
869                 break;
870 
871             case "userType":
872                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getUserType(), op);
873                 break;
874 
875             case "preferredLanguage":
876                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getPreferredLanguage(), op);
877                 break;
878 
879             case "locale":
880                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getLocale(), op);
881                 break;
882 
883             case "timezone":
884                 setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getTimezone(), op);
885                 break;
886 
887             case "emails":
888                 if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
889                     setAttribute(
890                             userUR.getPlainAttrs(),
891                             conf.getUserConf().getEmails(),
892                             ((SCIMUser) op.getValue().get(0)).getEmails(),
893                             op.getOp());
894                 } else if (op.getPath().getFilter() != null) {
895                     setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getEmails(), op);
896                 }
897                 break;
898 
899             case "phoneNumbers":
900                 if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
901                     setAttribute(
902                             userUR.getPlainAttrs(),
903                             conf.getUserConf().getPhoneNumbers(),
904                             ((SCIMUser) op.getValue().get(0)).getPhoneNumbers(),
905                             op.getOp());
906                 } else if (op.getPath().getFilter() != null) {
907                     setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getPhoneNumbers(), op);
908                 }
909                 break;
910 
911             case "ims":
912                 if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
913                     setAttribute(
914                             userUR.getPlainAttrs(),
915                             conf.getUserConf().getIms(),
916                             ((SCIMUser) op.getValue().get(0)).getIms(),
917                             op.getOp());
918                 } else if (op.getPath().getFilter() != null) {
919                     setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getIms(), op);
920                 }
921                 break;
922 
923             case "photos":
924                 if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
925                     setAttribute(
926                             userUR.getPlainAttrs(),
927                             conf.getUserConf().getPhotos(),
928                             ((SCIMUser) op.getValue().get(0)).getPhotos(),
929                             op.getOp());
930                 } else if (op.getPath().getFilter() != null) {
931                     setAttribute(userUR.getPlainAttrs(), conf.getUserConf().getPhotos(), op);
932                 }
933                 break;
934 
935             case "addresses":
936                 if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue().get(0) instanceof SCIMUser) {
937                     SCIMUser after = (SCIMUser) op.getValue().get(0);
938                     after.getAddresses().stream().filter(address -> address.getType() != null).
939                             forEach(address -> conf.getUserConf().getAddresses().stream().
940                             filter(object -> address.getType().equals(object.getType().name())).findFirst().
941                             ifPresent(addressConf -> setAttribute(userUR.getPlainAttrs(), addressConf, op)));
942                 } else if (op.getPath().getFilter() != null) {
943                     conf.getUserConf().getAddresses().stream().
944                             filter(addressConf -> BooleanUtils.toBoolean(JexlUtils.evaluate(
945                             filter2JexlExpression(op.getPath().getFilter()),
946                             new MapContext(Map.of("type", addressConf.getType().name()))).toString())).findFirst().
947                             ifPresent(addressConf -> setAttribute(userUR.getPlainAttrs(), addressConf, op));
948                 }
949                 break;
950 
951             case "employeeNumber":
952                 setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
953                         map(SCIMEnterpriseUserConf::getEmployeeNumber).orElse(null), op);
954                 break;
955 
956             case "costCenter":
957                 setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
958                         map(SCIMEnterpriseUserConf::getCostCenter).orElse(null), op);
959                 break;
960 
961             case "organization":
962                 setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
963                         map(SCIMEnterpriseUserConf::getOrganization).orElse(null), op);
964                 break;
965 
966             case "division":
967                 setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
968                         map(SCIMEnterpriseUserConf::getDivision).orElse(null), op);
969                 break;
970 
971             case "department":
972                 setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
973                         map(SCIMEnterpriseUserConf::getDepartment).orElse(null), op);
974                 break;
975 
976             case "manager":
977                 setAttribute(userUR.getPlainAttrs(), Optional.ofNullable(conf.getEnterpriseUserConf()).
978                         map(SCIMEnterpriseUserConf::getManager).map(SCIMManagerConf::getKey).orElse(null), op);
979                 break;
980 
981             default:
982         }
983 
984         return Pair.of(userUR, statusR);
985     }
986 
987     public SCIMGroup toSCIMGroup(
988             final GroupTO groupTO,
989             final String location,
990             final List<String> attributes,
991             final List<String> excludedAttributes) {
992 
993         SCIMGroup group = new SCIMGroup(
994                 groupTO.getKey(),
995                 new Meta(
996                         Resource.Group,
997                         groupTO.getCreationDate(),
998                         Optional.ofNullable(groupTO.getLastChangeDate()).orElse(groupTO.getCreationDate()),
999                         groupTO.getETagValue(),
1000                         location),
1001                 output(attributes, excludedAttributes, "displayName", groupTO.getName()));
1002 
1003         SCIMConf conf = confManager.get();
1004 
1005         Map<String, Attr> attrs = new HashMap<>();
1006         attrs.putAll(EntityTOUtils.buildAttrMap(groupTO.getPlainAttrs()));
1007         attrs.putAll(EntityTOUtils.buildAttrMap(groupTO.getDerAttrs()));
1008         attrs.putAll(EntityTOUtils.buildAttrMap(groupTO.getVirAttrs()));
1009 
1010         if (output(attributes, excludedAttributes, "externalId")
1011                 && conf.getGroupConf() != null
1012                 && conf.getGroupConf().getExternalId() != null
1013                 && attrs.containsKey(conf.getGroupConf().getExternalId())) {
1014 
1015             group.setExternalId(attrs.get(conf.getGroupConf().getExternalId()).getValues().get(0));
1016         }
1017 
1018         MembershipCond membCond = new MembershipCond();
1019         membCond.setGroup(groupTO.getKey());
1020         SearchCond searchCond = SearchCond.getLeaf(membCond);
1021 
1022         if (output(attributes, excludedAttributes, "members")) {
1023             int count = userLogic.search(searchCond,
1024                     1, 1, List.of(), SyncopeConstants.ROOT_REALM, true, false).getLeft();
1025 
1026             for (int page = 1; page <= (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
1027                 List<UserTO> users = userLogic.search(
1028                         searchCond,
1029                         page,
1030                         AnyDAO.DEFAULT_PAGE_SIZE,
1031                         List.of(),
1032                         SyncopeConstants.ROOT_REALM,
1033                         true,
1034                         false).
1035                         getRight();
1036                 users.forEach(userTO -> group.getMembers().add(new Member(
1037                         userTO.getKey(),
1038                         StringUtils.substringBefore(location, "/Groups") + "/Users/" + userTO.getKey(),
1039                         userTO.getUsername())));
1040             }
1041         }
1042 
1043         return group;
1044     }
1045 
1046     public GroupTO toGroupTO(final SCIMGroup group, final boolean checkSchemas) {
1047         if (checkSchemas && !GROUP_SCHEMAS.equals(group.getSchemas())) {
1048             throw new BadRequestException(ErrorType.invalidValue);
1049         }
1050 
1051         GroupTO groupTO = new GroupTO();
1052         groupTO.setRealm(SyncopeConstants.ROOT_REALM);
1053         groupTO.setKey(group.getId());
1054         groupTO.setName(group.getDisplayName());
1055 
1056         SCIMConf conf = confManager.get();
1057         if (conf.getGroupConf() != null
1058                 && conf.getGroupConf().getExternalId() != null && group.getExternalId() != null) {
1059 
1060             groupTO.getPlainAttrs().add(
1061                     new Attr.Builder(conf.getGroupConf().getExternalId()).
1062                             value(group.getExternalId()).build());
1063         }
1064 
1065         return groupTO;
1066     }
1067 
1068     public GroupCR toGroupCR(final SCIMGroup group) {
1069         GroupTO groupTO = toGroupTO(group, true);
1070         GroupCR groupCR = new GroupCR();
1071         EntityTOUtils.toAnyCR(groupTO, groupCR);
1072         return groupCR;
1073     }
1074 
1075     public GroupUR toGroupUR(final GroupTO before, final SCIMPatchOperation op) {
1076         if (op.getPath() == null) {
1077             throw new UnsupportedOperationException("Empty path not supported for Groups");
1078         }
1079 
1080         GroupUR groupUR = new GroupUR.Builder(before.getKey()).build();
1081 
1082         if ("displayName".equals(op.getPath().getAttribute())) {
1083             StringReplacePatchItem.Builder name = new StringReplacePatchItem.Builder().
1084                     operation(op.getOp() == PatchOp.remove ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE);
1085             if (!CollectionUtils.isEmpty(op.getValue())) {
1086                 name.value(op.getValue().get(0).toString());
1087             }
1088             groupUR.setName(name.build());
1089         } else {
1090             SCIMConf conf = confManager.get();
1091             if (conf.getGroupConf() != null) {
1092                 setAttribute(groupUR.getPlainAttrs(), conf.getGroupConf().getExternalId(), op);
1093             }
1094         }
1095 
1096         return groupUR;
1097     }
1098 }