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