001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.security;
029
030import org.opencms.crypto.CmsEncryptionException;
031import org.opencms.file.CmsGroup;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsUser;
034import org.opencms.i18n.CmsEncoder;
035import org.opencms.main.A_CmsAuthorizationHandler;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsHttpAuthenticationSettings;
038import org.opencms.main.OpenCms;
039import org.opencms.ui.login.CmsLoginHelper;
040import org.opencms.util.CmsMacroResolver;
041import org.opencms.util.CmsRequestUtil;
042import org.opencms.util.CmsStringUtil;
043import org.opencms.workplace.CmsWorkplaceManager;
044import org.opencms.workplace.CmsWorkplaceSettings;
045
046import java.io.IOException;
047import java.util.ArrayList;
048import java.util.Arrays;
049import java.util.List;
050import java.util.Set;
051import java.util.stream.Collectors;
052
053import javax.servlet.http.HttpServletRequest;
054import javax.servlet.http.HttpServletResponse;
055import javax.servlet.http.HttpSession;
056
057import org.apache.commons.codec.binary.Base64;
058
059import com.google.common.base.Joiner;
060
061/**
062 * Defines default authorization methods.<p>
063 *
064 * @since 6.5.4
065 */
066public class CmsDefaultAuthorizationHandler extends A_CmsAuthorizationHandler {
067
068    /** Configuration parameter to control for which paths startup settings should be applied after HTTP Basic authentication. */
069    public static final String PARAM_HTTP_BASICAUTH_USESTARTSETTINGS_PATHS = "http.basicauth.usestartsettings.paths";
070
071    /** Configuration parameter to control for which users startup settings should be applied after HTTP Basic authentication. */
072    public static final String PARAM_HTTP_BASICAUTH_USESTARTSETTINGS_USERS = "http.basicauth.usestartsettings.users";
073
074    /** Basic authorization prefix constant. */
075    public static final String AUTHORIZATION_BASIC_PREFIX = "BASIC ";
076    /** Authorization header constant. */
077    public static final String HEADER_AUTHORIZATION = "Authorization";
078
079    /** Parameter for passing the encrypted version of the requested resource. */
080    public static final String PARAM_ENCRYPTED_REQUESTED_RESOURCE = "encryptedRequestedResource";
081
082    /** Credentials separator constant. */
083    public static final String SEPARATOR_CREDENTIALS = ":";
084
085    /**
086     * Checks if a request URI path matches a given set of prefix paths.
087     *
088     * @param uri the request URI path
089     * @param pathSpec a comma separated list of path prefixes, which may contain %(contextPath) macros
090     * @return true if the URI path matches the path spec
091     */
092    protected static boolean checkPath(String uri, String pathSpec) {
093
094        if (CmsStringUtil.isEmptyOrWhitespaceOnly(pathSpec)) {
095            return false;
096        }
097        CmsMacroResolver resolver = new CmsMacroResolver();
098        pathSpec = resolver.resolveMacros(pathSpec);
099        String[] pathPatterns = pathSpec.split(",");
100        for (String pathToken : pathPatterns) {
101            if (CmsStringUtil.isPrefixPath(pathToken, uri)) {
102                return true;
103            }
104        }
105        return false;
106    }
107
108    /**
109     * Checks if the authenticated user matches a user specification string.
110     *
111     * <p>The user specification string is a comma-separed list of entries of the form TYPE.Name, where
112     * TYPE is either ROLE, GROUP, or USER. The method returns true if the user matches any of the groups, roles, or user names from this list.
113     *
114     * <p>It's also possible to configure an entry "*", which always matches.
115     *
116     * @param cms the CMS context
117     * @param userSpec the user specification
118     * @return true if the user matches any entry from the user specification
119     */
120    protected static boolean checkUser(CmsObject cms, String userSpec) {
121
122        if (CmsStringUtil.isEmptyOrWhitespaceOnly(userSpec)) {
123            return false;
124        }
125
126        Set<String> groupsOfUser = null; // lazily initialized
127        String[] entries = userSpec.split(",");
128        for (String userSpecEntry : entries) {
129            userSpecEntry = userSpecEntry.trim();
130            if ("*".equals(userSpecEntry)) {
131                return true;
132            } else if (userSpecEntry.startsWith(I_CmsPrincipal.PRINCIPAL_USER)) {
133                String userName = CmsUser.removePrefix(userSpecEntry);
134                if (cms.getRequestContext().getCurrentUser().getName().equals(userName)) {
135                    return true;
136                }
137            } else if (userSpecEntry.startsWith(CmsRole.PRINCIPAL_ROLE)) {
138                String actualRole = CmsRole.removePrefix(userSpecEntry);
139                CmsRole roleObj = null;
140                if (actualRole.contains("/")) {
141                    roleObj = CmsRole.valueOfRoleName(actualRole);
142                } else {
143                    roleObj = CmsRole.valueOfRoleName(actualRole).forOrgUnit(null);
144                }
145                if (OpenCms.getRoleManager().hasRole(cms, roleObj)) {
146                    return true;
147                }
148            } else if (userSpecEntry.startsWith(I_CmsPrincipal.PRINCIPAL_GROUP)) {
149                String groupName = CmsGroup.removePrefix(userSpecEntry);
150
151                if (groupsOfUser == null) {
152                    try {
153                        groupsOfUser = cms.getGroupsOfUser(
154                            cms.getRequestContext().getCurrentUser().getName(),
155                            false).stream().map(group -> group.getName()).collect(Collectors.toSet());
156                    } catch (Exception e) {
157                        LOG.error(e.getLocalizedMessage(), e);
158                        continue;
159                    }
160                }
161                if (groupsOfUser.contains(groupName)) {
162                    return true;
163                }
164            }
165        }
166        return false;
167    }
168
169    /**
170     * @see org.opencms.security.I_CmsAuthorizationHandler#getLoginFormURL(java.lang.String, java.lang.String, java.lang.String)
171     */
172    public String getLoginFormURL(String loginFormURL, String params, String callbackURL) {
173
174        if (loginFormURL != null) {
175
176            StringBuffer fullURL = new StringBuffer(loginFormURL);
177            if (callbackURL != null) {
178                fullURL.append("?");
179                fullURL.append(CmsWorkplaceManager.PARAM_LOGIN_REQUESTED_RESOURCE);
180                fullURL.append("=");
181                fullURL.append(callbackURL);
182            }
183            List<String> paramList;
184            if (params != null) {
185                paramList = new ArrayList<>(Arrays.asList(params.split("&")));
186            } else {
187                paramList = new ArrayList<>();
188            }
189            if (callbackURL != null) {
190                try {
191                    paramList.add(
192                        PARAM_ENCRYPTED_REQUESTED_RESOURCE
193                            + "="
194                            + OpenCms.getDefaultTextEncryption().encrypt(CmsEncoder.decode(callbackURL)));
195                } catch (CmsEncryptionException e) {
196                    LOG.error(e.getLocalizedMessage(), e);
197                }
198            }
199            fullURL.append((callbackURL != null) ? "&" : "?");
200            fullURL.append(Joiner.on("&").join(paramList));
201            return fullURL.toString();
202        }
203
204        return null;
205    }
206
207    /**
208     * @see I_CmsAuthorizationHandler#initCmsObject(HttpServletRequest)
209     */
210    public CmsObject initCmsObject(HttpServletRequest request) {
211
212        // check if "basic" authorization data is provided
213        CmsObject cms = checkBasicAuthorization(request);
214        // basic authorization successful?
215        if (cms != null) {
216            try {
217                // register the session into OpenCms and
218                // return successful logged in user
219                return registerSession(request, cms);
220            } catch (CmsException e) {
221                // ignore and threat the whole login process as failed
222            }
223        }
224        // failed
225        return null;
226    }
227
228    /**
229     * @see org.opencms.security.I_CmsAuthorizationHandler#initCmsObject(javax.servlet.http.HttpServletRequest, org.opencms.security.I_CmsAuthorizationHandler.I_PrivilegedLoginAction)
230     */
231    public CmsObject initCmsObject(
232        HttpServletRequest request,
233        I_CmsAuthorizationHandler.I_PrivilegedLoginAction loginAction) {
234
235        return initCmsObject(request);
236    }
237
238    /**
239     * @see I_CmsAuthorizationHandler#initCmsObject(HttpServletRequest, String, String)
240     */
241    public CmsObject initCmsObject(HttpServletRequest request, String userName, String pwd) throws CmsException {
242
243        // first, try to validate the session
244        CmsObject cms = initCmsObjectFromSession(request);
245        if (cms != null) {
246            return cms;
247        }
248        // try to login with the given credentials
249        cms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest());
250        // this will throw an exception if login fails
251        cms.loginUser(userName, pwd);
252        // register the session into OpenCms and
253        // return successful logged in user
254        return registerSession(request, cms);
255    }
256
257    /**
258     * This method sends a request to the client to display a login form,
259     * it is needed for HTTP-Authentication.<p>
260     *
261     * @param req the client request
262     * @param res the response
263     * @param loginFormURL the full URL used for form based authentication
264     *
265     * @throws IOException if something goes wrong
266     */
267    public void requestAuthorization(HttpServletRequest req, HttpServletResponse res, String loginFormURL)
268    throws IOException {
269
270        CmsHttpAuthenticationSettings httpAuthenticationSettings = OpenCms.getSystemInfo().getHttpAuthenticationSettings();
271
272        if (loginFormURL == null) {
273            if (httpAuthenticationSettings.useBrowserBasedHttpAuthentication()) {
274                // HTTP basic authentication is used
275                res.setHeader(
276                    CmsRequestUtil.HEADER_WWW_AUTHENTICATE,
277                    "BASIC realm=\"" + OpenCms.getSystemInfo().getServerName() + "\"");
278                res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
279                return;
280
281            } else if (httpAuthenticationSettings.getFormBasedHttpAuthenticationUri() != null) {
282                loginFormURL = httpAuthenticationSettings.getFormBasedHttpAuthenticationUri();
283            } else {
284                LOG.error(
285                    Messages.get().getBundle().key(
286                        Messages.ERR_UNSUPPORTED_AUTHENTICATION_MECHANISM_1,
287                        httpAuthenticationSettings.getBrowserBasedAuthenticationMechanism()));
288                res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
289                return;
290            }
291        }
292
293        if (LOG.isDebugEnabled()) {
294            LOG.debug(
295                Messages.get().getBundle().key(
296                    Messages.LOG_AUTHENTICATE_PROPERTY_2,
297                    loginFormURL,
298                    req.getRequestURI()));
299        }
300        // finally redirect to the login form
301        res.sendRedirect(loginFormURL);
302    }
303
304    /**
305     * Checks if the current request contains HTTP basic authentication information in
306     * the headers, if so the user is tried to log in with this data, and on success a
307     * session is generated.<p>
308     *
309     * @param req the current HTTP request
310     *
311     * @return the authenticated cms object, or <code>null</code> if failed
312     */
313    protected CmsObject checkBasicAuthorization(HttpServletRequest req) {
314
315        if (LOG.isDebugEnabled()) {
316            LOG.debug("Checking for basic authorization.");
317        }
318        try {
319            CmsObject cms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest());
320            if (OpenCms.getSystemInfo().getHttpAuthenticationSettings().getBrowserBasedAuthenticationMechanism() == null) {
321                // browser base authorization is not enabled, return Guest user CmsObject
322                if (LOG.isDebugEnabled()) {
323                    LOG.debug("Browser based authorization not enabled.");
324                }
325                return cms;
326            }
327            // no user identified from the session and basic authentication is enabled
328            String auth = req.getHeader(HEADER_AUTHORIZATION);
329            if ((auth == null) || !auth.toUpperCase().startsWith(AUTHORIZATION_BASIC_PREFIX)) {
330                // no authorization data is available
331                return cms;
332            }
333            // get encoded user and password, following after "BASIC "
334            String base64Token = auth.substring(6);
335
336            // decode it, using base 64 decoder
337            String token = new String(Base64.decodeBase64(base64Token.getBytes()));
338            String username = null;
339            String password = null;
340            int pos = token.indexOf(SEPARATOR_CREDENTIALS);
341            if (pos != -1) {
342                username = token.substring(0, pos);
343                password = token.substring(pos + 1);
344            }
345            // authentication in the DB
346            cms.loginUser(username, password);
347
348            // authorization was successful create a session
349            HttpSession session = req.getSession(true);
350            String requestUri = req.getRequestURI();
351            boolean isWorkplace = requestUri.startsWith(OpenCms.getSystemInfo().getWorkplaceContext())
352                || requestUri.startsWith(
353                    CmsStringUtil.joinPaths(OpenCms.getSystemInfo().getOpenCmsContext(), "/system/workplace"));
354            isWorkplace = isWorkplace && OpenCms.getRoleManager().hasRole(cms, CmsRole.ELEMENT_AUTHOR);
355            LOG.debug("isWorkplace = " + isWorkplace);
356            boolean initStartSettings = isWorkplace || shouldUseStartSettingsForHttpBasicAuth(cms, req);
357            LOG.debug("initStartSettings = " + initStartSettings);
358            OpenCms.getSiteManager().isWorkplaceRequest(req);
359            if (initStartSettings) {
360                CmsWorkplaceSettings settings = CmsLoginHelper.initSiteAndProject(cms);
361                session.setAttribute(CmsWorkplaceManager.SESSION_WORKPLACE_SETTINGS, settings);
362            }
363
364            return cms;
365        } catch (CmsException e) {
366            // authorization failed
367            return null;
368        }
369    }
370
371    /**
372     * Checks whether start settings should be used after HTTP Basic authentication.
373     *
374     * <p>This method will not be called for workplace requests; for these the start settings will always be used.
375     *
376     * @param cms the CMS context initialized with the user from the HTTP Basic authentication
377     * @param req the current request
378     *
379     * @return true if the start settings should be used
380     */
381    protected boolean shouldUseStartSettingsForHttpBasicAuth(CmsObject cms, HttpServletRequest req) {
382
383        String userSpec = m_parameters.get(PARAM_HTTP_BASICAUTH_USESTARTSETTINGS_USERS);
384        String pathSpec = m_parameters.get(PARAM_HTTP_BASICAUTH_USESTARTSETTINGS_PATHS);
385
386        if (!checkPath(req.getRequestURI(), pathSpec)) {
387            LOG.debug("checkPath returned false for " + req.getRequestURI() + ", pathSpec=" + pathSpec);
388            return false;
389        }
390        if (!checkUser(cms, userSpec)) {
391            LOG.debug(
392                "checkUser returned false for "
393                    + cms.getRequestContext().getCurrentUser().getName()
394                    + ", userSpec = "
395                    + userSpec);
396            return false;
397        }
398
399        return true;
400    }
401}