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}