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