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