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