View Javadoc
1   /*
2    * Copyright 2012-2021 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.ldap;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.util.ArrayList;
21  import java.util.Base64;
22  import java.util.Collections;
23  import java.util.HashSet;
24  import java.util.Hashtable;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.function.BiConsumer;
30  import java.util.function.Consumer;
31  import java.util.function.Supplier;
32  import java.util.stream.Collectors;
33  import java.util.stream.Stream;
34  
35  import javax.annotation.PostConstruct;
36  import javax.naming.Context;
37  import javax.naming.NamingException;
38  import javax.naming.directory.Attribute;
39  import javax.naming.directory.Attributes;
40  import javax.naming.directory.BasicAttribute;
41  import javax.naming.directory.BasicAttributes;
42  import javax.naming.directory.DirContext;
43  import javax.naming.directory.InitialDirContext;
44  import javax.naming.directory.ModificationItem;
45  import javax.naming.directory.SearchControls;
46  import javax.naming.directory.SearchResult;
47  
48  import org.apache.logging.log4j.LogManager;
49  import org.apache.logging.log4j.Logger;
50  import org.codelibs.core.lang.StringUtil;
51  import org.codelibs.core.timer.TimeoutManager;
52  import org.codelibs.fess.Constants;
53  import org.codelibs.fess.entity.FessUser;
54  import org.codelibs.fess.es.user.exentity.Group;
55  import org.codelibs.fess.es.user.exentity.Role;
56  import org.codelibs.fess.es.user.exentity.User;
57  import org.codelibs.fess.exception.LdapConfigurationException;
58  import org.codelibs.fess.exception.LdapOperationException;
59  import org.codelibs.fess.helper.SystemHelper;
60  import org.codelibs.fess.mylasta.direction.FessConfig;
61  import org.codelibs.fess.util.ComponentUtil;
62  import org.codelibs.fess.util.OptionalUtil;
63  import org.dbflute.optional.OptionalEntity;
64  import org.dbflute.util.DfTypeUtil;
65  
66  public class LdapManager {
67      private static final Logger logger = LogManager.getLogger(LdapManager.class);
68  
69      protected ThreadLocal<DirContextHolder> contextLocal = new ThreadLocal<>();
70  
71      protected volatile boolean isBind = false;
72  
73      protected FessConfig fessConfig;
74  
75      @PostConstruct
76      public void init() {
77          if (logger.isDebugEnabled()) {
78              logger.debug("Initialize {}", this.getClass().getSimpleName());
79          }
80          fessConfig = ComponentUtil.getFessConfig();
81      }
82  
83      protected Hashtable<String, String> createEnvironment(final String initialContextFactory, final String securityAuthentication,
84              final String providerUrl, final String principal, final String credntials) {
85          final Hashtable<String, String> env = new Hashtable<>();
86          putEnv(env, Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
87          putEnv(env, Context.SECURITY_AUTHENTICATION, securityAuthentication);
88          putEnv(env, Context.PROVIDER_URL, providerUrl);
89          putEnv(env, Context.SECURITY_PRINCIPAL, principal);
90          putEnv(env, Context.SECURITY_CREDENTIALS, credntials);
91          if (providerUrl != null && providerUrl.startsWith("ldaps://")) {
92              putEnv(env, Context.SECURITY_PROTOCOL, "ssl");
93          }
94          return env;
95      }
96  
97      protected void putEnv(final Hashtable<String, String> env, final String key, final String value) {
98          if (value == null) {
99              throw new LdapConfigurationException(key + " is null.");
100         }
101         env.put(key, value);
102     }
103 
104     protected Hashtable<String, String> createAdminEnv() {
105         return createEnvironment(//
106                 fessConfig.getLdapInitialContextFactory(), //
107                 fessConfig.getLdapSecurityAuthentication(), fessConfig.getLdapProviderUrl(), //
108                 fessConfig.getLdapAdminSecurityPrincipal(), //
109                 fessConfig.getLdapAdminSecurityCredentials());
110     }
111 
112     protected Hashtable<String, String> createSearchEnv(final String username, final String password) {
113         return createEnvironment(//
114                 fessConfig.getLdapInitialContextFactory(), //
115                 fessConfig.getLdapSecurityAuthentication(), //
116                 fessConfig.getLdapProviderUrl(), //
117                 fessConfig.getLdapSecurityPrincipal(username), password);
118     }
119 
120     protected Hashtable<String, String> createSearchEnv() {
121         return createEnvironment(//
122                 fessConfig.getLdapInitialContextFactory(), //
123                 fessConfig.getLdapSecurityAuthentication(), fessConfig.getLdapProviderUrl(), //
124                 fessConfig.getLdapAdminSecurityPrincipal(), //
125                 fessConfig.getLdapAdminSecurityCredentials());
126     }
127 
128     public void updateConfig() {
129         isBind = false;
130     }
131 
132     protected boolean validate() {
133         if (!isBind) {
134             if (fessConfig.getLdapAdminSecurityPrincipal() == null || fessConfig.getLdapAdminSecurityCredentials() == null) {
135                 // no credentials
136                 return !fessConfig.isLdapAuthValidation();
137             }
138             final Hashtable<String, String> env = createAdminEnv();
139             try (DirContextHolder holder = getDirContext(() -> env)) {
140                 final DirContext context = holder.get();
141                 if (logger.isDebugEnabled()) {
142                     logger.debug("Logged in as Bind DN.", context);
143                 }
144                 isBind = true;
145             } catch (final Exception e) {
146                 logger.warn("LDAP configuration is wrong.", e);
147             }
148         }
149         return isBind;
150     }
151 
152     public OptionalEntity<FessUser> login(final String username, final String password) {
153         if (StringUtil.isBlank(fessConfig.getLdapProviderUrl())) {
154             return OptionalEntity.empty();
155         }
156 
157         if (!validate()) {
158             return OptionalEntity.empty();
159         }
160 
161         final Hashtable<String, String> env = createSearchEnv(username, password);
162         try (DirContextHolder holder = getDirContext(() -> env)) {
163             final DirContext context = holder.get();
164             if (logger.isDebugEnabled()) {
165                 logger.debug("Logged in.", context);
166             }
167             return OptionalEntity.of(createLdapUser(username, env));
168         } catch (final Exception e) {
169             logger.debug("Login failed.", e);
170         }
171         return OptionalEntity.empty();
172     }
173 
174     public OptionalEntity<FessUser> login(final String username) {
175         final Hashtable<String, String> env = createSearchEnv();
176         try (DirContextHolder holder = getDirContext(() -> env)) {
177             final DirContext context = holder.get();
178             if (logger.isDebugEnabled()) {
179                 logger.debug("Logged in.", context);
180             }
181             return OptionalEntity.of(createLdapUser(username, env));
182         } catch (final Exception e) {
183             logger.debug("Login failed.", e);
184         }
185         return OptionalEntity.empty();
186     }
187 
188     protected LdapUser createLdapUser(final String username, final Hashtable<String, String> env) {
189         return new LdapUser(env, username);
190     }
191 
192     public String[] getRoles(final LdapUser ldapUser, final String bindDn, final String accountFilter, final String groupFilter,
193             final Consumer<String[]> lazyLoading) {
194         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
195         final Set<String> roleSet = new HashSet<>();
196 
197         if (fessConfig.isLdapRoleSearchUserEnabled()) {
198             roleSet.add(normalizePermissionName(systemHelper.getSearchRoleByUser(ldapUser.getName())));
199         }
200 
201         // LDAP: cn=%s
202         // AD: (&(objectClass=user)(sAMAccountName=%s))
203         final String filter = String.format(accountFilter, ldapUser.getName());
204         if (logger.isDebugEnabled()) {
205             logger.debug("Account Filter: {}", filter);
206         }
207         final Set<String> subRoleSet = new HashSet<>();
208         search(bindDn, filter, new String[] { fessConfig.getLdapMemberofAttribute() }, () -> ldapUser.getEnvironment(), result -> {
209             processSearchRoles(result, entryDn -> {
210                 updateSearchRoles(roleSet, entryDn);
211 
212                 if (StringUtil.isNotBlank(groupFilter)) {
213                     subRoleSet.add(entryDn);
214                 }
215             });
216         });
217 
218         if (logger.isDebugEnabled()) {
219             logger.debug("role: {}", roleSet);
220         }
221         final String[] roles = roleSet.toArray(new String[roleSet.size()]);
222 
223         if (!subRoleSet.isEmpty()) {
224             TimeoutManager.getInstance().addTimeoutTarget(() -> {
225                 processSubRoles(ldapUser, bindDn, subRoleSet, groupFilter, roleSet);
226                 if (logger.isDebugEnabled()) {
227                     logger.debug("role(lazy loading): {}", roleSet);
228                 }
229                 lazyLoading.accept(roleSet.toArray(new String[roleSet.size()]));
230             }, 0, false);
231         }
232 
233         return roles;
234     }
235 
236     protected void processSubRoles(final LdapUser ldapUser, final String bindDn, final Set<String> subRoleSet, final String groupFilter,
237             final Set<String> roleSet) {
238         // (member:1.2.840.113556.1.4.1941:=%s)
239         if (subRoleSet.isEmpty()) {
240             return;
241         }
242         String filter = subRoleSet.stream().map(s -> String.format(groupFilter, s)).collect(Collectors.joining());
243         if (subRoleSet.size() > 1) {
244             filter = "(|" + filter + ")";
245         }
246 
247         if (logger.isDebugEnabled()) {
248             logger.debug("Group Filter: {}", filter);
249         }
250         search(bindDn, filter, null, () -> ldapUser.getEnvironment(), result -> {
251             for (final SearchResult srcrslt : result) {
252                 final String groupDn = srcrslt.getNameInNamespace();
253                 if (logger.isDebugEnabled()) {
254                     logger.debug("groupDn: {}", groupDn);
255                 }
256                 updateSearchRoles(roleSet, groupDn);
257             }
258         });
259     }
260 
261     protected void updateSearchRoles(final Set<String> roleSet, final String entryDn) {
262         final String name = getSearchRoleName(entryDn);
263         if (StringUtil.isBlank(name)) {
264             return;
265         }
266 
267         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
268         final boolean isRole = entryDn.toLowerCase(Locale.ROOT).indexOf("ou=role") != -1;
269         if (isRole) {
270             if (fessConfig.isLdapRoleSearchRoleEnabled()) {
271                 roleSet.add(systemHelper.getSearchRoleByRole(normalizePermissionName(name)));
272             }
273         } else if (fessConfig.isLdapRoleSearchGroupEnabled()) {
274             roleSet.add(systemHelper.getSearchRoleByGroup(normalizePermissionName(name)));
275         }
276     }
277 
278     public String normalizePermissionName(final String name) {
279         if (fessConfig.isLdapLowercasePermissionName()) {
280             return name.toLowerCase(Locale.ROOT);
281         }
282         return name;
283     }
284 
285     protected void processSearchRoles(final List<SearchResult> result, final BiConsumer<String, String> consumer) throws NamingException {
286         processSearchRoles(result, entryDn -> {
287             final String name = getSearchRoleName(entryDn);
288             if (name != null) {
289                 consumer.accept(entryDn, name);
290             }
291         });
292     }
293 
294     protected void processSearchRoles(final List<SearchResult> result, final Consumer<String> consumer) throws NamingException {
295         for (final SearchResult srcrslt : result) {
296             final Attributes attrs = srcrslt.getAttributes();
297 
298             //get group attr
299             final Attribute attr = attrs.get(fessConfig.getLdapMemberofAttribute());
300             if (attr == null) {
301                 continue;
302             }
303 
304             for (int i = 0; i < attr.size(); i++) {
305                 final Object attrValue = attr.get(i);
306                 if (attrValue != null) {
307                     final String entryDn = attrValue.toString();
308 
309                     if (logger.isDebugEnabled()) {
310                         logger.debug("entryDn: {}", entryDn);
311                     }
312                     consumer.accept(entryDn);
313                 }
314             }
315         }
316     }
317 
318     protected String getSearchRoleName(final String entryDn) {
319         if (entryDn == null) {
320             return null;
321         }
322         int start = entryDn.toLowerCase(Locale.ROOT).indexOf("cn=");
323         if (start == -1) {
324             return null;
325         }
326         start += 3;
327 
328         final int end = entryDn.indexOf(',', start);
329         final String value = end == -1 ? entryDn.substring(start) : entryDn.substring(start, end);
330         if (fessConfig.isLdapGroupNameWithUnderscores()) {
331             return replaceWithUnderscores(value);
332         }
333         return value;
334     }
335 
336     protected String replaceWithUnderscores(final String value) {
337         return value.replaceAll("[/\\\\\\[\\]:;|=,+\\*?<>]", "_");
338     }
339 
340     protected void setAttributeValue(final List<SearchResult> result, final String name, final Consumer<Object> consumer) {
341         final List<Object> attrList = getAttributeValueList(result, name);
342         if (!attrList.isEmpty()) {
343             consumer.accept(attrList.get(0));
344         }
345     }
346 
347     protected List<Object> getAttributeValueList(final List<SearchResult> result, final String name) {
348         try {
349             for (final SearchResult srcrslt : result) {
350                 final Attributes attrs = srcrslt.getAttributes();
351 
352                 final Attribute attr = attrs.get(name);
353                 if (attr == null) {
354                     continue;
355                 }
356 
357                 final List<Object> attrList = new ArrayList<>();
358                 for (int i = 0; i < attr.size(); i++) {
359                     final Object attrValue = attr.get(i);
360                     if (attrValue != null) {
361                         attrList.add(attrValue);
362                     }
363                 }
364                 return attrList;
365             }
366             return Collections.emptyList();
367         } catch (final NamingException e) {
368             throw new LdapOperationException("Failed to parse attribute values for " + name, e);
369         }
370     }
371 
372     public void apply(final User user) {
373         if (!fessConfig.isLdapAdminEnabled(user.getName())) {
374             return;
375         }
376 
377         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
378         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(user.getName()), null, adminEnv, result -> {
379             if (!result.isEmpty()) {
380                 setAttributeValue(result, fessConfig.getLdapAttrSurname(), o -> user.setSurname(o.toString()));
381                 setAttributeValue(result, fessConfig.getLdapAttrGivenName(), o -> user.setGivenName(o.toString()));
382                 setAttributeValue(result, fessConfig.getLdapAttrMail(), o -> user.setMail(o.toString()));
383                 setAttributeValue(result, fessConfig.getLdapAttrEmployeeNumber(), o -> user.setEmployeeNumber(o.toString()));
384                 setAttributeValue(result, fessConfig.getLdapAttrTelephoneNumber(), o -> user.setTelephoneNumber(o.toString()));
385                 setAttributeValue(result, fessConfig.getLdapAttrHomePhone(), o -> user.setHomePhone(o.toString()));
386                 setAttributeValue(result, fessConfig.getLdapAttrHomePostalAddress(), o -> user.setHomePostalAddress(o.toString()));
387                 setAttributeValue(result, fessConfig.getLdapAttrLabeleduri(), o -> user.setLabeledURI(o.toString()));
388                 setAttributeValue(result, fessConfig.getLdapAttrRoomNumber(), o -> user.setRoomNumber(o.toString()));
389                 setAttributeValue(result, fessConfig.getLdapAttrDescription(), o -> user.setDescription(o.toString()));
390                 setAttributeValue(result, fessConfig.getLdapAttrTitle(), o -> user.setTitle(o.toString()));
391                 setAttributeValue(result, fessConfig.getLdapAttrPager(), o -> user.setPager(o.toString()));
392                 setAttributeValue(result, fessConfig.getLdapAttrStreet(), o -> user.setStreet(o.toString()));
393                 setAttributeValue(result, fessConfig.getLdapAttrPostalCode(), o -> user.setPostalCode(o.toString()));
394                 setAttributeValue(result, fessConfig.getLdapAttrPhysicalDeliveryOfficeName(),
395                         o -> user.setPhysicalDeliveryOfficeName(o.toString()));
396                 setAttributeValue(result, fessConfig.getLdapAttrDestinationIndicator(), o -> user.setDestinationIndicator(o.toString()));
397                 setAttributeValue(result, fessConfig.getLdapAttrInternationalisdnNumber(),
398                         o -> user.setInternationaliSDNNumber(o.toString()));
399                 setAttributeValue(result, fessConfig.getLdapAttrState(), o -> user.setState(o.toString()));
400                 setAttributeValue(result, fessConfig.getLdapAttrEmployeeType(), o -> user.setEmployeeType(o.toString()));
401                 setAttributeValue(result, fessConfig.getLdapAttrFacsimileTelephoneNumber(),
402                         o -> user.setFacsimileTelephoneNumber(o.toString()));
403                 setAttributeValue(result, fessConfig.getLdapAttrPostOfficeBox(), o -> user.setPostOfficeBox(o.toString()));
404                 setAttributeValue(result, fessConfig.getLdapAttrInitials(), o -> user.setInitials(o.toString()));
405                 setAttributeValue(result, fessConfig.getLdapAttrCarLicense(), o -> user.setCarLicense(o.toString()));
406                 setAttributeValue(result, fessConfig.getLdapAttrMobile(), o -> user.setMobile(o.toString()));
407                 setAttributeValue(result, fessConfig.getLdapAttrPostalAddress(), o -> user.setPostalAddress(o.toString()));
408                 setAttributeValue(result, fessConfig.getLdapAttrCity(), o -> user.setCity(o.toString()));
409                 setAttributeValue(result, fessConfig.getLdapAttrTeletexTerminalIdentifier(),
410                         o -> user.setTeletexTerminalIdentifier(o.toString()));
411                 setAttributeValue(result, fessConfig.getLdapAttrX121Address(), o -> user.setX121Address(o.toString()));
412                 setAttributeValue(result, fessConfig.getLdapAttrBusinessCategory(), o -> user.setBusinessCategory(o.toString()));
413                 setAttributeValue(result, fessConfig.getLdapAttrRegisteredAddress(), o -> user.setRegisteredAddress(o.toString()));
414                 setAttributeValue(result, fessConfig.getLdapAttrDisplayName(), o -> user.setDisplayName(o.toString()));
415                 setAttributeValue(result, fessConfig.getLdapAttrPreferredLanguage(), o -> user.setPreferredLanguage(o.toString()));
416                 setAttributeValue(result, fessConfig.getLdapAttrDepartmentNumber(), o -> user.setDepartmentNumber(o.toString()));
417                 setAttributeValue(result, fessConfig.getLdapAttrUidNumber(), o -> user.setUidNumber(DfTypeUtil.toLong(o)));
418                 setAttributeValue(result, fessConfig.getLdapAttrGidNumber(), o -> user.setGidNumber(DfTypeUtil.toLong(o)));
419                 setAttributeValue(result, fessConfig.getLdapAttrHomeDirectory(), o -> user.setHomeDirectory(o.toString()));
420             }
421         });
422 
423         // groups and roles
424         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(user.getName()),
425                 new String[] { fessConfig.getLdapMemberofAttribute() }, adminEnv, result -> {
426                     if (!result.isEmpty()) {
427                         final List<String> groupList = new ArrayList<>();
428                         final List<String> roleList = new ArrayList<>();
429                         final String lowerGroupDn = fessConfig.getLdapAdminGroupBaseDn().toLowerCase(Locale.ROOT);
430                         final String lowerRoleDn = fessConfig.getLdapAdminRoleBaseDn().toLowerCase(Locale.ROOT);
431                         processSearchRoles(result, (entryDn, name) -> {
432                             final String lowerEntryDn = entryDn.toLowerCase(Locale.ROOT);
433                             if (lowerEntryDn.indexOf(lowerGroupDn) != -1) {
434                                 groupList.add(Base64.getUrlEncoder().encodeToString(name.getBytes(Constants.CHARSET_UTF_8)));
435                             } else if (lowerEntryDn.indexOf(lowerRoleDn) != -1) {
436                                 roleList.add(Base64.getUrlEncoder().encodeToString(name.getBytes(Constants.CHARSET_UTF_8)));
437                             }
438                         });
439                         user.setGroups(groupList.toArray(new String[groupList.size()]));
440                         user.setRoles(roleList.toArray(new String[roleList.size()]));
441                     }
442                 });
443 
444     }
445 
446     public void insert(final User user) {
447         if (!fessConfig.isLdapAdminEnabled(user.getName())) {
448             return;
449         }
450 
451         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
452         final String userDN = fessConfig.getLdapAdminUserSecurityPrincipal(user.getName());
453         // attributes
454         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(user.getName()), null, adminEnv, result -> {
455             if (!result.isEmpty()) {
456                 modifyUserAttributes(user, adminEnv, userDN, result);
457             } else {
458                 final BasicAttributes entry = new BasicAttributes();
459                 addUserAttributes(entry, user);
460                 final Attribute oc = fessConfig.getLdapAdminUserObjectClassAttribute();
461                 entry.put(oc);
462                 insert(userDN, entry, adminEnv);
463             }
464         });
465 
466         // groups and roles
467         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(user.getName()),
468                 new String[] { fessConfig.getLdapMemberofAttribute() }, adminEnv, result -> {
469                     if (!result.isEmpty()) {
470                         final List<String> oldGroupList = new ArrayList<>();
471                         final List<String> oldRoleList = new ArrayList<>();
472                         final String lowerGroupDn = fessConfig.getLdapAdminGroupBaseDn().toLowerCase(Locale.ROOT);
473                         final String lowerRoleDn = fessConfig.getLdapAdminRoleBaseDn().toLowerCase(Locale.ROOT);
474                         processSearchRoles(result, (entryDn, name) -> {
475                             final String lowerEntryDn = entryDn.toLowerCase(Locale.ROOT);
476                             if (lowerEntryDn.indexOf(lowerGroupDn) != -1) {
477                                 oldGroupList.add(name);
478                             } else if (lowerEntryDn.indexOf(lowerRoleDn) != -1) {
479                                 oldRoleList.add(name);
480                             }
481                         });
482                         final List<String> newGroupList = stream(user.getGroupNames()).get(stream -> stream.collect(Collectors.toList()));
483                         stream(user.getGroupNames()).of(stream -> stream.forEach(name -> {
484                             if (oldGroupList.contains(name)) {
485                                 oldGroupList.remove(name);
486                                 newGroupList.remove(name);
487                             }
488                         }));
489                         oldGroupList.stream().forEach(name -> {
490                             search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(name), null, adminEnv,
491                                     subResult -> {
492                                         if (!subResult.isEmpty()) {
493                                             final List<ModificationItem> modifyList = new ArrayList<>();
494                                             modifyDeleteEntry(modifyList, "member", userDN);
495                                             modify(fessConfig.getLdapAdminGroupSecurityPrincipal(name), modifyList, adminEnv);
496                                         }
497                                     });
498                         });
499                         newGroupList.stream().forEach(name -> {
500                             search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(name), null, adminEnv,
501                                     subResult -> {
502                                         if (subResult.isEmpty()) {
503                                             final Group group = new Group();
504                                             group.setName(name);
505                                             insert(group);
506                                         }
507                                         final List<ModificationItem> modifyList = new ArrayList<>();
508                                         modifyAddEntry(modifyList, "member", userDN);
509                                         modify(fessConfig.getLdapAdminGroupSecurityPrincipal(name), modifyList, adminEnv);
510                                     });
511                         });
512 
513                         final List<String> newRoleList = stream(user.getRoleNames()).get(stream -> stream.collect(Collectors.toList()));
514                         stream(user.getRoleNames()).of(stream -> stream.forEach(name -> {
515                             if (oldRoleList.contains(name)) {
516                                 oldRoleList.remove(name);
517                                 newRoleList.remove(name);
518                             }
519                         }));
520                         oldRoleList.stream().forEach(name -> {
521                             search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(name), null, adminEnv,
522                                     subResult -> {
523                                         if (!subResult.isEmpty()) {
524                                             final List<ModificationItem> modifyList = new ArrayList<>();
525                                             modifyDeleteEntry(modifyList, "member", userDN);
526                                             modify(fessConfig.getLdapAdminRoleSecurityPrincipal(name), modifyList, adminEnv);
527                                         }
528                                     });
529                         });
530                         newRoleList.stream().forEach(name -> {
531                             search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(name), null, adminEnv,
532                                     subResult -> {
533                                         if (subResult.isEmpty()) {
534                                             final Role role = new Role();
535                                             role.setName(name);
536                                             insert(role);
537                                         }
538                                         final List<ModificationItem> modifyList = new ArrayList<>();
539                                         modifyAddEntry(modifyList, "member", userDN);
540                                         modify(fessConfig.getLdapAdminRoleSecurityPrincipal(name), modifyList, adminEnv);
541                                     });
542                         });
543                     } else {
544                         stream(user.getGroupNames()).of(stream -> stream.forEach(name -> {
545                             search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(name), null, adminEnv,
546                                     subResult -> {
547                                         if (subResult.isEmpty()) {
548                                             final Group group = new Group();
549                                             group.setName(name);
550                                             insert(group);
551                                         }
552                                         final List<ModificationItem> modifyList = new ArrayList<>();
553                                         modifyAddEntry(modifyList, "member", userDN);
554                                         modify(fessConfig.getLdapAdminGroupSecurityPrincipal(name), modifyList, adminEnv);
555                                     });
556                         }));
557 
558                         stream(user.getRoleNames()).of(stream -> stream.forEach(name -> {
559                             search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(name), null, adminEnv,
560                                     subResult -> {
561                                         if (subResult.isEmpty()) {
562                                             final Role role = new Role();
563                                             role.setName(name);
564                                             insert(role);
565                                         }
566                                         final List<ModificationItem> modifyList = new ArrayList<>();
567                                         modifyAddEntry(modifyList, "member", userDN);
568                                         modify(fessConfig.getLdapAdminRoleSecurityPrincipal(name), modifyList, adminEnv);
569                                     });
570                         }));
571                     }
572                 });
573 
574     }
575 
576     protected void modifyUserAttributes(final User user, final Supplier<Hashtable<String, String>> adminEnv, final String userDN,
577             final List<SearchResult> result) {
578         final List<ModificationItem> modifyList = new ArrayList<>();
579         if (user.getOriginalPassword() != null) {
580             modifyReplaceEntry(modifyList, "userPassword", user.getOriginalPassword());
581         }
582 
583         final String attrSurname = fessConfig.getLdapAttrSurname();
584         OptionalUtil.ofNullable(user.getSurname()).filter(StringUtil::isNotBlank)
585                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrSurname, s)).orElse(() -> getAttributeValueList(result, attrSurname)
586                         .stream().forEach(v -> modifyDeleteEntry(modifyList, attrSurname, v)));
587         final String attrGivenName = fessConfig.getLdapAttrGivenName();
588         OptionalUtil.ofNullable(user.getGivenName()).filter(StringUtil::isNotBlank)
589                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrGivenName, s)).orElse(() -> getAttributeValueList(result, attrGivenName)
590                         .stream().forEach(v -> modifyDeleteEntry(modifyList, attrGivenName, v)));
591         final String attrMail = fessConfig.getLdapAttrMail();
592         OptionalUtil.ofNullable(user.getMail()).filter(StringUtil::isNotBlank).ifPresent(s -> modifyReplaceEntry(modifyList, attrMail, s))
593                 .orElse(() -> getAttributeValueList(result, attrMail).stream().forEach(v -> modifyDeleteEntry(modifyList, attrMail, v)));
594         final String attrEmployeeNumber = fessConfig.getLdapAttrEmployeeNumber();
595         OptionalUtil.ofNullable(user.getEmployeeNumber()).filter(StringUtil::isNotBlank)
596                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrEmployeeNumber, s))
597                 .orElse(() -> getAttributeValueList(result, attrEmployeeNumber).stream()
598                         .forEach(v -> modifyDeleteEntry(modifyList, attrEmployeeNumber, v)));
599         final String attrTelephoneNumber = fessConfig.getLdapAttrTelephoneNumber();
600         OptionalUtil.ofNullable(user.getTelephoneNumber()).filter(StringUtil::isNotBlank)
601                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrTelephoneNumber, s))
602                 .orElse(() -> getAttributeValueList(result, attrTelephoneNumber).stream()
603                         .forEach(v -> modifyDeleteEntry(modifyList, attrTelephoneNumber, v)));
604         final String attrHomePhone = fessConfig.getLdapAttrHomePhone();
605         OptionalUtil.ofNullable(user.getHomePhone()).filter(StringUtil::isNotBlank)
606                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrHomePhone, s)).orElse(() -> getAttributeValueList(result, attrHomePhone)
607                         .stream().forEach(v -> modifyDeleteEntry(modifyList, attrHomePhone, v)));
608         final String attrHomePostalAddress = fessConfig.getLdapAttrHomePostalAddress();
609         OptionalUtil.ofNullable(user.getHomePostalAddress()).filter(StringUtil::isNotBlank)
610                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrHomePostalAddress, s))
611                 .orElse(() -> getAttributeValueList(result, attrHomePostalAddress).stream()
612                         .forEach(v -> modifyDeleteEntry(modifyList, attrHomePostalAddress, v)));
613         final String attrLabeledURI = fessConfig.getLdapAttrLabeleduri();
614         OptionalUtil.ofNullable(user.getLabeledURI()).filter(StringUtil::isNotBlank)
615                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrLabeledURI, s))
616                 .orElse(() -> getAttributeValueList(result, attrLabeledURI).stream()
617                         .forEach(v -> modifyDeleteEntry(modifyList, attrLabeledURI, v)));
618         final String attrRoomNumber = fessConfig.getLdapAttrRoomNumber();
619         OptionalUtil.ofNullable(user.getRoomNumber()).filter(StringUtil::isNotBlank)
620                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrRoomNumber, s))
621                 .orElse(() -> getAttributeValueList(result, attrRoomNumber).stream()
622                         .forEach(v -> modifyDeleteEntry(modifyList, attrRoomNumber, v)));
623         final String attrDescription = fessConfig.getLdapAttrDescription();
624         OptionalUtil.ofNullable(user.getDescription()).filter(StringUtil::isNotBlank)
625                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrDescription, s))
626                 .orElse(() -> getAttributeValueList(result, attrDescription).stream()
627                         .forEach(v -> modifyDeleteEntry(modifyList, attrDescription, v)));
628         final String attrTitle = fessConfig.getLdapAttrTitle();
629         OptionalUtil.ofNullable(user.getTitle()).filter(StringUtil::isNotBlank).ifPresent(s -> modifyReplaceEntry(modifyList, attrTitle, s))
630                 .orElse(() -> getAttributeValueList(result, attrTitle).stream().forEach(v -> modifyDeleteEntry(modifyList, attrTitle, v)));
631         final String attrPager = fessConfig.getLdapAttrPager();
632         OptionalUtil.ofNullable(user.getPager()).filter(StringUtil::isNotBlank).ifPresent(s -> modifyReplaceEntry(modifyList, attrPager, s))
633                 .orElse(() -> getAttributeValueList(result, attrPager).stream().forEach(v -> modifyDeleteEntry(modifyList, attrPager, v)));
634         final String attrStreet = fessConfig.getLdapAttrStreet();
635         OptionalUtil.ofNullable(user.getStreet()).filter(StringUtil::isNotBlank)
636                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrStreet, s)).orElse(() -> getAttributeValueList(result, attrStreet)
637                         .stream().forEach(v -> modifyDeleteEntry(modifyList, attrStreet, v)));
638         final String attrPostalCode = fessConfig.getLdapAttrPostalCode();
639         OptionalUtil.ofNullable(user.getPostalCode()).filter(StringUtil::isNotBlank)
640                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPostalCode, s))
641                 .orElse(() -> getAttributeValueList(result, attrPostalCode).stream()
642                         .forEach(v -> modifyDeleteEntry(modifyList, attrPostalCode, v)));
643         final String attrPhysicalDeliveryOfficeName = fessConfig.getLdapAttrPhysicalDeliveryOfficeName();
644         OptionalUtil.ofNullable(user.getPhysicalDeliveryOfficeName()).filter(StringUtil::isNotBlank)
645                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPhysicalDeliveryOfficeName, s))
646                 .orElse(() -> getAttributeValueList(result, attrPhysicalDeliveryOfficeName).stream()
647                         .forEach(v -> modifyDeleteEntry(modifyList, attrPhysicalDeliveryOfficeName, v)));
648         final String attrDestinationIndicator = fessConfig.getLdapAttrDestinationIndicator();
649         OptionalUtil.ofNullable(user.getDestinationIndicator()).filter(StringUtil::isNotBlank)
650                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrDestinationIndicator, s))
651                 .orElse(() -> getAttributeValueList(result, attrDestinationIndicator).stream()
652                         .forEach(v -> modifyDeleteEntry(modifyList, attrDestinationIndicator, v)));
653         final String attrInternationaliSDNNumber = fessConfig.getLdapAttrInternationalisdnNumber();
654         OptionalUtil.ofNullable(user.getInternationaliSDNNumber()).filter(StringUtil::isNotBlank)
655                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrInternationaliSDNNumber, s))
656                 .orElse(() -> getAttributeValueList(result, attrInternationaliSDNNumber).stream()
657                         .forEach(v -> modifyDeleteEntry(modifyList, attrInternationaliSDNNumber, v)));
658         final String attrState = fessConfig.getLdapAttrState();
659         OptionalUtil.ofNullable(user.getState()).filter(StringUtil::isNotBlank).ifPresent(s -> modifyReplaceEntry(modifyList, attrState, s))
660                 .orElse(() -> getAttributeValueList(result, attrState).stream().forEach(v -> modifyDeleteEntry(modifyList, attrState, v)));
661         final String attrEmployeeType = fessConfig.getLdapAttrEmployeeType();
662         OptionalUtil.ofNullable(user.getEmployeeType()).filter(StringUtil::isNotBlank)
663                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrEmployeeType, s))
664                 .orElse(() -> getAttributeValueList(result, attrEmployeeType).stream()
665                         .forEach(v -> modifyDeleteEntry(modifyList, attrEmployeeType, v)));
666         final String attrFacsimileTelephoneNumber = fessConfig.getLdapAttrFacsimileTelephoneNumber();
667         OptionalUtil.ofNullable(user.getFacsimileTelephoneNumber()).filter(StringUtil::isNotBlank)
668                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrFacsimileTelephoneNumber, s))
669                 .orElse(() -> getAttributeValueList(result, attrFacsimileTelephoneNumber).stream()
670                         .forEach(v -> modifyDeleteEntry(modifyList, attrFacsimileTelephoneNumber, v)));
671         final String attrPostOfficeBox = fessConfig.getLdapAttrPostOfficeBox();
672         OptionalUtil.ofNullable(user.getPostOfficeBox()).filter(StringUtil::isNotBlank)
673                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPostOfficeBox, s))
674                 .orElse(() -> getAttributeValueList(result, attrPostOfficeBox).stream()
675                         .forEach(v -> modifyDeleteEntry(modifyList, attrPostOfficeBox, v)));
676         final String attrInitials = fessConfig.getLdapAttrInitials();
677         OptionalUtil.ofNullable(user.getInitials()).filter(StringUtil::isNotBlank)
678                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrInitials, s)).orElse(() -> getAttributeValueList(result, attrInitials)
679                         .stream().forEach(v -> modifyDeleteEntry(modifyList, attrInitials, v)));
680         final String attrCarLicense = fessConfig.getLdapAttrCarLicense();
681         OptionalUtil.ofNullable(user.getCarLicense()).filter(StringUtil::isNotBlank)
682                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrCarLicense, s))
683                 .orElse(() -> getAttributeValueList(result, attrCarLicense).stream()
684                         .forEach(v -> modifyDeleteEntry(modifyList, attrCarLicense, v)));
685         final String attrMobile = fessConfig.getLdapAttrMobile();
686         OptionalUtil.ofNullable(user.getMobile()).filter(StringUtil::isNotBlank)
687                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrMobile, s)).orElse(() -> getAttributeValueList(result, attrMobile)
688                         .stream().forEach(v -> modifyDeleteEntry(modifyList, attrMobile, v)));
689         final String attrPostalAddress = fessConfig.getLdapAttrPostalAddress();
690         OptionalUtil.ofNullable(user.getPostalAddress()).filter(StringUtil::isNotBlank)
691                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPostalAddress, s))
692                 .orElse(() -> getAttributeValueList(result, attrPostalAddress).stream()
693                         .forEach(v -> modifyDeleteEntry(modifyList, attrPostalAddress, v)));
694         final String attrCity = fessConfig.getLdapAttrCity();
695         OptionalUtil.ofNullable(user.getCity()).filter(StringUtil::isNotBlank).ifPresent(s -> modifyReplaceEntry(modifyList, attrCity, s))
696                 .orElse(() -> getAttributeValueList(result, attrCity).stream().forEach(v -> modifyDeleteEntry(modifyList, attrCity, v)));
697         final String attrTeletexTerminalIdentifier = fessConfig.getLdapAttrTeletexTerminalIdentifier();
698         OptionalUtil.ofNullable(user.getTeletexTerminalIdentifier()).filter(StringUtil::isNotBlank)
699                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrTeletexTerminalIdentifier, s))
700                 .orElse(() -> getAttributeValueList(result, attrTeletexTerminalIdentifier).stream()
701                         .forEach(v -> modifyDeleteEntry(modifyList, attrTeletexTerminalIdentifier, v)));
702         final String attrX121Address = fessConfig.getLdapAttrX121Address();
703         OptionalUtil.ofNullable(user.getX121Address()).filter(StringUtil::isNotBlank)
704                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrX121Address, s))
705                 .orElse(() -> getAttributeValueList(result, attrX121Address).stream()
706                         .forEach(v -> modifyDeleteEntry(modifyList, attrX121Address, v)));
707         final String attrBusinessCategory = fessConfig.getLdapAttrBusinessCategory();
708         OptionalUtil.ofNullable(user.getBusinessCategory()).filter(StringUtil::isNotBlank)
709                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrBusinessCategory, s))
710                 .orElse(() -> getAttributeValueList(result, attrBusinessCategory).stream()
711                         .forEach(v -> modifyDeleteEntry(modifyList, attrBusinessCategory, v)));
712         final String attrRegisteredAddress = fessConfig.getLdapAttrRegisteredAddress();
713         OptionalUtil.ofNullable(user.getRegisteredAddress()).filter(StringUtil::isNotBlank)
714                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrRegisteredAddress, s))
715                 .orElse(() -> getAttributeValueList(result, attrRegisteredAddress).stream()
716                         .forEach(v -> modifyDeleteEntry(modifyList, attrRegisteredAddress, v)));
717         final String attrDisplayName = fessConfig.getLdapAttrDisplayName();
718         OptionalUtil.ofNullable(user.getDisplayName()).filter(StringUtil::isNotBlank)
719                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrDisplayName, s))
720                 .orElse(() -> getAttributeValueList(result, attrDisplayName).stream()
721                         .forEach(v -> modifyDeleteEntry(modifyList, attrDisplayName, v)));
722         final String attrPreferredLanguage = fessConfig.getLdapAttrPreferredLanguage();
723         OptionalUtil.ofNullable(user.getPreferredLanguage()).filter(StringUtil::isNotBlank)
724                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrPreferredLanguage, s))
725                 .orElse(() -> getAttributeValueList(result, attrPreferredLanguage).stream()
726                         .forEach(v -> modifyDeleteEntry(modifyList, attrPreferredLanguage, v)));
727         final String attrDepartmentNumber = fessConfig.getLdapAttrDepartmentNumber();
728         OptionalUtil.ofNullable(user.getDepartmentNumber()).filter(StringUtil::isNotBlank)
729                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrDepartmentNumber, s))
730                 .orElse(() -> getAttributeValueList(result, attrDepartmentNumber).stream()
731                         .forEach(v -> modifyDeleteEntry(modifyList, attrDepartmentNumber, v)));
732         final String attrUidNumber = fessConfig.getLdapAttrUidNumber();
733         OptionalUtil.ofNullable(user.getUidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
734                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrUidNumber, s.toString()))
735                 .orElse(() -> getAttributeValueList(result, attrUidNumber).stream()
736                         .forEach(v -> modifyDeleteEntry(modifyList, attrUidNumber, v)));
737         final String attrGidNumber = fessConfig.getLdapAttrGidNumber();
738         OptionalUtil.ofNullable(user.getGidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
739                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrGidNumber, s.toString()))
740                 .orElse(() -> getAttributeValueList(result, attrGidNumber).stream()
741                         .forEach(v -> modifyDeleteEntry(modifyList, attrGidNumber, v)));
742         final String attrHomeDirectory = fessConfig.getLdapAttrHomeDirectory();
743         OptionalUtil.ofNullable(user.getHomeDirectory()).filter(StringUtil::isNotBlank)
744                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrHomeDirectory, s))
745                 .orElse(() -> getAttributeValueList(result, attrHomeDirectory).stream()
746                         .forEach(v -> modifyDeleteEntry(modifyList, attrHomeDirectory, v)));
747 
748         modify(userDN, modifyList, adminEnv);
749     }
750 
751     protected void addUserAttributes(final BasicAttributes entry, final User user) {
752         entry.put(new BasicAttribute("cn", user.getName()));
753         entry.put(new BasicAttribute("userPassword", user.getOriginalPassword()));
754 
755         OptionalUtil.ofNullable(user.getSurname()).filter(StringUtil::isNotBlank)
756                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrSurname(), s)));
757         OptionalUtil.ofNullable(user.getGivenName()).filter(StringUtil::isNotBlank)
758                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrGivenName(), s)));
759         OptionalUtil.ofNullable(user.getMail()).filter(StringUtil::isNotBlank)
760                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrMail(), s)));
761         OptionalUtil.ofNullable(user.getEmployeeNumber()).filter(StringUtil::isNotBlank)
762                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrEmployeeNumber(), s)));
763         OptionalUtil.ofNullable(user.getTelephoneNumber()).filter(StringUtil::isNotBlank)
764                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrTelephoneNumber(), s)));
765         OptionalUtil.ofNullable(user.getHomePhone()).filter(StringUtil::isNotBlank)
766                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrHomePhone(), s)));
767         OptionalUtil.ofNullable(user.getHomePostalAddress()).filter(StringUtil::isNotBlank)
768                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrHomePostalAddress(), s)));
769         OptionalUtil.ofNullable(user.getLabeledURI()).filter(StringUtil::isNotBlank)
770                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrLabeleduri(), s)));
771         OptionalUtil.ofNullable(user.getRoomNumber()).filter(StringUtil::isNotBlank)
772                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrRoomNumber(), s)));
773         OptionalUtil.ofNullable(user.getDescription()).filter(StringUtil::isNotBlank)
774                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrDescription(), s)));
775         OptionalUtil.ofNullable(user.getTitle()).filter(StringUtil::isNotBlank)
776                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrTitle(), s)));
777         OptionalUtil.ofNullable(user.getPager()).filter(StringUtil::isNotBlank)
778                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPager(), s)));
779         OptionalUtil.ofNullable(user.getStreet()).filter(StringUtil::isNotBlank)
780                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrStreet(), s)));
781         OptionalUtil.ofNullable(user.getPostalCode()).filter(StringUtil::isNotBlank)
782                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPostalCode(), s)));
783         OptionalUtil.ofNullable(user.getPhysicalDeliveryOfficeName()).filter(StringUtil::isNotBlank)
784                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPhysicalDeliveryOfficeName(), s)));
785         OptionalUtil.ofNullable(user.getDestinationIndicator()).filter(StringUtil::isNotBlank)
786                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrDestinationIndicator(), s)));
787         OptionalUtil.ofNullable(user.getInternationaliSDNNumber()).filter(StringUtil::isNotBlank)
788                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrInternationalisdnNumber(), s)));
789         OptionalUtil.ofNullable(user.getState()).filter(StringUtil::isNotBlank)
790                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrState(), s)));
791         OptionalUtil.ofNullable(user.getEmployeeType()).filter(StringUtil::isNotBlank)
792                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrEmployeeType(), s)));
793         OptionalUtil.ofNullable(user.getFacsimileTelephoneNumber()).filter(StringUtil::isNotBlank)
794                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrFacsimileTelephoneNumber(), s)));
795         OptionalUtil.ofNullable(user.getPostOfficeBox()).filter(StringUtil::isNotBlank)
796                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPostOfficeBox(), s)));
797         OptionalUtil.ofNullable(user.getInitials()).filter(StringUtil::isNotBlank)
798                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrInitials(), s)));
799         OptionalUtil.ofNullable(user.getCarLicense()).filter(StringUtil::isNotBlank)
800                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrCarLicense(), s)));
801         OptionalUtil.ofNullable(user.getMobile()).filter(StringUtil::isNotBlank)
802                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrMobile(), s)));
803         OptionalUtil.ofNullable(user.getPostalAddress()).filter(StringUtil::isNotBlank)
804                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPostalAddress(), s)));
805         OptionalUtil.ofNullable(user.getCity()).filter(StringUtil::isNotBlank)
806                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrCity(), s)));
807         OptionalUtil.ofNullable(user.getTeletexTerminalIdentifier()).filter(StringUtil::isNotBlank)
808                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrTeletexTerminalIdentifier(), s)));
809         OptionalUtil.ofNullable(user.getX121Address()).filter(StringUtil::isNotBlank)
810                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrX121Address(), s)));
811         OptionalUtil.ofNullable(user.getBusinessCategory()).filter(StringUtil::isNotBlank)
812                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrBusinessCategory(), s)));
813         OptionalUtil.ofNullable(user.getRegisteredAddress()).filter(StringUtil::isNotBlank)
814                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrRegisteredAddress(), s)));
815         OptionalUtil.ofNullable(user.getDisplayName()).filter(StringUtil::isNotBlank)
816                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrDisplayName(), s)));
817         OptionalUtil.ofNullable(user.getPreferredLanguage()).filter(StringUtil::isNotBlank)
818                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrPreferredLanguage(), s)));
819         OptionalUtil.ofNullable(user.getDepartmentNumber()).filter(StringUtil::isNotBlank)
820                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrDepartmentNumber(), s)));
821         OptionalUtil.ofNullable(user.getUidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
822                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrUidNumber(), s)));
823         OptionalUtil.ofNullable(user.getGidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
824                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrGidNumber(), s)));
825         OptionalUtil.ofNullable(user.getHomeDirectory()).filter(StringUtil::isNotBlank)
826                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrHomeDirectory(), s)));
827     }
828 
829     public void validateUserAttributes(final Class<?> type, final Map<String, String> attributes, final Consumer<String> consumer) {
830         if (type == Long.class) {
831             // Long type attributes
832             final String attrUidNumber = fessConfig.getLdapAttrUidNumber();
833             final String attrGidNumber = fessConfig.getLdapAttrGidNumber();
834 
835             Stream.of(attrUidNumber, attrGidNumber)
836                     .forEach(attrName -> OptionalUtil.ofNullable(attributes.get(attrName)).filter(StringUtil::isNotBlank).ifPresent(s -> {
837                         try {
838                             DfTypeUtil.toLong(s);
839                         } catch (final NumberFormatException e) {
840                             consumer.accept(attrName);
841                         }
842                     }));
843         } else {
844             // do nothing
845         }
846     }
847 
848     public void delete(final User user) {
849         if (!fessConfig.isLdapAdminEnabled(user.getName())) {
850             return;
851         }
852 
853         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
854         final String userDN = fessConfig.getLdapAdminUserSecurityPrincipal(user.getName());
855 
856         stream(user.getGroupNames()).of(stream -> stream.forEach(name -> {
857             search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(name), null, adminEnv, subResult -> {
858                 if (subResult.isEmpty()) {
859                     final Group group = new Group();
860                     group.setName(name);
861                     insert(group);
862                 }
863                 final List<ModificationItem> modifyList = new ArrayList<>();
864                 modifyDeleteEntry(modifyList, "member", userDN);
865                 modify(fessConfig.getLdapAdminGroupSecurityPrincipal(name), modifyList, adminEnv);
866             });
867         }));
868         stream(user.getRoleNames()).of(stream -> stream.forEach(name -> {
869             search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(name), null, adminEnv, subResult -> {
870                 if (subResult.isEmpty()) {
871                     final Role role = new Role();
872                     role.setName(name);
873                     insert(role);
874                 }
875                 final List<ModificationItem> modifyList = new ArrayList<>();
876                 modifyDeleteEntry(modifyList, "member", userDN);
877                 modify(fessConfig.getLdapAdminRoleSecurityPrincipal(name), modifyList, adminEnv);
878             });
879         }));
880 
881         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(user.getName()), null, adminEnv, result -> {
882             if (!result.isEmpty()) {
883                 delete(userDN, adminEnv);
884             } else {
885                 logger.info("{} does not exist in LDAP server.", user.getName());
886             }
887         });
888 
889     }
890 
891     public void insert(final Role role) {
892         if (!fessConfig.isLdapAdminEnabled()) {
893             return;
894         }
895 
896         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
897         search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(role.getName()), null, adminEnv, result -> {
898             if (!result.isEmpty()) {
899                 logger.info("{} exists in LDAP server.", role.getName());
900             } else {
901                 final String entryDN = fessConfig.getLdapAdminRoleSecurityPrincipal(role.getName());
902                 final BasicAttributes entry = new BasicAttributes();
903                 addRoleAttributes(entry, role);
904                 final Attribute oc = fessConfig.getLdapAdminRoleObjectClassAttribute();
905                 entry.put(oc);
906                 insert(entryDN, entry, adminEnv);
907             }
908         });
909 
910     }
911 
912     protected void addRoleAttributes(final BasicAttributes entry, final Role user) {
913         // nothing
914     }
915 
916     public void delete(final Role role) {
917         if (!fessConfig.isLdapAdminEnabled()) {
918             return;
919         }
920 
921         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
922         search(fessConfig.getLdapAdminRoleBaseDn(), fessConfig.getLdapAdminRoleFilter(role.getName()), null, adminEnv, result -> {
923             if (!result.isEmpty()) {
924                 final String entryDN = fessConfig.getLdapAdminRoleSecurityPrincipal(role.getName());
925                 delete(entryDN, adminEnv);
926             } else {
927                 logger.info("{} does not exist in LDAP server.", role.getName());
928             }
929         });
930 
931     }
932 
933     public void apply(final Group group) {
934         if (!fessConfig.isLdapAdminEnabled()) {
935             return;
936         }
937 
938         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
939         search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(group.getName()), null, adminEnv, result -> {
940             if (!result.isEmpty()) {
941                 setAttributeValue(result, fessConfig.getLdapAttrGidNumber(), o -> group.setGidNumber(DfTypeUtil.toLong(o)));
942             }
943         });
944     }
945 
946     public void insert(final Group group) {
947         if (!fessConfig.isLdapAdminEnabled()) {
948             return;
949         }
950 
951         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
952         final String entryDN = fessConfig.getLdapAdminGroupSecurityPrincipal(group.getName());
953         search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(group.getName()), null, adminEnv, result -> {
954             if (!result.isEmpty()) {
955                 logger.info("{} exists in LDAP server.", group.getName());
956                 modifyGroupAttributes(group, adminEnv, entryDN, result);
957             } else {
958                 final BasicAttributes entry = new BasicAttributes();
959                 addGroupAttributes(entry, group);
960                 final Attribute oc = fessConfig.getLdapAdminGroupObjectClassAttribute();
961                 entry.put(oc);
962                 insert(entryDN, entry, adminEnv);
963             }
964         });
965     }
966 
967     protected void modifyGroupAttributes(final Group group, final Supplier<Hashtable<String, String>> adminEnv, final String entryDN,
968             final List<SearchResult> result) {
969         final List<ModificationItem> modifyList = new ArrayList<>();
970 
971         final String attrGidNumber = fessConfig.getLdapAttrGidNumber();
972         OptionalUtil.ofNullable(group.getGidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
973                 .ifPresent(s -> modifyReplaceEntry(modifyList, attrGidNumber, s.toString()))
974                 .orElse(() -> getAttributeValueList(result, attrGidNumber).stream()
975                         .forEach(v -> modifyDeleteEntry(modifyList, attrGidNumber, v)));
976 
977         modify(entryDN, modifyList, adminEnv);
978     }
979 
980     protected void addGroupAttributes(final BasicAttributes entry, final Group group) {
981         OptionalUtil.ofNullable(group.getGidNumber()).filter(s -> StringUtil.isNotBlank(s.toString()))
982                 .ifPresent(s -> entry.put(new BasicAttribute(fessConfig.getLdapAttrGidNumber(), s)));
983     }
984 
985     public void validateGroupAttributes(final Class<?> type, final Map<String, String> attributes, final Consumer<String> consumer) {
986         if (type == Long.class) {
987             // Long type attributes
988             final String attrGidNumber = fessConfig.getLdapAttrGidNumber();
989 
990             Stream.of(attrGidNumber)
991                     .forEach(attrName -> OptionalUtil.ofNullable(attributes.get(attrName)).filter(StringUtil::isNotBlank).ifPresent(s -> {
992                         try {
993                             DfTypeUtil.toLong(s);
994                         } catch (final NumberFormatException e) {
995                             consumer.accept(attrName);
996                         }
997                     }));
998         } else {
999             // do nothing
1000         }
1001     }
1002 
1003     public void delete(final Group group) {
1004         if (!fessConfig.isLdapAdminEnabled()) {
1005             return;
1006         }
1007 
1008         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
1009         search(fessConfig.getLdapAdminGroupBaseDn(), fessConfig.getLdapAdminGroupFilter(group.getName()), null, adminEnv, result -> {
1010             if (!result.isEmpty()) {
1011                 final String entryDN = fessConfig.getLdapAdminGroupSecurityPrincipal(group.getName());
1012                 delete(entryDN, adminEnv);
1013             } else {
1014                 logger.info("{} does not exist in LDAP server.", group.getName());
1015             }
1016         });
1017     }
1018 
1019     public boolean changePassword(final String username, final String password) {
1020         if (!fessConfig.isLdapAdminEnabled(username)) {
1021             return false;
1022         }
1023 
1024         final Supplier<Hashtable<String, String>> adminEnv = this::createAdminEnv;
1025         final String userDN = fessConfig.getLdapAdminUserSecurityPrincipal(username);
1026         search(fessConfig.getLdapAdminUserBaseDn(), fessConfig.getLdapAdminUserFilter(username), null, adminEnv, result -> {
1027             if (result.isEmpty()) {
1028                 throw new LdapOperationException("User is not found: " + username);
1029             }
1030             final List<ModificationItem> modifyList = new ArrayList<>();
1031             modifyReplaceEntry(modifyList, "userPassword", password);
1032             modify(userDN, modifyList, adminEnv);
1033         });
1034         return true;
1035     }
1036 
1037     protected void insert(final String entryDN, final Attributes entry, final Supplier<Hashtable<String, String>> envSupplier) {
1038         try (DirContextHolder holder = getDirContext(envSupplier)) {
1039             logger.debug("Inserting {}", entryDN);
1040             holder.get().createSubcontext(entryDN, entry);
1041         } catch (final NamingException e) {
1042             throw new LdapOperationException("Failed to add " + entryDN, e);
1043         }
1044     }
1045 
1046     protected void delete(final String entryDN, final Supplier<Hashtable<String, String>> envSupplier) {
1047         try (DirContextHolder holder = getDirContext(envSupplier)) {
1048             logger.debug("Deleting {}", entryDN);
1049             holder.get().destroySubcontext(entryDN);
1050         } catch (final NamingException e) {
1051             throw new LdapOperationException("Failed to delete " + entryDN, e);
1052         }
1053     }
1054 
1055     protected void search(final String baseDn, final String filter, final String[] returningAttrs,
1056             final Supplier<Hashtable<String, String>> envSupplier, final SearchConsumer consumer) {
1057         try (DirContextHolder holder = getDirContext(envSupplier)) {
1058             final SearchControls controls = new SearchControls();
1059             controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
1060             if (returningAttrs != null) {
1061                 controls.setReturningAttributes(returningAttrs);
1062             }
1063 
1064             final long startTime = System.currentTimeMillis();
1065             final List<SearchResult> list = Collections.list(holder.get().search(baseDn, filter, controls));
1066             if (logger.isDebugEnabled()) {
1067                 logger.debug("LDAP search[{}ms]: {} - {}", System.currentTimeMillis() - startTime, baseDn, filter);
1068             }
1069             consumer.accept(list);
1070         } catch (final NamingException e) {
1071             throw new LdapOperationException("Failed to search " + baseDn + " with " + filter, e);
1072         }
1073     }
1074 
1075     protected void modifyAddEntry(final List<ModificationItem> modifyList, final String name, final String value) {
1076         final Attribute attr = new BasicAttribute(name, value);
1077         final ModificationItem mod = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr);
1078         modifyList.add(mod);
1079     }
1080 
1081     protected void modifyReplaceEntry(final List<ModificationItem> modifyList, final String name, final String value) {
1082         final Attribute attr = new BasicAttribute(name, value);
1083         final ModificationItem mod = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr);
1084         modifyList.add(mod);
1085     }
1086 
1087     protected void modifyDeleteEntry(final List<ModificationItem> modifyList, final String name, final Object value) {
1088         final Attribute attr = new BasicAttribute(name, value);
1089         final ModificationItem mod = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attr);
1090         modifyList.add(mod);
1091     }
1092 
1093     protected void modify(final String dn, final List<ModificationItem> modifyList, final Supplier<Hashtable<String, String>> envSupplier) {
1094         if (modifyList.isEmpty()) {
1095             return;
1096         }
1097         try (DirContextHolder holder = getDirContext(envSupplier)) {
1098             holder.get().modifyAttributes(dn, modifyList.toArray(new ModificationItem[modifyList.size()]));
1099         } catch (final NamingException e) {
1100             throw new LdapOperationException("Failed to search " + dn, e);
1101         }
1102     }
1103 
1104     interface SearchConsumer {
1105         void accept(List<SearchResult> t) throws NamingException;
1106     }
1107 
1108     protected DirContextHolder getDirContext(final Supplier<Hashtable<String, String>> envSupplier) {
1109         DirContextHolder holder = contextLocal.get();
1110         if (holder != null) {
1111             holder.inc();
1112             return holder;
1113         }
1114         final Hashtable<String, String> env = envSupplier.get();
1115         try {
1116             holder = new DirContextHolder(new InitialDirContext(env));
1117             contextLocal.set(holder);
1118             return holder;
1119         } catch (final NamingException e) {
1120             throw new LdapOperationException("Failed to create DirContext.", e);
1121         }
1122     }
1123 
1124     protected class DirContextHolder implements AutoCloseable {
1125         private final DirContext context;
1126 
1127         private int counter = 1;
1128 
1129         protected DirContextHolder(final DirContext context) {
1130             this.context = context;
1131         }
1132 
1133         public DirContext get() {
1134             return context;
1135         }
1136 
1137         public void inc() {
1138             counter++;
1139         }
1140 
1141         @Override
1142         public void close() {
1143             if (counter > 1) {
1144                 counter--;
1145             } else {
1146                 contextLocal.remove();
1147                 if (context != null) {
1148                     try {
1149                         context.close();
1150                     } catch (final NamingException e) {
1151                         // ignored
1152                     }
1153                 }
1154             }
1155         }
1156     }
1157 }