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.core.user.population; 017 018import java.sql.Connection; 019import java.sql.PreparedStatement; 020import java.sql.ResultSet; 021import java.sql.SQLException; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.core.ObservationConstants; 037import org.ametys.core.datasource.ConnectionHelper; 038import org.ametys.core.observation.Event; 039import org.ametys.core.observation.ObservationManager; 040import org.ametys.core.right.RightManager; 041import org.ametys.core.right.RightManager.RightResult; 042import org.ametys.core.ui.Callable; 043import org.ametys.core.user.CurrentUserProvider; 044import org.ametys.core.user.UserIdentity; 045import org.ametys.runtime.authentication.AccessDeniedException; 046import org.ametys.runtime.config.Config; 047import org.ametys.runtime.plugin.component.AbstractLogEnabled; 048 049/** 050 * Helper for associating {@link UserPopulation}s to contexts. 051 */ 052public class PopulationContextHelper extends AbstractLogEnabled implements Component, Serviceable 053{ 054 /** Avalon Role */ 055 public static final String ROLE = PopulationContextHelper.class.getName(); 056 057 /** The name of request attribute holding the current population contexts */ 058 public static final String POPULATION_CONTEXTS_REQUEST_ATTR = "populationContexts"; 059 060 /** The "admin" context */ 061 public static final String ADMIN_CONTEXT = "/admin"; 062 063 /** The name of the JDBC table for user populations by context */ 064 protected static final String __USER_POPULATIONS_TABLE = "UserPopulationsByContext"; 065 /** The id of right for administration access */ 066 protected static final String __ADMIN_RIGHT_ACCESS = "Runtime_Rights_Admin_Access"; 067 068 /** The service manager */ 069 protected ServiceManager _manager; 070 071 /** The DAO for {@link UserPopulation}s */ 072 private UserPopulationDAO _userPopulationDAO; 073 074 private ObservationManager _observationManager; 075 076 private CurrentUserProvider _currentUserProvider; 077 078 private RightManager _rightManager; 079 080 @Override 081 public void service(ServiceManager manager) throws ServiceException 082 { 083 _manager = manager; 084 } 085 086 /** 087 * Get the observation manager 088 * @return the 089 */ 090 protected ObservationManager getObservationManager() 091 { 092 if (_observationManager == null) 093 { 094 try 095 { 096 _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE); 097 } 098 catch (ServiceException e) 099 { 100 throw new RuntimeException(e); 101 } 102 } 103 return _observationManager; 104 } 105 106 /** 107 * Get the DAO for user populations 108 * @return the DAO for user populations 109 */ 110 protected UserPopulationDAO getUserPopulationDAO() 111 { 112 if (_userPopulationDAO == null) 113 { 114 try 115 { 116 _userPopulationDAO = (UserPopulationDAO) _manager.lookup(UserPopulationDAO.ROLE); 117 } 118 catch (ServiceException e) 119 { 120 throw new RuntimeException(e); 121 } 122 } 123 return _userPopulationDAO; 124 } 125 126 /** 127 * Get the current user provider 128 * @return the current user provider 129 */ 130 protected CurrentUserProvider getCurrentUserProvider() 131 { 132 if (_currentUserProvider == null) 133 { 134 try 135 { 136 _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE); 137 } 138 catch (ServiceException e) 139 { 140 throw new RuntimeException(e); 141 } 142 } 143 return _currentUserProvider; 144 } 145 146 /** 147 * Get the right manager 148 * @return the right manager 149 */ 150 protected RightManager getRightManager() 151 { 152 if (_rightManager == null) 153 { 154 try 155 { 156 _rightManager = (RightManager) _manager.lookup(RightManager.ROLE); 157 } 158 catch (ServiceException e) 159 { 160 throw new RuntimeException(e); 161 } 162 } 163 return _rightManager; 164 } 165 166 /** 167 * Get the connection to the database 168 * @return the SQL connection 169 */ 170 protected Connection getSQLConnection () 171 { 172 String datasourceId = Config.getInstance().getValue("runtime.assignments.userpopulations"); 173 return ConnectionHelper.getConnection(datasourceId); 174 } 175 176 /** 177 * Links given populations to a context. 178 * @param context The context 179 * @param ids The ids of the populations to link 180 * @return The ids of the changed user populations (the ones unlinked and the ones linked) 181 */ 182 @SuppressWarnings("resource") 183 @Callable 184 public Set<String> link(String context, Collection<String> ids) 185 { 186 Set<String> result = getUserPopulationsOnContext(context, true); // Get the already linked ids 187 188 Connection connection = null; 189 PreparedStatement stmt = null; 190 try 191 { 192 connection = getSQLConnection(); 193 194 // Remove all the ids affected to this context 195 String sql = "DELETE FROM " + __USER_POPULATIONS_TABLE + " WHERE Context=?"; 196 stmt = connection.prepareStatement(sql); 197 stmt.setString(1, context); 198 stmt.executeUpdate(); 199 getLogger().info("{}\n[{}]", sql, context); 200 201 // Set the new ids to this context 202 sql = "INSERT INTO " + __USER_POPULATIONS_TABLE + " (Context, Ordering, UserPopulation_Id) VALUES(?, ?, ?)"; 203 stmt = connection.prepareStatement(sql); 204 205 int index = 0; 206 for (String id : ids) 207 { 208 if (getUserPopulationDAO().getUserPopulation(id) != null) 209 { 210 stmt.setString(1, context); 211 stmt.setInt(2, index); 212 stmt.setString(3, id); 213 stmt.executeUpdate(); 214 215 getLogger().info("{}\n[{}, {}, {}]", sql, context, index, id); 216 if (result.contains(id)) 217 { 218 result.remove(id); // the population was already linked, so its status didn't changed 219 } 220 else 221 { 222 result.add(id); 223 } 224 } 225 else 226 { 227 getLogger().warn("The user population with id '{}' does not exist. It will not be linked.", id); 228 } 229 230 index++; 231 } 232 } 233 catch (SQLException e) 234 { 235 getLogger().error("SQL error while linking user populations to a context", e); 236 } 237 finally 238 { 239 ConnectionHelper.cleanup(connection); 240 ConnectionHelper.cleanup(stmt); 241 } 242 243 Map<String, Object> eventParams = new HashMap<>(); 244 eventParams.put(ObservationConstants.ARGS_USERPOPULATION_IDS, ids); 245 eventParams.put(ObservationConstants.ARGS_USERPOPULATION_CONTEXT, context); 246 getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATIONS_ASSIGNMENT, getCurrentUserProvider().getUser(), eventParams)); 247 248 return result; 249 } 250 251 /** 252 * Gets the populations linked to the given context (need the population to be enabled) 253 * @param context The context 254 * @param withDisabled True to also return disabled populations 255 * @return The ids of populations linked to the context 256 */ 257 public Set<String> getUserPopulationsOnContext(String context, boolean withDisabled) 258 { 259 if (ADMIN_CONTEXT.equals(context)) 260 { 261 // Return all the enabled populations 262 List<UserPopulation> populations = withDisabled ? getUserPopulationDAO().getUserPopulations(true) : getUserPopulationDAO().getEnabledUserPopulations(true); 263 return populations.stream().map(UserPopulation::getId).collect(Collectors.toSet()); 264 } 265 else 266 { 267 return _getPopulationsOnContextFromDatabase(context, withDisabled); 268 } 269 } 270 271 /** 272 * Gets the populations linked to at least one of the given contexts (need the population to be enabled) 273 * @param contexts The contexts 274 * @param withDisabled True to also return disabled populations 275 * @param checkRights True to check that current user belongs to one of populations or is an administrator user 276 * @return The ids of populations linked to the contexts 277 */ 278 public Set<String> getUserPopulationsOnContexts(Collection<String> contexts, boolean withDisabled, boolean checkRights) 279 { 280 UserIdentity currentUser = getCurrentUserProvider().getUser(); 281 if (checkRights && contexts.contains(ADMIN_CONTEXT) && getRightManager().currentUserHasRight(__ADMIN_RIGHT_ACCESS, ADMIN_CONTEXT) != RightResult.RIGHT_ALLOW) 282 { 283 throw new AccessDeniedException("User " + getCurrentUserProvider().getUser() + " tried to access the list of all user populations without convenient rights"); 284 } 285 286 if (!checkRights) 287 { 288 return contexts.stream() 289 .map(context -> getUserPopulationsOnContext(context, withDisabled)) 290 .flatMap(Set::stream) 291 .collect(Collectors.toSet()); 292 } 293 294 if (currentUser == null) 295 { 296 throw new AccessDeniedException("Anonymous user tried to access the list of user populations on contexts '" + StringUtils.join(contexts, ",") + "'."); 297 } 298 299 boolean isAdministrator = getRightManager().currentUserHasRight(__ADMIN_RIGHT_ACCESS, ADMIN_CONTEXT) == RightResult.RIGHT_ALLOW; 300 Set<String> populations = new HashSet<>(); 301 302 // Check current user belongs to a population on each context 303 for (String context : contexts) 304 { 305 Set<String> ctxPopulations = getUserPopulationsOnContext(context, withDisabled); 306 307 if (!ctxPopulations.contains(currentUser.getPopulationId()) && !isAdministrator) 308 { 309 throw new AccessDeniedException("User " + currentUser + " tried to access the list of user populations on context '" + context + "', but he does not belong to any populations on this context."); 310 } 311 312 populations.addAll(ctxPopulations); 313 } 314 315 return populations; 316 } 317 318 /** 319 * Gets the populations linked to at least one of the given contexts (need the population to be enabled) 320 * @param contexts The contexts 321 * @param withDisabled True to also return disabled populations 322 * @return The ids of populations linked to the contexts 323 */ 324 @Callable 325 public Set<String> getUserPopulationsOnContexts(Collection<String> contexts, boolean withDisabled) 326 { 327 return getUserPopulationsOnContexts(contexts, withDisabled, true); 328 } 329 330 private Set<String> _getPopulationsOnContextFromDatabase(String context, boolean withDisabled) 331 { 332 Set<String> result = new java.util.HashSet<>(); 333 334 Connection connection = null; 335 PreparedStatement stmt = null; 336 ResultSet rs = null; 337 try 338 { 339 connection = getSQLConnection(); 340 341 String sql = "SELECT Context, Ordering, UserPopulation_Id FROM " + __USER_POPULATIONS_TABLE + " WHERE Context=? ORDER BY Ordering"; 342 stmt = connection.prepareStatement(sql); 343 stmt.setString(1, context); 344 rs = stmt.executeQuery(); 345 getLogger().info("{}\n[{}]", sql, context); 346 347 while (rs.next()) 348 { 349 String userPopulationId = rs.getString(3); 350 if (getUserPopulationDAO().getUserPopulation(userPopulationId) == null) 351 { 352 getLogger().warn("The population of id '{}' is linked to a context, but does not exist anymore.", userPopulationId); 353 } 354 else if (!withDisabled && !getUserPopulationDAO().getUserPopulation(userPopulationId).isEnabled()) 355 { 356 getLogger().warn("The population of id '{}' is linked to a context but disabled. It will not be returned.", userPopulationId); 357 } 358 else 359 { 360 result.add(userPopulationId); 361 } 362 } 363 } 364 catch (SQLException e) 365 { 366 getLogger().error("Error in sql query", e); 367 } 368 finally 369 { 370 ConnectionHelper.cleanup(connection); 371 ConnectionHelper.cleanup(stmt); 372 ConnectionHelper.cleanup(rs); 373 } 374 375 return result; 376 } 377 378 /** 379 * Returns true if the user population is linked to a context. 380 * @param upId The id of the user population 381 * @return True if the user population is currently linked to a context. 382 */ 383 public boolean isLinked(String upId) 384 { 385 boolean result = false; 386 387 Connection connection = null; 388 PreparedStatement stmt = null; 389 ResultSet rs = null; 390 try 391 { 392 connection = getSQLConnection(); 393 394 String sql = "SELECT UserPopulation_Id FROM " + __USER_POPULATIONS_TABLE + " WHERE UserPopulation_Id=?"; 395 stmt = connection.prepareStatement(sql); 396 stmt.setString(1, upId); 397 rs = stmt.executeQuery(); 398 getLogger().info("{}\n[{}]", sql, upId); 399 400 result = rs.next(); 401 } 402 catch (SQLException e) 403 { 404 getLogger().error("SQL error while checking if the population is linked to a context", e); 405 } 406 finally 407 { 408 ConnectionHelper.cleanup(connection); 409 ConnectionHelper.cleanup(stmt); 410 ConnectionHelper.cleanup(rs); 411 } 412 413 return result; 414 } 415}