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