001/*
002 *  Copyright 2020 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.mobileapp.action;
017
018import java.io.InputStream;
019import java.nio.charset.StandardCharsets;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import javax.servlet.http.HttpServletRequest;
026
027import org.apache.avalon.framework.parameters.ParameterException;
028import org.apache.avalon.framework.parameters.Parameters;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.thread.ThreadSafe;
032import org.apache.cocoon.acting.ServiceableAction;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Redirector;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.SourceResolver;
037import org.apache.cocoon.environment.http.HttpEnvironment;
038
039import org.ametys.core.authentication.CredentialProvider;
040import org.ametys.core.authentication.token.AuthenticationTokenManager;
041import org.ametys.core.cocoon.JSonReader;
042import org.ametys.core.user.CurrentUserProvider;
043import org.ametys.core.user.UserIdentity;
044import org.ametys.core.user.population.PopulationContextHelper;
045import org.ametys.core.user.population.UserPopulation;
046import org.ametys.core.user.population.UserPopulationDAO;
047import org.ametys.core.util.JSONUtils;
048import org.ametys.runtime.authentication.AccessDeniedException;
049
050/**
051 * Returns the token for a user (only to be used by the mobile app on one site)
052 */
053public abstract class AbstractGetTokenAction extends ServiceableAction implements ThreadSafe
054{
055    /** Authentication Token Manager */
056    protected AuthenticationTokenManager _authenticationTokenManager;
057
058    /** The user population DAO */
059    protected UserPopulationDAO _userPopulationDAO;
060
061    /** The helper for the associations population/context */
062    protected PopulationContextHelper _populationContextHelper;
063
064    /** The current user provider */
065    protected CurrentUserProvider _currentUserProvider;
066
067    /** JSON Utils */
068    protected JSONUtils _jsonUtils;
069
070    @Override
071    public void service(ServiceManager smanager) throws ServiceException
072    {
073        super.service(smanager);
074        _authenticationTokenManager = (AuthenticationTokenManager) smanager.lookup(AuthenticationTokenManager.ROLE);
075
076        _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE);
077        _populationContextHelper = (PopulationContextHelper) smanager.lookup(PopulationContextHelper.ROLE);
078        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
079
080        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
081    }
082
083    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
084    {
085        Map<String, Object> result = new HashMap<>();
086        Request request = ObjectModelHelper.getRequest(objectModel);
087
088        String body = null;
089        HttpServletRequest postReq = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT);
090        try (InputStream postBody = postReq.getInputStream())
091        {
092            body = new String(postBody.readAllBytes(), StandardCharsets.UTF_8);
093        }
094
095        String siteName = parameters.getParameter("site");
096        List<String> populationContexts = List.of("/sites/" + siteName, "/sites-fo/" + siteName);
097
098        Map<String, Object> jsonParams = _jsonUtils.convertJsonToMap(body);
099
100        String population = (String) getParameter("population", jsonParams, request);
101
102        UserIdentity user = null;
103        UserPopulation userPopulation = population != null ? _userPopulationDAO.getUserPopulation(population) : null;
104
105        if (userPopulation != null)
106        {
107            user = authenticate(jsonParams, request, parameters, populationContexts, userPopulation);
108        }
109        else
110        {
111            user = authenticate(jsonParams, request, parameters, populationContexts);
112        }
113
114        if (user != null)
115        {
116            String generateToken = _authenticationTokenManager.generateToken(user, 0, "mobileapp", "Token for the mobile app");
117
118            result.put("code", 200);
119            result.put("token", generateToken);
120            
121            // If the authentication process has lead to an effective session creation, it should be destroyed
122            // Do not provide the redirector. We don't want the request to be redirected
123            _currentUserProvider.logout(null);
124        }
125        else
126        {
127            result.put("code", 403);
128            throw new AccessDeniedException();
129        }
130
131        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
132        return EMPTY_MAP;
133    }
134    
135    /**
136     * Get a parameter from the mobile app, either from the request body or a request parameter
137     * @param name the parameter name
138     * @param jsonParams the decoded JSON request body, if any
139     * @param request the request
140     * @return the parameter value, or null if not found
141     */
142    protected Object getParameter(String name, Map<String, Object> jsonParams, Request request)
143    {
144        if (jsonParams.containsKey(name))
145        {
146            return jsonParams.get(name);
147        }
148        else if (request.getParameter(name) != null)
149        {
150            return request.getParameter(name);
151        }
152        else
153        {
154            return request.getAttribute(name);
155        }
156    }
157
158    private UserIdentity authenticate(Map<String, Object> params, Request request, Parameters parameters, List<String> populationContexts) throws ParameterException
159    {
160        String siteName = parameters.getParameter("site");
161
162        for (String context : populationContexts)
163        {
164            List<UserPopulation> populations = _populationContextHelper.getUserPopulationsOnContexts(List.of(context), false, false).stream()
165                    .map(_userPopulationDAO::getUserPopulation)
166                    .toList();
167
168            for (UserPopulation population : populations)
169            {
170                UserIdentity user = _tryConnect(params, request, context, population);
171                if (user != null)
172                {
173                    return user;
174                }
175            }
176        }
177
178        getLogger().error("Unable to log in from the mobile application to the '" + siteName + "' site. At least one population should be configured with credential provider compatible with the mobile app.");
179        return null;
180    }
181
182    private UserIdentity authenticate(Map<String, Object> params, Request request, Parameters parameters, List<String> populationContexts, UserPopulation userPopulation) throws ParameterException
183    {
184        String siteName = parameters.getParameter("site");
185
186        Set<String> contextsForUserPopulation = _populationContextHelper.getContextsForUserPopulation(userPopulation.getId());
187        
188        for (String context : populationContexts)
189        {
190            if (contextsForUserPopulation.contains(context))
191            {
192                UserIdentity user = _tryConnect(params, request, context, userPopulation);
193                if (user != null)
194                {
195                    return user;
196                }
197            }
198        }
199
200        getLogger().error("Unable to log in from the mobile application to the '" + siteName + "' site, with population '" + userPopulation.getId() + "'.");
201        return null;
202    }
203    
204    private UserIdentity _tryConnect(Map<String, Object> params, Request request, String context, UserPopulation userPopulation)
205    {
206        List<CredentialProvider> credentialProviders = userPopulation.getCredentialProviders();
207        
208        for (int i = 0; i < credentialProviders.size(); i++)
209        {
210            CredentialProvider credentialProvider = credentialProviders.get(i);
211            UserIdentity user = tryConnect(params, request, context, userPopulation, credentialProvider, i);
212            if (user != null)
213            {
214                return user;
215            }
216        }
217        
218        return null;
219    }
220
221    /**
222     * Try to authenticate the current mobile user
223     * @param params the parameters issued by the mobile app
224     * @param request the authentication request
225     * @param context the population context
226     * @param userPopulation the population
227     * @param credentialProvider the {@link CredentialProvider}
228     * @param credentialProviderIndex the index of the credentialProvider in the given userPopulation
229     * @return the authenticated user, if any
230     */
231    protected abstract UserIdentity tryConnect(Map<String, Object> params, Request request, String context, UserPopulation userPopulation, CredentialProvider credentialProvider, int credentialProviderIndex);
232}