001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.userdirectory.synchronize; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.collections4.MapUtils; 034import org.apache.commons.lang.StringUtils; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import org.ametys.cms.repository.Content; 039import org.ametys.cms.repository.ModifiableDefaultContent; 040import org.ametys.core.user.directory.UserDirectory; 041import org.ametys.core.user.population.UserPopulation; 042import org.ametys.core.user.population.UserPopulationDAO; 043import org.ametys.core.util.JSONUtils; 044import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 045import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 046import org.ametys.plugins.contentio.synchronize.impl.LDAPCollectionHelper; 047import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory; 048import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory; 049import org.ametys.plugins.userdirectory.DeleteUserComponent; 050 051/** 052 * Implementation of {@link SynchronizableContentsCollection} to be synchronized with a {@link UserPopulation} of the CMS. 053 */ 054public class UserPopulationSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection 055{ 056 /** Name of parameter holding the id of population */ 057 protected static final String __PARAM_POPULATION_ID = "populationId"; 058 /** Name of parameter for the login metadata */ 059 protected static final String __PARAM_LOGIN_METADATA_NAME = "login"; 060 /** Name of parameter for the firstname metadata */ 061 protected static final String __PARAM_FIRSTNAME_METADATA_NAME = "firstname"; 062 /** Name of parameter for the lastname metadata */ 063 protected static final String __PARAM_LASTNAME_METADATA_NAME = "lastname"; 064 /** Name of parameter for the email metadata */ 065 protected static final String __PARAM_EMAIL_METADATA_NAME = "email"; 066 /** Name of parameter holding the fields mapping */ 067 protected static final String __PARAM_MAPPING = "mapping"; 068 /** Name of parameter holding the additional search filter */ 069 protected static final String __PARAM_ADDITIONAL_SEARCH_FILTER = "additionalSearchFilter"; 070 /** Name of parameter into mapping holding the synchronized property */ 071 protected static final String __PARAM_MAPPING_SYNCHRO = "synchro"; 072 /** Name of parameter into mapping holding the path of metadata */ 073 protected static final String __PARAM_MAPPING_METADATA_REF = "metadata-ref"; 074 /** Name of parameter into mapping holding the remote attribute */ 075 protected static final String __PARAM_MAPPING_ATTRIBUTE_PREFIX = "attribute-"; 076 077 /** The logger */ 078 protected static final Logger _LOGGER = LoggerFactory.getLogger(UserPopulationSynchronizableContentsCollection.class); 079 080 private static final int __LDAP_DEFAULT_PAGE_SIZE = 1000; 081 082 /** The DAO for user populations */ 083 protected UserPopulationDAO _userPopulationDAO; 084 /** The service manager */ 085 protected ServiceManager _manager; 086 /** The JSON utils */ 087 protected JSONUtils _jsonUtils; 088 /** The user SCC helper */ 089 protected UserSCCHelper _userSCCHelper; 090 /** The delete user component */ 091 protected DeleteUserComponent _deleteUserComponent; 092 093 /** Mapping of the metadata with source data */ 094 protected Map<String, Map<String, List<String>>> _mapping; 095 /** Synchronized fields */ 096 protected Set<String> _syncFields; 097 /** External fields */ 098 protected Set<String> _extFields; 099 100 @Override 101 public void service(ServiceManager manager) throws ServiceException 102 { 103 super.service(manager); 104 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 105 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 106 _userSCCHelper = (UserSCCHelper) manager.lookup(UserSCCHelper.ROLE); 107 _deleteUserComponent = (DeleteUserComponent) manager.lookup(DeleteUserComponent.ROLE); 108 _manager = manager; 109 } 110 111 public boolean handleRightAssignmentContext() 112 { 113 return false; 114 } 115 116 @Override 117 protected void configureDataSource(Configuration configuration) throws ConfigurationException 118 { 119 _mapping = new HashMap<>(); 120 _syncFields = new HashSet<>(); 121 _extFields = new HashSet<>(); 122 String mappingAsString = (String) getParameterValues().get(__PARAM_MAPPING); 123 if (StringUtils.isNotEmpty(mappingAsString)) 124 { 125 List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString); 126 for (Object object : mappingAsList) 127 { 128 @SuppressWarnings("unchecked") 129 Map<String, Object> field = (Map<String, Object>) object; 130 String metadataRef = (String) field.get(__PARAM_MAPPING_METADATA_REF); 131 132 String prefix = __PARAM_MAPPING_ATTRIBUTE_PREFIX; 133 for (String prefixedUserDirectoryKey : _getUserDirectoryKeys(field, prefix)) 134 { 135 String userDirectoryKey = prefixedUserDirectoryKey.substring(prefix.length()); 136 if (!_mapping.containsKey(userDirectoryKey)) 137 { 138 _mapping.put(userDirectoryKey, new HashMap<>()); 139 } 140 141 String[] attributes = ((String) field.get(prefixedUserDirectoryKey)).split(","); 142 143 Map<String, List<String>> userDirectoryMapping = _mapping.get(userDirectoryKey); 144 userDirectoryMapping.put(metadataRef, Arrays.asList(attributes)); 145 } 146 147 boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false; 148 if (isSynchronized) 149 { 150 _syncFields.add(metadataRef); 151 } 152 else 153 { 154 _extFields.add(metadataRef); 155 } 156 } 157 } 158 } 159 160 @Override 161 protected void configureSearchModel() 162 { 163 _LOGGER.info("Currently, SCC '{}' cannot define a search model", UserPopulationSynchronizableContentsCollection.class.getName()); 164 } 165 166 @Override 167 protected List<ModifiableDefaultContent> _internalPopulate(Logger logger) 168 { 169 List<ModifiableDefaultContent> contents = new ArrayList<>(); 170 171 UserPopulation population = _userPopulationDAO.getUserPopulation(getPopulationId()); 172 173 for (UserDirectory userDirectory : population.getUserDirectories()) 174 { 175 Map<String, Object> searchParams = new HashMap<>(); 176 searchParams.put("userDirectory", userDirectory); 177 contents.addAll(_importOrSynchronizeContents(searchParams, false, logger)); 178 } 179 180 return contents; 181 } 182 183 /** 184 * Search contents from a LDAP user directory of the population. 185 * To avoid code duplication and useless operations, we return a {@link Map}<{@link String}, {@link Map}<{@link String}, {@link Object}>> 186 * if getRemoteValues is set to false and {@link Map}<{@link String}, {@link Map}<{@link String}, {@link List}<{@link Object}>>> 187 * if remoteValues is true. 188 * Without this operation, we have to duplicate the code of searchLDAP and _internalSearch methods. 189 * @param userDirectory The LDAP user directory 190 * @param parameters Parameters for the search 191 * @param offset Begin of the search 192 * @param limit Number of results 193 * @param logger The logger 194 * @param getRemoteValues if true, values are organized by the metadata mapping 195 * @return Contents found in LDAP 196 */ 197 @SuppressWarnings("unchecked") 198 protected Map<String, Map<String, Object>> searchLDAP(LdapUserDirectory userDirectory, Map<String, Object> parameters, int offset, int limit, Logger logger, boolean getRemoteValues) 199 { 200 Map<String, Map<String, Object>> results = new HashMap<>(); 201 202 Map<String, Object> ldapParameterValues = userDirectory.getParameterValues(); 203 String dataSourceId = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_DATASOURCE_ID); 204 String relativeDN = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_RELATIVE_DN); 205 String filter = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_OBJECT_FILTER); 206 String searchScope = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_SEARCH_SCOPE); 207 String loginAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_LOGIN_ATTRIBUTE); 208 209 Map<String, List<String>> udMapping = _mapping.get(userDirectory.getId()); 210 if (udMapping == null) 211 { 212 udMapping = new HashMap<>(); 213 } 214 udMapping.put(getLoginMetadata(), Collections.singletonList(loginAttr)); 215 216 // If first name metadata is set 217 String firstNameMetadata = getFirstNameMetadata(); 218 if (StringUtils.isNotBlank(firstNameMetadata)) 219 { 220 String firstNameAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_FIRSTNAME_ATTRIBUTE); 221 udMapping.put(firstNameMetadata, Collections.singletonList(firstNameAttr)); 222 } 223 224 // If last name metadata is set 225 String lastNameMetadata = getLastNameMetadata(); 226 if (StringUtils.isNotBlank(lastNameMetadata)) 227 { 228 String lastNameAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_LASTNAME_ATTRIBUTE); 229 udMapping.put(lastNameMetadata, Collections.singletonList(lastNameAttr)); 230 } 231 232 // If email metadata is set 233 String emailMetadata = getEmailMetadata(); 234 if (StringUtils.isNotBlank(emailMetadata)) 235 { 236 String emailAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_EMAIL_ATTRIBUTE); 237 udMapping.put(emailMetadata, Collections.singletonList(emailAttr)); 238 } 239 240 try 241 { 242 LDAPCollectionHelper ldapHelper = (LDAPCollectionHelper) _manager.lookup(LDAPCollectionHelper.ROLE); 243 ldapHelper._delayedInitialize(dataSourceId); 244 245 List<String> filters = new ArrayList<>(); 246 if (StringUtils.isNotEmpty(filter)) 247 { 248 filters.add(filter); 249 } 250 if (parameters != null) 251 { 252 for (String parameterName : parameters.keySet()) 253 { 254 filters.add(parameterName + "=" + parameters.get(parameterName)); 255 } 256 } 257 String additionalSearchFilter = getAdditionalSearchFilter(); 258 if (StringUtils.isNotEmpty(additionalSearchFilter)) 259 { 260 filters.add(additionalSearchFilter); 261 } 262 263 String filtersReduced = filters.stream().filter(StringUtils::isNotEmpty).map(s -> "(" + s + ")").reduce("", (s1, s2) -> s1 + s2); 264 if (!filtersReduced.isEmpty()) 265 { 266 filtersReduced = "(&" + filtersReduced + ")"; 267 } 268 269 results = ldapHelper.search(getId(), __LDAP_DEFAULT_PAGE_SIZE, relativeDN, filtersReduced, searchScope, offset, limit, udMapping, loginAttr, logger); 270 if (getRemoteValues) 271 { 272 Map<String, Map<String, List<Object>>> organizedResults = _sccHelper.organizeRemoteValuesByMetadata(results, udMapping); 273 ldapHelper.transformTypedAttributes(organizedResults, getContentType(), udMapping.keySet()); 274 results = (Map<String, Map<String, Object>>) (Object) organizedResults; 275 } 276 _nbError = ldapHelper.getNbErrors(); 277 _hasGlobalError = ldapHelper.hasGlobalError(); 278 } 279 catch (Exception e) 280 { 281 throw new RuntimeException("An error occured when importing from LDAP UserDirectory", e); 282 } 283 284 return results; 285 } 286 287 @Override 288 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger) 289 { 290 return _internalSearch(parameters, offset, limit, sort, logger, false); 291 } 292 293 /** 294 * Internal search 295 * @param parameters the search parameters 296 * @param offset starting index 297 * @param limit max number of results 298 * @param sort not used 299 * @param logger the logger 300 * @param getRemoteValues to get remote values or not 301 * @return The search result 302 */ 303 private Map<String, Map<String, Object>> _internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger, boolean getRemoteValues) 304 { 305 Map<String, Map<String, Object>> results = new LinkedHashMap<>(); 306 307 List<UserDirectory> userDirectories = new ArrayList<>(); 308 if (parameters.containsKey("userDirectory")) 309 { 310 userDirectories.add((UserDirectory) parameters.get("userDirectory")); 311 parameters.remove("userDirectory"); 312 } 313 else 314 { 315 UserPopulation population = _userPopulationDAO.getUserPopulation(getPopulationId()); 316 userDirectories = population.getUserDirectories(); 317 } 318 319 for (UserDirectory userDirectory : userDirectories) 320 { 321 if (userDirectory instanceof LdapUserDirectory) 322 { 323 // Sort is ignored for LDAP 324 results.putAll(searchLDAP((LdapUserDirectory) userDirectory, parameters, offset, limit, logger, getRemoteValues)); 325 } 326 else if (userDirectory instanceof JdbcUserDirectory) 327 { 328 // TODO handle SQL case 329 // remoteValuesByContent = searchSQL((JdbcUserDirectory) userDirectory, logger); 330 } 331 } 332 333 return results; 334 } 335 336 @Override 337 protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableDefaultContent content, boolean create, Logger logger) 338 { 339 boolean hasChanges = super._fillContent(remoteValues, content, create, logger); 340 341 String newLogin = remoteValues.get(getIdField()).get(0).toString(); 342 hasChanges = _userSCCHelper.synchronizeUserMetadata(newLogin, getPopulationId(), content, logger) || hasChanges; 343 344 return hasChanges; 345 } 346 347 @Override 348 @SuppressWarnings("unchecked") 349 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> parameters, Logger logger) 350 { 351 return (Map<String, Map<String, List<Object>>>) (Object) _internalSearch(parameters, 0, Integer.MAX_VALUE, null, logger, true); 352 } 353 354 /** 355 * Get the id of the user population 356 * @return The id of user population 357 */ 358 public String getPopulationId() 359 { 360 return (String) getParameterValues().get(__PARAM_POPULATION_ID); 361 } 362 363 @Override 364 public String getIdField() 365 { 366 return getLoginMetadata(); 367 } 368 369 /** 370 * Get the metadata name for the login 371 * @return The the metadata name for the login 372 */ 373 public String getLoginMetadata() 374 { 375 return UserSCCHelper.USER_UNIQUE_ID_METADATA_NAME; 376 } 377 378 /** 379 * Get the metadata name for the first name 380 * @return The the metadata name for the first name 381 */ 382 public String getFirstNameMetadata() 383 { 384 return (String) getParameterValues().get(__PARAM_FIRSTNAME_METADATA_NAME); 385 } 386 387 /** 388 * Get the metadata name for the last name 389 * @return The the metadata name for the last name 390 */ 391 public String getLastNameMetadata() 392 { 393 return (String) getParameterValues().get(__PARAM_LASTNAME_METADATA_NAME); 394 } 395 396 /** 397 * Get the metadata name for the email 398 * @return The the metadata name for the email 399 */ 400 public String getEmailMetadata() 401 { 402 return (String) getParameterValues().get(__PARAM_EMAIL_METADATA_NAME); 403 } 404 405 /** 406 * Get the additional filter for searching 407 * @return The additional filter for searching 408 */ 409 public String getAdditionalSearchFilter() 410 { 411 return (String) getParameterValues().get(__PARAM_ADDITIONAL_SEARCH_FILTER); 412 } 413 414 @Override 415 public Set<String> getExternalOnlyFields(Map<String, Object> additionalParameters) 416 { 417 return _extFields; 418 } 419 420 @Override 421 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 422 { 423 return _syncFields; 424 } 425 426 private Set<String> _getUserDirectoryKeys(Map<String, Object> field, String prefix) 427 { 428 return field.keySet().stream() 429 .filter(name -> name.startsWith(prefix)) 430 .collect(Collectors.toSet()); 431 } 432 433 @Override 434 protected Map<String, Object> putIdParameter(String idValue) 435 { 436 Map<String, Object> parameters = new HashMap<>(); 437 438 for (String userDirectory : _mapping.keySet()) 439 { 440 List<String> remoteKeys = _mapping.get(userDirectory).get(getIdField()); 441 if (remoteKeys != null && remoteKeys.size() > 0) 442 { 443 parameters.put(userDirectory + "$" + remoteKeys.get(0), idValue); 444 } 445 } 446 447 return parameters; 448 } 449 450 @Override 451 protected int _deleteContents(List<Content> contentsToRemove, Logger logger) 452 { 453 return _deleteUserComponent.deleteContentsWithLog(contentsToRemove, MapUtils.EMPTY_SORTED_MAP, MapUtils.EMPTY_SORTED_MAP, logger); 454 } 455}