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.site;
017
018import java.io.InputStream;
019import java.nio.charset.StandardCharsets;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Enumeration;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Set;
030import java.util.regex.Pattern;
031
032import org.apache.avalon.framework.configuration.Configuration;
033import org.apache.avalon.framework.configuration.ConfigurationException;
034import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
035import org.apache.avalon.framework.parameters.Parameters;
036import org.apache.cocoon.environment.ObjectModelHelper;
037import org.apache.cocoon.environment.Redirector;
038import org.apache.cocoon.environment.Request;
039import org.apache.cocoon.environment.Session;
040import org.apache.cocoon.environment.http.HttpCookie;
041import org.apache.commons.lang3.StringUtils;
042import org.apache.commons.lang3.Strings;
043import org.apache.hc.client5.http.classic.methods.HttpGet;
044import org.apache.hc.client5.http.classic.methods.HttpPost;
045import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
046import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
047import org.apache.hc.core5.http.HttpEntity;
048import org.apache.hc.core5.http.HttpResponse;
049import org.apache.hc.core5.http.NameValuePair;
050import org.apache.hc.core5.http.message.BasicNameValuePair;
051import org.xml.sax.SAXException;
052
053import org.ametys.core.ObservationConstants;
054import org.ametys.core.authentication.AuthenticateAction;
055import org.ametys.core.authentication.CredentialProvider;
056import org.ametys.core.observation.Event;
057import org.ametys.core.user.UserIdentity;
058import org.ametys.core.user.population.UserPopulationDAO;
059import org.ametys.core.util.LambdaUtils.LambdaException;
060import org.ametys.plugins.core.user.UserDAO;
061import org.ametys.plugins.site.Site;
062import org.ametys.runtime.config.Config;
063import org.ametys.runtime.workspace.WorkspaceMatcher;
064
065/**
066 * The authenticate action for front side
067 */
068public class FrontAuthenticateAction extends AuthenticateAction
069{
070    /** url requires for authentication */
071    protected Collection<Pattern> _acceptedSiteUrlPatterns = Arrays.asList(new Pattern[]{Pattern.compile("^plugins/site/authenticate/[0-9]+$")});
072    
073    @Override
074    protected boolean _acceptedUrl(Request request)
075    {
076        // URL without server context and leading slash.
077        String url = (String) request.getAttribute(WorkspaceMatcher.IN_WORKSPACE_URL);
078        for (Pattern pattern : _acceptedSiteUrlPatterns)
079        {
080            if (pattern.matcher(url).matches())
081            {
082                // Anonymous request
083                request.setAttribute(REQUEST_ATTRIBUTE_GRANTED, true);
084
085                return true;
086            }
087        }
088        
089        return false;
090    }
091
092    @Override
093    protected void _setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
094    {
095        // Method overridden to call the correct static method, the default one would be the one from the parent class
096        setUserIdentityInSession(request, userIdentity, credentialProvider, blockingMode);
097        if (_observationManager != null)
098        {
099            Map<String, Object> eventParams = new HashMap<>();
100            eventParams.put(ObservationConstants.ARGS_USER, userIdentity);
101            _observationManager.notify(new Event(ObservationConstants.EVENT_USER_AUTHENTICATED, UserPopulationDAO.SYSTEM_USER_IDENTITY, eventParams));
102        }
103    }
104    
105    /**
106     * Save user identity in request
107     * @param request The request
108     * @param userIdentity The useridentity to save
109     * @param credentialProvider The credential provider used to connect
110     * @param blockingMode The mode used for the credential provider
111     */
112    public static void setUserIdentityInSession(Request request, UserIdentity userIdentity, CredentialProvider credentialProvider, boolean blockingMode)
113    {
114        Site site = (Site) request.getAttribute("site");
115        String siteName = site.getName();
116        
117        Session session = renewSession(request);
118        _resetConnectingStateToSession(request);
119        session.setAttribute(SESSION_USERIDENTITY + "-" + siteName, userIdentity);
120        session.setAttribute(SESSION_CREDENTIALPROVIDER + "-" + siteName, credentialProvider);
121        session.setAttribute(SESSION_CREDENTIALPROVIDER_MODE + "-" + siteName, blockingMode);
122    }
123    
124    @Override
125    protected UserIdentity _getUserIdentityFromSession(Request request)
126    {
127        UserIdentity userIdentityFromSession = getUserIdentityFromSession(request);
128        if (userIdentityFromSession != null)
129        {
130            return userIdentityFromSession;
131        }
132
133        Session session = request.getSession(false);
134        if (session != null)
135        {
136            Set<String> availableUserPopulationsIds = _getAvailableUserPopulationsIds(request, _getContexts(request, null));
137
138            // check if connected in the site application, not only on this site
139            userIdentityFromSession = super._getUserIdentityFromSession(request);
140            if (userIdentityFromSession != null && availableUserPopulationsIds.contains(userIdentityFromSession.getPopulationId()))
141            {
142                _setUserIdentityInSession(request, userIdentityFromSession, new UserDAO.ImpersonateCredentialProvider(), true);
143                return userIdentityFromSession;
144            }
145             
146            Enumeration<String> attributeNames = session.getAttributeNames();
147            while (attributeNames.hasMoreElements())
148            {
149                String attributeName = attributeNames.nextElement();
150                if (attributeName.startsWith(SESSION_USERIDENTITY + "-"))
151                {
152                    UserIdentity userIdentity = (UserIdentity) session.getAttribute(attributeName);
153                    
154                    if (availableUserPopulationsIds.contains(userIdentity.getPopulationId()))
155                    {
156                        _setUserIdentityInSession(request, userIdentity, new UserDAO.ImpersonateCredentialProvider(), true);
157                        return userIdentity;
158                    }
159                }
160            }
161        }
162
163        return null;
164    }
165    
166    @Override
167    protected Optional<String> _getWeakPasswordURI(Request request, UserIdentity userIdentity)
168    {
169        Site site = (Site) request.getAttribute("site");
170        
171        String defaultLanguage = "en";
172        Locale local = request.getLocale();
173        
174        String prefLanguage = local != null ? local.getLanguage() : defaultLanguage;
175
176        String weakPasswordUrl = site.getWeakPasswordUrl(prefLanguage);
177        if (weakPasswordUrl == null)
178        {
179            // try to get weak password from local language
180            weakPasswordUrl = site.getWeakPasswordUrl(prefLanguage);
181        }
182        
183        if (weakPasswordUrl == null)
184        {
185            // try to get weak password from default language
186            weakPasswordUrl = site.getWeakPasswordUrl(defaultLanguage);
187        }
188        
189        if (weakPasswordUrl == null)
190        {
191            for (String lang : site.getLanguages())
192            {
193                weakPasswordUrl = site.getWeakPasswordUrl(lang);
194                if (weakPasswordUrl != null)
195                {
196                    break;
197                }
198            }
199        }
200        
201        return Optional.ofNullable(weakPasswordUrl + "&userIdentity=" + UserIdentity.userIdentityToString(userIdentity));
202    }
203    
204    /**
205     * Get the user identity of the connected user from the session
206     * @param request The request
207     * @return The connected useridentity or null
208     */
209    public static UserIdentity getUserIdentityFromSession(Request request)
210    {
211        Site site = (Site) request.getAttribute("site");
212        String siteName = site.getName();
213        
214        return getUserIdentityFromSession(request, siteName);
215    }
216    
217    /**
218     * Get the user identity of the connected user from the session
219     * @param request The request
220     * @param siteName The current site name
221     * @return The connected useridentity or null
222     */
223    public static UserIdentity getUserIdentityFromSession(Request request, String siteName)
224    {
225        Session session = request.getSession(false);
226        if (session != null)
227        {
228            return (UserIdentity) session.getAttribute(SESSION_USERIDENTITY + "-" + siteName);
229        }
230        return null;
231    }
232    
233    @Override
234    protected CredentialProvider _getCredentialProviderFromSession(Request request)
235    {
236        return getCredentialProviderFromSession(request);
237    }
238    
239    /**
240     * Get the credential provider used for the current connection
241     * @param request The request
242     * @return The credential provider used or null
243     */
244    public static CredentialProvider getCredentialProviderFromSession(Request request)
245    {
246        Site site = (Site) request.getAttribute("site");
247        
248        if (site == null)
249        {
250            return null;
251        }
252        
253        String siteName = site.getName();
254        
255        return getCredentialProviderFromSession(request, siteName);
256    }
257    
258    /**
259     * Get the credential provider used for the current connection
260     * @param request The request
261     * @param siteName The current site name
262     * @return The credential provider used or null
263     */
264    public static CredentialProvider getCredentialProviderFromSession(Request request, String siteName)
265    {
266        Session session = request.getSession(false);
267        if (session != null)
268        {
269            return (CredentialProvider) session.getAttribute(SESSION_CREDENTIALPROVIDER + "-" + siteName);
270        }
271        return null;
272    }
273    
274    @Override
275    protected Boolean _getCredentialProviderModeFromSession(Request request)
276    {
277        return getCredentialProviderModeFromSession(request);
278    }
279    
280    /**
281     * Get the credential provider mode used for the current connection
282     * @param request The request
283     * @return The credential provider mode used or null
284     */
285    public static Boolean getCredentialProviderModeFromSession(Request request)
286    {
287        Site site = (Site) request.getAttribute("site");
288        String siteName = site.getName();
289        
290        return getCredentialProviderModeFromSession(request, siteName);
291    }
292    
293    /**
294     * Get the credential provider mode used for the current connection
295     * @param request The request
296     * @param siteName The current site name
297     * @return The credential provider mode used or null
298     */
299    public static Boolean getCredentialProviderModeFromSession(Request request, String siteName)
300    {
301        Session session = request.getSession(false);
302        if (session != null)
303        {
304            return (Boolean) session.getAttribute(SESSION_CREDENTIALPROVIDER_MODE + "-" + siteName);
305        }
306        return null;
307    }
308    
309    @Override
310    protected List<String> _getContexts(Request request, Parameters parameters)
311    {
312        Site site = (Site) request.getAttribute("site");
313        String siteName = site.getName();
314        return Arrays.asList("/sites/" + siteName, "/sites-fo/" + siteName);
315    }
316
317    @Override
318    protected String getLoginURL(Request request)
319    {
320        Site site = (Site) request.getAttribute("site");
321        String siteName = site.getName();
322        
323        return getLoginURLParameters(request, "cocoon://_generate/plugins/web/frontoffice-formbasedauthentication/login/login/" + siteName);
324    }
325    
326    @Override
327    protected String getLogoutURL(Request request)
328    {
329        Site site = (Site) request.getAttribute("site");
330        String siteName = site.getName();
331        return "cocoon://_generate/plugins/web/frontoffice-formbasedauthentication/login/logout/" + siteName;
332    }
333    
334    @Override
335    protected boolean _handleLogout(Redirector redirector, Map objectModel, String source, Parameters parameters) throws Exception
336    {
337        boolean logout = super._handleLogout(redirector, objectModel, source, parameters);
338        if (logout)
339        {
340            // Additionally we need to destroy potential server side session
341            Request request = ObjectModelHelper.getRequest(objectModel);
342            HttpCookie sessionId = (HttpCookie) request.getCookieMap().get(GeneratePageAction.__BACKOFFICE_JSESSION_ID);
343            if (sessionId != null)
344            {
345                // FIXME CMS-12715 Reuse the same client for all request from site to cms
346                try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient())
347                {
348                    String cmsURL = Config.getInstance().getValue("org.ametys.site.bo");
349                    HttpGet httpGet = new HttpGet(cmsURL + "/logout.html");
350                    httpGet.addHeader("Cookie", "JSESSIONID=" + sessionId.getValue());
351                    int status = httpClient.execute(httpGet, HttpResponse::getCode);
352                    if (status != 200)
353                    {
354                        getLogger().warn("BO responded to request to logout with status :" + status);
355                    }
356                }
357            }
358        }
359        return logout;
360    }
361    
362    @Override
363    protected UserIdentity _validateToken(String token, String context)
364    {
365        String tokenContext = context;
366        if (StringUtils.isBlank(tokenContext)
367            || Strings.CS.equals(tokenContext, "application"))
368        {
369            tokenContext = "sites";
370        }
371        
372        // Get site names and URLs from the CMS
373        String cmsURL = Config.getInstance().getValue("org.ametys.site.bo");
374        
375        // FIXME CMS-12715 Reuse the same client for all request from site to cms
376        try (CloseableHttpClient httpClient = BackOfficeRequestHelper.getHttpClient())
377        {
378            HttpPost httpPost = new HttpPost(cmsURL + "/_validate_token.xml");
379            httpPost.addHeader("X-Ametys-FO", "true");
380            
381            List<NameValuePair> nvps = new ArrayList<>();
382            nvps.add(new BasicNameValuePair("token", token));
383            nvps.add(new BasicNameValuePair("tokenContext", tokenContext));
384            httpPost.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
385            
386            return httpClient.execute(httpPost, response -> {
387                
388                switch (response.getCode())
389                {
390                    case 200:
391                        break;
392                        
393                    case 403:
394                        throw new IllegalStateException("The CMS back-office refused the connection");
395                        
396                    case 500:
397                    default:
398                        throw new IllegalStateException("The CMS back-office returned an error");
399                }
400                
401                try (HttpEntity entity = response.getEntity();
402                        InputStream is = entity.getContent())
403                {
404                    Configuration conf = new DefaultConfigurationBuilder().build(is);
405                    
406                    String login = conf.getChild("login").getValue(null);
407                    String populationId = conf.getChild("populationId").getValue(null);
408                    
409                    if (StringUtils.isNoneBlank(login, populationId))
410                    {
411                        return new UserIdentity(login, populationId);
412                    }
413                    else
414                    {
415                        return null;
416                    }
417                }
418                catch (ConfigurationException | SAXException e)
419                {
420                    throw new LambdaException(e);
421                }
422            });
423        }
424        catch (Exception e)
425        {
426            if (e instanceof LambdaException lambda)
427            {
428                e = (Exception) lambda.getCause(); // we can cast because we know that we only wrapped exception
429            }
430            throw new RuntimeException("Unable to synchronize site data", e);
431        }
432    }
433}