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.HashMap; 021import java.util.HashSet; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.commons.lang.StringUtils; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.repository.ModifiableContent; 038import org.ametys.core.schedule.progression.ContainerProgressionTracker; 039import org.ametys.core.user.UserIdentity; 040import org.ametys.core.user.directory.StoredUser; 041import org.ametys.core.user.directory.UserDirectory; 042import org.ametys.core.user.population.UserPopulation; 043import org.ametys.core.user.population.UserPopulationDAO; 044import org.ametys.core.util.JSONUtils; 045import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 046import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 047import org.ametys.plugins.contentio.synchronize.impl.LDAPCollectionHelper; 048import org.ametys.plugins.contentio.synchronize.impl.LDAPCollectionHelper.LDAPCollectionHelperSearchResult; 049import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory; 050import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory; 051import org.ametys.plugins.userdirectory.DeleteOrgUnitComponent; 052import org.ametys.plugins.userdirectory.DeleteUserComponent; 053import org.ametys.runtime.i18n.I18nizableText; 054 055/** 056 * Implementation of {@link SynchronizableContentsCollection} to be synchronized with a {@link UserPopulation} of the CMS. 057 */ 058public class UserPopulationSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection 059{ 060 /** Name of parameter holding the id of population */ 061 protected static final String __PARAM_POPULATION_ID = "populationId"; 062 /** Name of parameter for the firstname attribute */ 063 protected static final String __PARAM_FIRSTNAME_ATTRIBUTE_NAME = "firstname"; 064 /** Name of parameter for the lastname attribute */ 065 protected static final String __PARAM_LASTNAME_ATTRIBUTE_NAME = "lastname"; 066 /** Name of parameter for the email attribute */ 067 protected static final String __PARAM_EMAIL_ATTRIBUTE_NAME = "email"; 068 /** Name of parameter holding the fields mapping */ 069 protected static final String __PARAM_MAPPING = "mapping"; 070 /** Name of parameter holding the additional search filter */ 071 protected static final String __PARAM_ADDITIONAL_SEARCH_FILTER = "additionalSearchFilter"; 072 /** Name of parameter into mapping holding the synchronized property */ 073 protected static final String __PARAM_MAPPING_SYNCHRO = "synchro"; 074 /** Name of parameter into mapping holding the path of attribute */ 075 protected static final String __PARAM_MAPPING_ATTRIBUTE_REF = "metadata-ref"; 076 /** Name of parameter into mapping holding the remote attribute */ 077 protected static final String __PARAM_MAPPING_ATTRIBUTE_PREFIX = "attribute-"; 078 079 /** The logger */ 080 protected static final Logger _LOGGER = LoggerFactory.getLogger(UserPopulationSynchronizableContentsCollection.class); 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 delete user component */ 089 protected DeleteUserComponent _deleteUserComponent; 090 091 /** Mapping of the attributes with source data */ 092 protected Map<String, Map<String, List<String>>> _mapping; 093 /** Synchronized fields */ 094 protected Set<String> _syncFields; 095 /** LDAP collection helper fields */ 096 protected LDAPCollectionHelper _ldapHelper; 097 098 @Override 099 public void service(ServiceManager manager) throws ServiceException 100 { 101 super.service(manager); 102 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 103 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 104 _deleteUserComponent = (DeleteUserComponent) manager.lookup(DeleteUserComponent.ROLE); 105 _ldapHelper = (LDAPCollectionHelper) manager.lookup(LDAPCollectionHelper.ROLE); 106 _manager = manager; 107 } 108 109 public boolean handleRightAssignmentContext() 110 { 111 return false; 112 } 113 114 @Override 115 protected void configureDataSource(Configuration configuration) throws ConfigurationException 116 { 117 _mapping = new HashMap<>(); 118 _syncFields = new HashSet<>(); 119 String mappingAsString = (String) getParameterValues().get(__PARAM_MAPPING); 120 if (StringUtils.isNotEmpty(mappingAsString)) 121 { 122 List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString); 123 for (Object object : mappingAsList) 124 { 125 @SuppressWarnings("unchecked") 126 Map<String, Object> field = (Map<String, Object>) object; 127 String attributeRef = (String) field.get(__PARAM_MAPPING_ATTRIBUTE_REF); 128 129 String prefix = __PARAM_MAPPING_ATTRIBUTE_PREFIX; 130 for (String prefixedUserDirectoryKey : _getUserDirectoryKeys(field, prefix)) 131 { 132 String userDirectoryKey = prefixedUserDirectoryKey.substring(prefix.length()); 133 if (!_mapping.containsKey(userDirectoryKey)) 134 { 135 _mapping.put(userDirectoryKey, new HashMap<>()); 136 } 137 138 String[] attributes = ((String) field.get(prefixedUserDirectoryKey)).split(","); 139 140 Map<String, List<String>> userDirectoryMapping = _mapping.get(userDirectoryKey); 141 userDirectoryMapping.put(attributeRef, Arrays.asList(attributes)); 142 } 143 144 boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false; 145 if (isSynchronized) 146 { 147 _syncFields.add(attributeRef); 148 } 149 } 150 } 151 } 152 153 @Override 154 protected void configureSearchModel() 155 { 156 _LOGGER.info("Currently, SCC '{}' cannot define a search model", UserPopulationSynchronizableContentsCollection.class.getName()); 157 } 158 159 @Override 160 protected List<ModifiableContent> _internalPopulate(Logger logger, ContainerProgressionTracker progressionTracker) 161 { 162 List<ModifiableContent> contents = new ArrayList<>(); 163 164 UserPopulation population = _userPopulationDAO.getUserPopulation(getPopulationId()); 165 166 for (UserDirectory userDirectory : population.getUserDirectories()) 167 { 168 String labelOrId = userDirectory.getLabel().isEmpty() ? userDirectory.getId() : userDirectory.getLabel(); 169 progressionTracker.addContainerStep(userDirectory.getId(), new I18nizableText("plugin.user-directory", "PLUGINS_USER_DIRECTORY_SCHEDULER_SYNCHRONIZE_COLLECTION_USER_DIRECTORY_STEP_LABEL", List.of(labelOrId))); 170 } 171 172 for (UserDirectory userDirectory : population.getUserDirectories()) 173 { 174 Map<String, Object> searchParams = new HashMap<>(); 175 searchParams.put("userDirectory", userDirectory); 176 contents.addAll(_importOrSynchronizeContents(searchParams, false, logger, progressionTracker.getStep(userDirectory.getId()))); 177 } 178 179 return contents; 180 } 181 182 /** 183 * Search contents from a LDAP user directory of the population. 184 * To avoid code duplication and useless operations, we return a {@link Map}<{@link String}, {@link Map}<{@link String}, {@link Object}>> 185 * if getRemoteValues is set to false and {@link Map}<{@link String}, {@link Map}<{@link String}, {@link List}<{@link Object}>>> 186 * if remoteValues is true. 187 * Without this operation, we have to duplicate the code of searchLDAP and _internalSearch methods. 188 * @param userDirectory The LDAP user directory 189 * @param searchParameters Parameters for the search 190 * @param offset Begin of the search 191 * @param limit Number of results 192 * @param logger The logger 193 * @param getRemoteValues if <code>true</code>, values are organized by the attribute mapping 194 * @return Contents found in LDAP 195 */ 196 @SuppressWarnings("unchecked") 197 protected Map<String, Map<String, Object>> searchLDAP(LdapUserDirectory userDirectory, Map<String, Object> searchParameters, int offset, int limit, Logger logger, boolean getRemoteValues) 198 { 199 Map<String, Map<String, Object>> searchResults = new HashMap<>(); 200 201 Map<String, Object> ldapParameterValues = userDirectory.getParameterValues(); 202 String dataSourceId = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_DATASOURCE_ID); 203 String relativeDN = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_RELATIVE_DN); 204 String filter = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_OBJECT_FILTER); 205 String searchScope = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_SEARCH_SCOPE); 206 String loginAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_LOGIN_ATTRIBUTE); 207 208 Map<String, List<String>> udMapping = _mapping.getOrDefault(userDirectory.getId(), new HashMap<>()); 209 udMapping.put(getLoginAttributeName(), List.of(loginAttr)); 210 211 // If first name attribute is set 212 String firstNameAttribute = getFirstNameAttributeName(); 213 if (StringUtils.isNotBlank(firstNameAttribute)) 214 { 215 String firstNameAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_FIRSTNAME_ATTRIBUTE); 216 udMapping.put(firstNameAttribute, List.of(firstNameAttr)); 217 } 218 219 // If last name attribute is set 220 String lastNameAttribute = getLastNameAttributeName(); 221 if (StringUtils.isNotBlank(lastNameAttribute)) 222 { 223 String lastNameAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_LASTNAME_ATTRIBUTE); 224 udMapping.put(lastNameAttribute, List.of(lastNameAttr)); 225 } 226 227 // If email attribute is set 228 String emailAttribute = getEmailAttributeName(); 229 if (StringUtils.isNotBlank(emailAttribute)) 230 { 231 String emailAttr = (String) ldapParameterValues.get(LdapUserDirectory.PARAM_USERS_EMAIL_ATTRIBUTE); 232 udMapping.put(emailAttribute, List.of(emailAttr)); 233 } 234 235 try 236 { 237 List<String> filters = new ArrayList<>(); 238 if (StringUtils.isNotEmpty(filter)) 239 { 240 filters.add(filter); 241 } 242 243 if (searchParameters != null) 244 { 245 for (String parameterName : searchParameters.keySet()) 246 { 247 filters.add(parameterName + "=" + searchParameters.get(parameterName)); 248 } 249 } 250 251 String additionalSearchFilter = getAdditionalSearchFilter(); 252 if (StringUtils.isNotEmpty(additionalSearchFilter)) 253 { 254 filters.add(additionalSearchFilter); 255 } 256 257 String filtersReduced = filters.stream() 258 .filter(StringUtils::isNotEmpty) 259 .map(s -> "(" + s + ")") 260 .reduce("", (s1, s2) -> s1 + s2); 261 if (!filtersReduced.isEmpty()) 262 { 263 filtersReduced = "(&" + filtersReduced + ")"; 264 } 265 266 LDAPCollectionHelperSearchResult results = _ldapHelper.search(getId(), relativeDN, filtersReduced, searchScope, offset, limit, udMapping, loginAttr, logger, dataSourceId); 267 searchResults = results.searchResults(); 268 269 if (getRemoteValues) 270 { 271 Map<String, Map<String, List<Object>>> organizedResults = _sccHelper.organizeRemoteValuesByAttribute(searchResults, udMapping); 272 _ldapHelper.transformTypedAttributes(organizedResults, getContentType(), udMapping.keySet()); 273 searchResults = (Map<String, Map<String, Object>>) (Object) organizedResults; 274 } 275 276 _nbError = results.nbErrors(); 277 _hasGlobalError = results.hasGlobalError(); 278 } 279 catch (Exception e) 280 { 281 throw new RuntimeException("An error occured when importing from LDAP UserDirectory", e); 282 } 283 284 return searchResults; 285 } 286 287 @Override 288 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger) 289 { 290 return _internalSearch(searchParameters, offset, limit, sort, logger, false); 291 } 292 293 /** 294 * Internal search 295 * @param searchParameters 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> searchParameters, 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 (searchParameters.containsKey("userDirectory")) 309 { 310 userDirectories.add((UserDirectory) searchParameters.get("userDirectory")); 311 searchParameters.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, searchParameters, offset, limit, logger, getRemoteValues)); 325 } 326 else if (userDirectory instanceof JdbcUserDirectory) 327 { 328 // TODO handle SQL case 329 // remoteValuesByContent = searchSQL((JdbcUserDirectory) userDirectory, logger); 330 logger.warn("Population with SQL datasource is not implemented."); 331 } 332 else 333 { 334 @SuppressWarnings("unchecked") 335 Map<String, Map<String, Object>> userAsSearchResults = userDirectory.getStoredUsers() 336 .stream() 337 .map(this::_userToSearchResult) 338 .collect(Collectors.toMap( 339 m -> ((List<String>) m.get(getIdField())).get(0), 340 m -> m 341 )); 342 343 results.putAll(userAsSearchResults); 344 } 345 } 346 347 return results; 348 } 349 350 /** 351 * Transform user to search result 352 * @param storedUser the user 353 * @return the search result 354 */ 355 protected Map<String, Object> _userToSearchResult(StoredUser storedUser) 356 { 357 Map<String, Object> json = new HashMap<>(); 358 359 json.put(getIdField(), List.of(storedUser.getIdentifier())); 360 361 String firstName = storedUser.getFirstName(); 362 json.put(getFirstNameAttributeName(), StringUtils.isNotBlank(firstName) ? List.of(firstName) : List.of()); 363 364 String lastName = storedUser.getLastName(); 365 json.put(getLastNameAttributeName(), StringUtils.isNotBlank(lastName) ? List.of(lastName) : List.of()); 366 367 String email = storedUser.getEmail(); 368 json.put(getEmailAttributeName(), StringUtils.isNotBlank(email) ? List.of(email) : List.of()); 369 370 return json; 371 } 372 373 @Override 374 @SuppressWarnings("unchecked") 375 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger) 376 { 377 return (Map<String, Map<String, List<Object>>>) (Object) _internalSearch(searchParameters, 0, Integer.MAX_VALUE, null, logger, true); 378 } 379 380 @Override 381 protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger) 382 { 383 Map<String, Object> additionalRemoteValues = super.getAdditionalAttributeValues(idValue, content, additionalParameters, create, logger); 384 UserIdentity user = new UserIdentity(idValue, getPopulationId()); 385 additionalRemoteValues.put(UserSCCConstants.USER_ATTRIBUTE_NAME, user); 386 return additionalRemoteValues; 387 } 388 389 /** 390 * Get the id of the user population 391 * @return The id of user population 392 */ 393 public String getPopulationId() 394 { 395 return (String) getParameterValues().get(__PARAM_POPULATION_ID); 396 } 397 398 @Override 399 public String getIdField() 400 { 401 return getLoginAttributeName(); 402 } 403 404 /** 405 * Get the attribute name for the login 406 * @return The the attribute name for the login 407 */ 408 public String getLoginAttributeName() 409 { 410 return UserSCCConstants.USER_UNIQUE_ID_ATTRIBUTE_NAME; 411 } 412 413 /** 414 * Get the attribute name for the first name 415 * @return The the attribute name for the first name 416 */ 417 public String getFirstNameAttributeName() 418 { 419 return (String) getParameterValues().get(__PARAM_FIRSTNAME_ATTRIBUTE_NAME); 420 } 421 422 /** 423 * Get the attribute name for the last name 424 * @return The the attribute name for the last name 425 */ 426 public String getLastNameAttributeName() 427 { 428 return (String) getParameterValues().get(__PARAM_LASTNAME_ATTRIBUTE_NAME); 429 } 430 431 /** 432 * Get the attribute name for the email 433 * @return The the attribute name for the email 434 */ 435 public String getEmailAttributeName() 436 { 437 return (String) getParameterValues().get(__PARAM_EMAIL_ATTRIBUTE_NAME); 438 } 439 440 /** 441 * Get the additional filter for searching 442 * @return The additional filter for searching 443 */ 444 public String getAdditionalSearchFilter() 445 { 446 return (String) getParameterValues().get(__PARAM_ADDITIONAL_SEARCH_FILTER); 447 } 448 449 @Override 450 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 451 { 452 return _syncFields; 453 } 454 455 private Set<String> _getUserDirectoryKeys(Map<String, Object> field, String prefix) 456 { 457 return field.keySet().stream() 458 .filter(name -> name.startsWith(prefix)) 459 .collect(Collectors.toSet()); 460 } 461 462 @Override 463 protected Map<String, Object> putIdParameter(String idValue) 464 { 465 Map<String, Object> parameters = new HashMap<>(); 466 467 for (String userDirectory : _mapping.keySet()) 468 { 469 List<String> remoteKeys = _mapping.get(userDirectory).get(getIdField()); 470 if (remoteKeys != null && remoteKeys.size() > 0) 471 { 472 parameters.put(userDirectory + "$" + remoteKeys.get(0), idValue); 473 } 474 } 475 476 return parameters; 477 } 478 479 @Override 480 protected int _deleteContents(List<Content> contentsToRemove, Logger logger) 481 { 482 return _deleteUserComponent.deleteContentsWithLog(contentsToRemove, Map.of(DeleteOrgUnitComponent.SCC_ID_PARAMETERS_KEY, getId()), Map.of(), logger); 483 } 484}