001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.fcrepo.auth.webac; 020 021import static java.nio.charset.StandardCharsets.UTF_8; 022import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; 023import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate; 024import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_ADMIN_ROLE; 025import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_USER_ROLE; 026import static org.fcrepo.auth.webac.URIConstants.FOAF_AGENT_VALUE; 027import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_APPEND; 028import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_CONTROL; 029import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_READ; 030import static org.fcrepo.auth.webac.URIConstants.WEBAC_MODE_WRITE; 031import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; 032import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_BINARY; 033import static org.slf4j.LoggerFactory.getLogger; 034 035import java.io.IOException; 036import java.net.URI; 037import java.security.Principal; 038 039import javax.inject.Inject; 040import javax.servlet.Filter; 041import javax.servlet.FilterChain; 042import javax.servlet.FilterConfig; 043import javax.servlet.ServletException; 044import javax.servlet.ServletRequest; 045import javax.servlet.ServletResponse; 046import javax.servlet.http.HttpServletRequest; 047import javax.servlet.http.HttpServletResponse; 048import javax.ws.rs.core.MediaType; 049import javax.ws.rs.core.UriBuilder; 050 051import org.apache.commons.io.IOUtils; 052import org.apache.jena.query.QueryParseException; 053import org.apache.jena.rdf.model.ModelFactory; 054import org.apache.jena.rdf.model.Resource; 055import org.apache.jena.sparql.modify.request.UpdateDataDelete; 056import org.apache.jena.sparql.modify.request.UpdateModify; 057import org.apache.jena.update.UpdateFactory; 058import org.apache.jena.update.UpdateRequest; 059import org.apache.shiro.SecurityUtils; 060import org.apache.shiro.subject.PrincipalCollection; 061import org.apache.shiro.subject.SimplePrincipalCollection; 062import org.apache.shiro.subject.Subject; 063import org.fcrepo.http.api.FedoraLdp; 064import org.fcrepo.http.commons.api.rdf.HttpResourceConverter; 065import org.fcrepo.http.commons.session.HttpSession; 066import org.fcrepo.http.commons.session.SessionFactory; 067import org.fcrepo.kernel.api.FedoraSession; 068import org.fcrepo.kernel.api.models.FedoraResource; 069import org.fcrepo.kernel.api.services.NodeService; 070import org.slf4j.Logger; 071 072/** 073 * @author peichman 074 */ 075public class WebACFilter implements Filter { 076 077 private static final Logger log = getLogger(WebACFilter.class); 078 079 private static final MediaType sparqlUpdate = MediaType.valueOf(contentTypeSPARQLUpdate); 080 081 private FedoraSession session; 082 083 private static final Principal FOAF_AGENT_PRINCIPAL = new Principal() { 084 085 @Override 086 public String getName() { 087 return FOAF_AGENT_VALUE; 088 } 089 090 @Override 091 public String toString() { 092 return getName(); 093 } 094 095 }; 096 097 private static final PrincipalCollection FOAF_AGENT_PRINCIPAL_COLLECTION = 098 new SimplePrincipalCollection(FOAF_AGENT_PRINCIPAL, WebACAuthorizingRealm.class.getCanonicalName()); 099 100 private static Subject FOAF_AGENT_SUBJECT; 101 102 @Inject 103 private NodeService nodeService; 104 105 @Inject 106 private SessionFactory sessionFactory; 107 108 @Override 109 public void init(final FilterConfig filterConfig) { 110 // this method intentionally left empty 111 } 112 113 @Override 114 public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) 115 throws IOException, ServletException { 116 final Subject currentUser = SecurityUtils.getSubject(); 117 HttpServletRequest httpRequest = (HttpServletRequest) request; 118 if (isSparqlUpdate(httpRequest)) { 119 httpRequest = new CachedSparqlRequest(httpRequest); 120 } 121 122 if (currentUser.isAuthenticated()) { 123 log.debug("User is authenticated"); 124 if (currentUser.hasRole(FEDORA_ADMIN_ROLE)) { 125 log.debug("User has fedoraAdmin role"); 126 } else if (currentUser.hasRole(FEDORA_USER_ROLE)) { 127 log.debug("User has fedoraUser role"); 128 // non-admins are subject to permission checks 129 if (!isAuthorized(currentUser, httpRequest)) { 130 // if the user is not authorized, set response to forbidden 131 ((HttpServletResponse) response).sendError(SC_FORBIDDEN); 132 return; 133 } 134 } else { 135 log.debug("User has no recognized servlet container role"); 136 // missing a container role, return forbidden 137 ((HttpServletResponse) response).sendError(SC_FORBIDDEN); 138 return; 139 } 140 } else { 141 log.debug("User is NOT authenticated"); 142 // anonymous users are subject to permission checks 143 if (!isAuthorized(getFoafAgentSubject(), httpRequest)) { 144 // if anonymous user is not authorized, set response to forbidden 145 ((HttpServletResponse) response).sendError(SC_FORBIDDEN); 146 return; 147 } 148 } 149 150 // proceed to the next filter 151 chain.doFilter(httpRequest, response); 152 } 153 154 private Subject getFoafAgentSubject() { 155 if (FOAF_AGENT_SUBJECT == null) { 156 FOAF_AGENT_SUBJECT = new Subject.Builder().principals(FOAF_AGENT_PRINCIPAL_COLLECTION).buildSubject(); 157 } 158 return FOAF_AGENT_SUBJECT; 159 } 160 161 @Override 162 public void destroy() { 163 // this method intentionally left empty 164 } 165 166 private FedoraSession session() { 167 if (session == null) { 168 session = sessionFactory.getInternalSession(); 169 } 170 return session; 171 } 172 173 private String getBaseURL(final HttpServletRequest servletRequest) { 174 final String url = servletRequest.getRequestURL().toString(); 175 // the base URL will be the request URL if there is no path info 176 String baseURL = url; 177 178 // strip out the path info, if it exists 179 final String pathInfo = servletRequest.getPathInfo(); 180 if (pathInfo != null) { 181 final int loc = url.lastIndexOf(pathInfo); 182 baseURL = url.substring(0, loc); 183 } 184 185 log.debug("Base URL determined from servlet request is {}", baseURL); 186 return baseURL; 187 } 188 189 private FedoraResource resource(final HttpServletRequest servletRequest) { 190 return nodeService.find(session(), getRepoPath(servletRequest)); 191 } 192 193 private boolean resourceExists(final HttpServletRequest servletRequest) { 194 return nodeService.exists(session(), getRepoPath(servletRequest)); 195 } 196 197 private String getRepoPath(final HttpServletRequest servletRequest) { 198 final String httpURI = servletRequest.getRequestURL().toString(); 199 final HttpSession httpSession = new HttpSession(session()); 200 201 final UriBuilder uriBuilder = UriBuilder.fromUri(getBaseURL(servletRequest)).path(FedoraLdp.class); 202 final HttpResourceConverter conv = new HttpResourceConverter(httpSession, uriBuilder); 203 final Resource resource = ModelFactory.createDefaultModel().createResource(httpURI); 204 205 final String repoPath = conv.asString(resource); 206 log.debug("Converted request URI {} to repo path {}", httpURI, repoPath); 207 return repoPath; 208 } 209 210 private boolean isAuthorized(final Subject currentUser, final HttpServletRequest httpRequest) throws IOException { 211 final String requestURL = httpRequest.getRequestURL().toString(); 212 final boolean isAcl = requestURL.endsWith(FCR_ACL); 213 final URI requestURI = URI.create(requestURL); 214 log.debug("Request URI is {}", requestURI); 215 216 // WebAC permissions 217 final WebACPermission toRead = new WebACPermission(WEBAC_MODE_READ, requestURI); 218 final WebACPermission toWrite = new WebACPermission(WEBAC_MODE_WRITE, requestURI); 219 final WebACPermission toAppend = new WebACPermission(WEBAC_MODE_APPEND, requestURI); 220 final WebACPermission toControl = new WebACPermission(WEBAC_MODE_CONTROL, requestURI); 221 222 switch (httpRequest.getMethod()) { 223 case "OPTIONS": 224 case "HEAD": 225 case "GET": 226 if (isAcl) { 227 if (currentUser.isPermitted(toControl)) { 228 log.debug("GET allowed by {} permission", toControl); 229 return true; 230 } else { 231 log.debug("GET prohibited without {} permission", toControl); 232 return false; 233 } 234 } else { 235 return currentUser.isPermitted(toRead); 236 } 237 case "PUT": 238 if (isAcl) { 239 if (currentUser.isPermitted(toControl)) { 240 log.debug("PUT allowed by {} permission", toControl); 241 return true; 242 } else { 243 log.debug("PUT prohibited without {} permission", toControl); 244 return false; 245 } 246 } else if (currentUser.isPermitted(toWrite)) { 247 log.debug("PUT allowed by {} permission", toWrite); 248 return true; 249 } else { 250 if (resourceExists(httpRequest)) { 251 // can't PUT to an existing resource without acl:Write permission 252 log.debug("PUT prohibited to existing resource without {} permission", toWrite); 253 return false; 254 } else { 255 // find nearest parent resource and verify that user has acl:Append on it 256 // this works because when the authorizations are inherited, it is the target request URI that is 257 // added as the resource, not the accessTo or other URI in the original authorization 258 log.debug("Resource doesn't exist; checking parent resources for acl:Append permission"); 259 if (currentUser.isPermitted(toAppend)) { 260 log.debug("PUT allowed for new resource by inherited {} permission", toAppend); 261 return true; 262 } else { 263 log.debug("PUT prohibited for new resource without inherited {} permission", toAppend); 264 return false; 265 } 266 } 267 } 268 case "POST": 269 if (currentUser.isPermitted(toWrite)) { 270 log.debug("POST allowed by {} permission", toWrite); 271 return true; 272 } 273 if (resourceExists(httpRequest)) { 274 if (resource(httpRequest).hasType(FEDORA_BINARY)) { 275 // LDP-NR 276 // user without the acl:Write permission cannot POST to binaries 277 log.debug("POST prohibited to binary resource without {} permission", toWrite); 278 return false; 279 } else { 280 // LDP-RS 281 // user with the acl:Append permission may POST to containers 282 if (currentUser.isPermitted(toAppend)) { 283 log.debug("POST allowed to container by {} permission", toAppend); 284 return true; 285 } else { 286 log.debug("POST prohibited to container without {} permission", toAppend); 287 return false; 288 } 289 } 290 } else { 291 // prohibit POST to non-existent resources without the acl:Write permission 292 log.debug("POST prohibited to non-existent resource without {} permission", toWrite); 293 return false; 294 } 295 case "DELETE": 296 if (isAcl) { 297 if (currentUser.isPermitted(toControl)) { 298 log.debug("DELETE allowed by {} permission", toControl); 299 return true; 300 } else { 301 log.debug("DELETE prohibited without {} permission", toControl); 302 return false; 303 } 304 } else { 305 return currentUser.isPermitted(toWrite); 306 } 307 case "PATCH": 308 309 if (isAcl) { 310 if (currentUser.isPermitted(toControl)) { 311 log.debug("PATCH allowed by {} permission", toControl); 312 return true; 313 } else { 314 log.debug("PATCH prohibited without {} permission", toControl); 315 return false; 316 } 317 } else if (currentUser.isPermitted(toWrite)) { 318 return true; 319 } else { 320 if (currentUser.isPermitted(toAppend)) { 321 return isPatchContentPermitted(httpRequest); 322 } 323 } 324 return false; 325 default: 326 return false; 327 } 328 } 329 330 private boolean isPatchContentPermitted(final HttpServletRequest httpRequest) throws IOException { 331 if (!isSparqlUpdate(httpRequest)) { 332 log.debug("Cannot verify authorization on NON-SPARQL Patch request."); 333 return false; 334 } 335 if (httpRequest.getInputStream() != null) { 336 boolean noDeletes = false; 337 try { 338 noDeletes = !hasDeleteClause(IOUtils.toString(httpRequest.getInputStream(), UTF_8)); 339 } catch (final QueryParseException ex) { 340 log.error("Cannot verify authorization! Exception while inspecting SPARQL query!", ex); 341 } 342 return noDeletes; 343 } else { 344 log.debug("Authorizing SPARQL request with no content."); 345 return true; 346 } 347 } 348 349 private boolean hasDeleteClause(final String sparqlString) { 350 final UpdateRequest sparqlUpdate = UpdateFactory.create(sparqlString); 351 return sparqlUpdate.getOperations().stream() 352 .filter(update -> update instanceof UpdateDataDelete) 353 .map(update -> (UpdateDataDelete) update) 354 .anyMatch(update -> update.getQuads().size() > 0) || 355 sparqlUpdate.getOperations().stream().filter(update -> (update instanceof UpdateModify)) 356 .peek(update -> log.debug("Inspecting update statement for DELETE clause: {}", update.toString())) 357 .map(update -> (UpdateModify)update) 358 .filter(UpdateModify::hasDeleteClause) 359 .anyMatch(update -> update.getDeleteQuads().size() > 0); 360 } 361 362 private boolean isSparqlUpdate(final HttpServletRequest request) { 363 try { 364 return request.getMethod().equals("PATCH") && 365 sparqlUpdate.isCompatible(MediaType.valueOf(request 366 .getContentType())); 367 } catch (final IllegalArgumentException e) { 368 return false; 369 } 370 } 371}