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, 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.xml.xml2json; 029 030import org.opencms.file.CmsGroup; 031import org.opencms.file.CmsObject; 032import org.opencms.main.CmsException; 033import org.opencms.main.CmsLog; 034 035import java.io.ByteArrayInputStream; 036import java.io.IOException; 037import java.io.InputStream; 038import java.util.List; 039import java.util.regex.Pattern; 040import java.util.stream.Collectors; 041 042import javax.servlet.http.HttpServletResponse; 043 044import org.apache.commons.logging.Log; 045 046import org.dom4j.Document; 047import org.dom4j.DocumentException; 048import org.dom4j.Element; 049import org.dom4j.io.SAXReader; 050 051/** 052 * Contains configuration for access restrictions to JSON handler. 053 */ 054public class CmsJsonAccessPolicy { 055 056 /** Default property filter: Property name must not contain secret, api, password or key. */ 057 public static final Pattern DEFAULT_PROP_FILTER = Pattern.compile("(?i)^(?!.*(?:secret|api|password|key)).*$"); 058 059 /** Default CORS filter. */ 060 public static final String DEFAULT_CORS_FILTER = "*"; 061 062 /** Logger instance for this class. */ 063 private static final Log LOG = CmsLog.getLog(CmsJsonAccessPolicy.class); 064 065 /** Group which should be allowed access. */ 066 private String m_accessGroup; 067 068 /** Exclude path patterns. */ 069 private List<Pattern> m_exclude; 070 071 /** HTTP response header Access-Control-Allow-Origin */ 072 private String m_corsAllowOrigin = DEFAULT_CORS_FILTER; 073 074 /** HTTP response header Access-Control-Allow-Methods */ 075 private String m_corsAllowMethods = DEFAULT_CORS_FILTER; 076 077 /** HTTP response header Access-Control-Allow-Headers */ 078 private String m_corsAllowHeaders = DEFAULT_CORS_FILTER; 079 080 /** Include path patterns. */ 081 private List<Pattern> m_include; 082 083 /** If this is set to a non-null value, that value will always be returned from checkAccess. */ 084 private Boolean m_overrideValue; 085 086 /** The property filter regex - only properties with names it matches are written to JSON .*/ 087 private Pattern m_propertyFilter = DEFAULT_PROP_FILTER; 088 089 /** 090 * Creates new access policy with a fixed return value for checkAccess. 091 * 092 * @param enabled true if allowed, false if forbidden 093 */ 094 public CmsJsonAccessPolicy(boolean enabled) { 095 096 m_overrideValue = Boolean.valueOf(enabled); 097 } 098 099 /** 100 * Creates a new instance. 101 * 102 * @param accessGroup the access group (may be null) 103 * @param includePatterns the include regexes 104 * @param excludePatterns the exclude regexes 105 * @param propertyFilterRegex the regular expression to filter property names with 106 * @param corsAllowOrigin the HTTP response header Access-Control-Allow-Origin 107 * @param corsAllowMethods the HTTP response header Access-Control-Allow-Methods 108 * @param corsAllowHeaders the HTTP response header Access-Control-Allow-Headers 109 */ 110 public CmsJsonAccessPolicy( 111 String accessGroup, 112 List<String> includePatterns, 113 List<String> excludePatterns, 114 String propertyFilterRegex, 115 String corsAllowOrigin, 116 String corsAllowMethods, 117 String corsAllowHeaders) { 118 119 m_accessGroup = accessGroup; 120 m_include = includePatterns.stream().map(Pattern::compile).collect(Collectors.toList()); 121 m_exclude = excludePatterns.stream().map(Pattern::compile).collect(Collectors.toList()); 122 if (propertyFilterRegex != null) { 123 m_propertyFilter = Pattern.compile(propertyFilterRegex); 124 } 125 m_corsAllowOrigin = corsAllowOrigin; 126 m_corsAllowMethods = corsAllowMethods; 127 m_corsAllowHeaders = corsAllowHeaders; 128 } 129 130 /** 131 * Parses an JSON handler access policy file. 132 * 133 * @param data the data 134 * @return the access policy 135 * 136 * @throws DocumentException if parsing fails 137 */ 138 public static CmsJsonAccessPolicy parse(byte[] data) throws DocumentException { 139 140 try (ByteArrayInputStream stream = new ByteArrayInputStream(data)) { 141 return parse(stream); 142 } catch (IOException e) { 143 return null; 144 } 145 } 146 147 /** 148 * Parses an JSON handler access policy file. 149 * 150 * @param stream the XML data stream 151 * @return the access policy 152 * 153 * @throws DocumentException if parsing fails 154 155 */ 156 public static CmsJsonAccessPolicy parse(InputStream stream) throws DocumentException { 157 158 SAXReader reader = new SAXReader(); 159 Document document = reader.read(stream); 160 Element root = document.getRootElement(); 161 Element groupElem = root.element("group"); 162 String groupName = null; 163 if (groupElem != null) { 164 groupName = groupElem.getTextTrim(); 165 } 166 Element propertyFilterElem = root.element("property-filter"); 167 String propertyFilterRegex = null; 168 if (propertyFilterElem != null) { 169 propertyFilterRegex = propertyFilterElem.getTextTrim(); 170 } 171 List<String> includes = root.elements("include").stream().map(elem -> elem.getTextTrim()).collect( 172 Collectors.toList()); 173 List<String> excludes = root.elements("exclude").stream().map(elem -> elem.getTextTrim()).collect( 174 Collectors.toList()); 175 Element elementCors = root.element("cors"); 176 String corsAllowOrigin = DEFAULT_CORS_FILTER; 177 String corsAllowMethods = DEFAULT_CORS_FILTER; 178 String corsAllowHeaders = DEFAULT_CORS_FILTER; 179 if (elementCors != null) { 180 corsAllowOrigin = elementCors.elementTextTrim("allow-origin"); 181 corsAllowMethods = elementCors.elementTextTrim("allow-methods"); 182 corsAllowHeaders = elementCors.elementTextTrim("allow-headers"); 183 } 184 return new CmsJsonAccessPolicy( 185 groupName, 186 includes, 187 excludes, 188 propertyFilterRegex, 189 corsAllowOrigin, 190 corsAllowMethods, 191 corsAllowHeaders); 192 } 193 194 /** 195 * Checks if a JSON handler request is allowed for this policy. 196 * 197 * @param cms the CMS context 198 * @param path the path 199 * 200 * @return true if the request is allowed 201 */ 202 public boolean checkAccess(CmsObject cms, String path) { 203 204 if (m_overrideValue != null) { 205 return m_overrideValue.booleanValue(); 206 } 207 if (m_accessGroup != null) { 208 try { 209 List<CmsGroup> groups = cms.getGroupsOfUser( 210 cms.getRequestContext().getCurrentUser().getName(), 211 true, 212 true); 213 boolean foundGroup = groups.stream().anyMatch(group -> group.getName().equals(m_accessGroup)); 214 if (!foundGroup) { 215 return false; 216 } 217 } catch (CmsException e) { 218 LOG.error(e.getLocalizedMessage(), e); 219 return false; 220 } 221 } 222 223 boolean included = (m_include.isEmpty() 224 || m_include.stream().anyMatch(include -> include.matcher(path).matches())) 225 && !m_exclude.stream().anyMatch(exclude -> exclude.matcher(path).matches()); 226 return included; 227 228 } 229 230 /** 231 * Checks if the property can be accessed (i.e. is not filtered out by property filter). 232 * 233 * @param property the property name to check 234 * @return true if the property can be written to JSON 235 */ 236 public boolean checkPropertyAccess(String property) { 237 238 boolean result = (m_propertyFilter == null) || m_propertyFilter.matcher(property).matches(); 239 if (!result) { 240 LOG.info("Filtered property " + property + " because it does not match the JSON property filter."); 241 } 242 return result; 243 } 244 245 /** 246 * Sets the configured CORS headers for a given HTTP servlet response.<p> 247 * 248 * @param response the given HTTP servlet response 249 */ 250 public void setCorsHeaders(HttpServletResponse response) { 251 252 if (m_corsAllowOrigin != null) { 253 response.setHeader("Access-Control-Allow-Origin", m_corsAllowOrigin); 254 } 255 if (m_corsAllowMethods != null) { 256 response.setHeader("Access-Control-Allow-Methods", m_corsAllowMethods); 257 } 258 if (m_corsAllowHeaders != null) { 259 response.setHeader("Access-Control-Allow-Headers", m_corsAllowHeaders); 260 } 261 } 262 263}