001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://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 org.apache.commons.logging.Log; 043 044import org.dom4j.Document; 045import org.dom4j.DocumentException; 046import org.dom4j.Element; 047import org.dom4j.io.SAXReader; 048 049import jakarta.servlet.http.HttpServletResponse; 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 public static CmsJsonAccessPolicy parse(InputStream stream) throws DocumentException { 156 157 SAXReader reader = new SAXReader(); 158 Document document = reader.read(stream); 159 Element root = document.getRootElement(); 160 Element groupElem = root.element("group"); 161 String groupName = null; 162 if (groupElem != null) { 163 groupName = groupElem.getTextTrim(); 164 } 165 Element propertyFilterElem = root.element("property-filter"); 166 String propertyFilterRegex = null; 167 if (propertyFilterElem != null) { 168 propertyFilterRegex = propertyFilterElem.getTextTrim(); 169 } 170 List<String> includes = root.elements("include").stream().map(elem -> elem.getTextTrim()).collect( 171 Collectors.toList()); 172 List<String> excludes = root.elements("exclude").stream().map(elem -> elem.getTextTrim()).collect( 173 Collectors.toList()); 174 Element elementCors = root.element("cors"); 175 String corsAllowOrigin = DEFAULT_CORS_FILTER; 176 String corsAllowMethods = DEFAULT_CORS_FILTER; 177 String corsAllowHeaders = DEFAULT_CORS_FILTER; 178 if (elementCors != null) { 179 corsAllowOrigin = elementCors.elementTextTrim("allow-origin"); 180 corsAllowMethods = elementCors.elementTextTrim("allow-methods"); 181 corsAllowHeaders = elementCors.elementTextTrim("allow-headers"); 182 } 183 return new CmsJsonAccessPolicy( 184 groupName, 185 includes, 186 excludes, 187 propertyFilterRegex, 188 corsAllowOrigin, 189 corsAllowMethods, 190 corsAllowHeaders); 191 } 192 193 /** 194 * Checks if a JSON handler request is allowed for this policy. 195 * 196 * @param cms the CMS context 197 * @param path the path 198 * 199 * @return true if the request is allowed 200 */ 201 public boolean checkAccess(CmsObject cms, String path) { 202 203 if (m_overrideValue != null) { 204 return m_overrideValue.booleanValue(); 205 } 206 if (m_accessGroup != null) { 207 try { 208 List<CmsGroup> groups = cms.getGroupsOfUser( 209 cms.getRequestContext().getCurrentUser().getName(), 210 true, 211 true); 212 boolean foundGroup = groups.stream().anyMatch(group -> group.getName().equals(m_accessGroup)); 213 if (!foundGroup) { 214 return false; 215 } 216 } catch (CmsException e) { 217 LOG.error(e.getLocalizedMessage(), e); 218 return false; 219 } 220 } 221 222 boolean included = (m_include.isEmpty() 223 || m_include.stream().anyMatch(include -> include.matcher(path).matches())) 224 && !m_exclude.stream().anyMatch(exclude -> exclude.matcher(path).matches()); 225 return included; 226 227 } 228 229 /** 230 * Checks if the property can be accessed (i.e. is not filtered out by property filter). 231 * 232 * @param property the property name to check 233 * @return true if the property can be written to JSON 234 */ 235 public boolean checkPropertyAccess(String property) { 236 237 boolean result = (m_propertyFilter == null) || m_propertyFilter.matcher(property).matches(); 238 if (!result) { 239 LOG.info("Filtered property " + property + " because it does not match the JSON property filter."); 240 } 241 return result; 242 } 243 244 /** 245 * Sets the configured CORS headers for a given HTTP servlet response.<p> 246 * 247 * @param response the given HTTP servlet response 248 */ 249 public void setCorsHeaders(HttpServletResponse response) { 250 251 if (m_corsAllowOrigin != null) { 252 response.setHeader("Access-Control-Allow-Origin", m_corsAllowOrigin); 253 } 254 if (m_corsAllowMethods != null) { 255 response.setHeader("Access-Control-Allow-Methods", m_corsAllowMethods); 256 } 257 if (m_corsAllowHeaders != null) { 258 response.setHeader("Access-Control-Allow-Headers", m_corsAllowHeaders); 259 } 260 } 261 262}