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 */ 018package org.fcrepo.kernel.impl.services; 019 020import org.apache.jena.graph.Graph; 021import org.apache.jena.graph.Node; 022import org.apache.jena.rdf.model.Model; 023import org.apache.jena.rdf.model.RDFNode; 024import org.apache.jena.rdf.model.Statement; 025 026import org.fcrepo.config.FedoraPropsConfig; 027import org.fcrepo.kernel.api.ContainmentIndex; 028import org.fcrepo.kernel.api.RdfLexicon; 029import org.fcrepo.kernel.api.Transaction; 030import org.fcrepo.kernel.api.exception.ACLAuthorizationConstraintViolationException; 031import org.fcrepo.kernel.api.exception.MalformedRdfException; 032import org.fcrepo.kernel.api.exception.RequestWithAclLinkHeaderException; 033import org.fcrepo.kernel.api.exception.ServerManagedPropertyException; 034import org.fcrepo.kernel.api.identifiers.FedoraId; 035import org.fcrepo.kernel.api.observer.EventAccumulator; 036import org.fcrepo.kernel.api.operations.ResourceOperation; 037import org.fcrepo.kernel.api.services.MembershipService; 038import org.fcrepo.kernel.api.services.ReferenceService; 039import org.fcrepo.persistence.api.PersistentStorageSession; 040import org.slf4j.Logger; 041import org.springframework.beans.factory.annotation.Autowired; 042import org.springframework.beans.factory.annotation.Qualifier; 043 044import javax.inject.Inject; 045import java.util.HashSet; 046import java.util.List; 047import java.util.Set; 048import java.util.concurrent.atomic.AtomicBoolean; 049import java.util.concurrent.atomic.AtomicInteger; 050import java.util.regex.Pattern; 051 052import static org.apache.jena.graph.NodeFactory.createURI; 053import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 054import static org.apache.jena.rdf.model.ResourceFactory.createResource; 055import static org.apache.jena.rdf.model.ResourceFactory.createStatement; 056import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; 057import static org.fcrepo.kernel.api.RdfLexicon.DEFAULT_INTERACTION_MODEL; 058import static org.fcrepo.kernel.api.RdfLexicon.HAS_MEMBER_RELATION; 059import static org.fcrepo.kernel.api.RdfLexicon.INSERTED_CONTENT_RELATION; 060import static org.fcrepo.kernel.api.RdfLexicon.INTERACTION_MODELS_FULL; 061import static org.fcrepo.kernel.api.RdfLexicon.IS_MEMBER_OF_RELATION; 062import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE; 063import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; 064import static org.fcrepo.kernel.api.RdfLexicon.WEBAC_ACCESS_TO; 065import static org.fcrepo.kernel.api.RdfLexicon.WEBAC_ACCESS_TO_CLASS; 066import static org.fcrepo.kernel.api.RdfLexicon.WEBAC_ACCESS_TO_PROPERTY; 067import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; 068import static org.fcrepo.kernel.api.rdf.DefaultRdfStream.fromModel; 069import static org.slf4j.LoggerFactory.getLogger; 070 071 072/** 073 * Abstract service for interacting with a kernel service 074 * 075 * @author whikloj 076 * @author bseeger 077 */ 078 079public abstract class AbstractService { 080 081 private static final Logger log = getLogger(ReplacePropertiesServiceImpl.class); 082 083 private static final Node WEBAC_ACCESS_TO_URI = createURI(WEBAC_ACCESS_TO); 084 085 private static final Node WEBAC_ACCESS_TO_CLASS_URI = createURI(WEBAC_ACCESS_TO_CLASS); 086 087 @Autowired 088 @Qualifier("containmentIndex") 089 protected ContainmentIndex containmentIndex; 090 091 @Inject 092 private EventAccumulator eventAccumulator; 093 094 @Autowired 095 @Qualifier("referenceService") 096 protected ReferenceService referenceService; 097 098 @Inject 099 protected MembershipService membershipService; 100 101 @Inject 102 protected FedoraPropsConfig fedoraPropsConfig; 103 104 /** 105 * Utility to determine the correct interaction model from elements of a request. 106 * 107 * @param linkTypes Link headers with rel="type" 108 * @param isRdfContentType Is the Content-type a known RDF type? 109 * @param contentPresent Is there content present on the request body? 110 * @param isExternalContent Is there Link headers that define external content? 111 * @return The determined or default interaction model. 112 */ 113 protected String determineInteractionModel(final List<String> linkTypes, 114 final boolean isRdfContentType, final boolean contentPresent, 115 final boolean isExternalContent) { 116 final String interactionModel = linkTypes == null ? null : 117 linkTypes.stream().filter(INTERACTION_MODELS_FULL::contains).findFirst().orElse(null); 118 119 // If you define a valid interaction model, we try to use it. 120 if (interactionModel != null) { 121 return interactionModel; 122 } 123 if (isExternalContent || (contentPresent && !isRdfContentType)) { 124 return NON_RDF_SOURCE.toString(); 125 } else { 126 return DEFAULT_INTERACTION_MODEL.toString(); 127 } 128 } 129 130 /** 131 * Check that we don't try to provide an ACL Link header. 132 * 133 * @param links list of the link headers provided. 134 * @throws RequestWithAclLinkHeaderException If we provide an rel="acl" link header. 135 */ 136 protected void checkAclLinkHeader(final List<String> links) throws RequestWithAclLinkHeaderException { 137 final var matcher = Pattern.compile("rel=[\"']?acl[\"']?").asPredicate(); 138 if (links != null && links.stream().anyMatch(matcher)) { 139 throw new RequestWithAclLinkHeaderException( 140 "Unable to handle request with the specified LDP-RS as the ACL."); 141 } 142 } 143 144 /** 145 * Verifies that DirectContainer properties are valid, throwing exceptions if the triples 146 * do not meet LDP requirements or a server managed property is specified as a membership relation. 147 * If no membershipResource or membership relation are specified, defaults will be populated. 148 * @param fedoraId id of the resource described 149 * @param interactionModel interaction model of the resource 150 * @param model model to check 151 */ 152 protected void ensureValidDirectContainer(final FedoraId fedoraId, final String interactionModel, 153 final Model model) { 154 final boolean isIndirect = RdfLexicon.INDIRECT_CONTAINER.getURI().equals(interactionModel); 155 if (!(RdfLexicon.DIRECT_CONTAINER.getURI().equals(interactionModel) 156 || isIndirect)) { 157 return; 158 } 159 final var dcResc = model.getResource(fedoraId.getFullId()); 160 final AtomicBoolean hasMembershipResc = new AtomicBoolean(false); 161 final AtomicBoolean hasRelation = new AtomicBoolean(false); 162 final AtomicInteger insertedContentRelationCount = new AtomicInteger(0); 163 164 dcResc.listProperties().forEachRemaining(stmt -> { 165 final var predicate = stmt.getPredicate(); 166 167 if (MEMBERSHIP_RESOURCE.equals(predicate)) { 168 if (hasMembershipResc.get()) { 169 throw new MalformedRdfException("Direct and Indirect containers must specify" 170 + " exactly one ldp:membershipResource property, multiple are present"); 171 } 172 173 if (stmt.getObject().isURIResource()) { 174 hasMembershipResc.set(true); 175 } else { 176 throw new MalformedRdfException("Direct and Indirect containers must specify" 177 + " a ldp:membershipResource property with a resource as the object"); 178 } 179 } else if (HAS_MEMBER_RELATION.equals(predicate) || IS_MEMBER_OF_RELATION.equals(predicate)) { 180 if (hasRelation.get()) { 181 throw new MalformedRdfException("Direct and Indirect containers must specify exactly one" 182 + " ldp:hasMemberRelation or ldp:isMemberOfRelation property, but multiple were present"); 183 } 184 185 final RDFNode obj = stmt.getObject(); 186 if (obj.isURIResource()) { 187 final String uri = obj.asResource().getURI(); 188 189 // Throw exception if object is a server-managed property 190 if (isManagedPredicate.test(createProperty(uri))) { 191 throw new ServerManagedPropertyException(String.format( 192 "%s cannot take a server managed property as an object: property value = %s.", 193 predicate.getLocalName(), uri)); 194 } 195 hasRelation.set(true); 196 } else { 197 throw new MalformedRdfException("Direct and Indirect containers must specify either" 198 + " ldp:hasMemberRelation or ldp:isMemberOfRelation properties," 199 + " with a predicate as the object"); 200 } 201 } else if (isIndirect && INSERTED_CONTENT_RELATION.equals(predicate)) { 202 insertedContentRelationCount.incrementAndGet(); 203 final RDFNode obj = stmt.getObject(); 204 if (obj.isURIResource()) { 205 final String uri = obj.asResource().getURI(); 206 // Throw exception if object is a server-managed property 207 if (isManagedPredicate.test(createProperty(uri))) { 208 throw new ServerManagedPropertyException(String.format( 209 "%s cannot take a server managed property as an object: property value = %s.", 210 predicate.getLocalName(), uri)); 211 } 212 } else { 213 throw new MalformedRdfException("Indirect containers must specify an" 214 + " ldp:insertedContentRelation property with a URI property as the object"); 215 } 216 } 217 }); 218 219 if (isIndirect) { 220 if (insertedContentRelationCount.get() > 1) { 221 throw new MalformedRdfException("Indirect containers must contain exactly one triple" 222 + " with the predicate ldp:insertedContentRelation and a property as the object."); 223 } else if (insertedContentRelationCount.get() == 0) { 224 dcResc.addProperty(INSERTED_CONTENT_RELATION, RdfLexicon.MEMBER_SUBJECT); 225 } 226 } 227 if (!hasMembershipResc.get()) { 228 dcResc.addProperty(MEMBERSHIP_RESOURCE, dcResc); 229 } 230 if (!hasRelation.get()) { 231 dcResc.addProperty(HAS_MEMBER_RELATION, RdfLexicon.LDP_MEMBER); 232 } 233 } 234 235 /** 236 * This method does two things: 237 * - Throws an exception if an authorization has both accessTo and accessToClass 238 * - Adds a default accessTo target if an authorization has neither accessTo nor accessToClass 239 * 240 * @param inputModel to be checked and updated 241 */ 242 protected void ensureValidACLAuthorization(final Model inputModel) { 243 244 // TODO -- check ACL first 245 246 final Set<Node> uniqueAuthSubjects = new HashSet<>(); 247 inputModel.listStatements().forEachRemaining((final Statement s) -> { 248 log.debug("statement: s={}, p={}, o={}", s.getSubject(), s.getPredicate(), s.getObject()); 249 final Node subject = s.getSubject().asNode(); 250 // If subject is Authorization Hash Resource, add it to the map with its accessTo/accessToClass status. 251 if (subject.toString().contains("/" + FCR_ACL + "#")) { 252 uniqueAuthSubjects.add(subject); 253 } 254 }); 255 final Graph graph = inputModel.getGraph(); 256 uniqueAuthSubjects.forEach((final Node subject) -> { 257 if (graph.contains(subject, WEBAC_ACCESS_TO_URI, Node.ANY) && 258 graph.contains(subject, WEBAC_ACCESS_TO_CLASS_URI, Node.ANY)) { 259 throw new ACLAuthorizationConstraintViolationException( 260 String.format( 261 "Using both accessTo and accessToClass within " + 262 "a single Authorization is not allowed: %s.", 263 subject.toString().substring(subject.toString().lastIndexOf("#")))); 264 } else if (!(graph.contains(subject, WEBAC_ACCESS_TO_URI, Node.ANY) || 265 graph.contains(subject, WEBAC_ACCESS_TO_CLASS_URI, Node.ANY))) { 266 inputModel.add(createDefaultAccessToStatement(subject.toString())); 267 } 268 }); 269 } 270 271 protected void recordEvent(final String transactionId, final FedoraId fedoraId, final ResourceOperation operation) { 272 this.eventAccumulator.recordEventForOperation(transactionId, fedoraId, operation); 273 } 274 275 /** 276 * Wrapper to call the referenceService updateReference method 277 * @param transactionId the transaction ID. 278 * @param resourceId the resource's ID. 279 * @param model the model of the request body. 280 */ 281 protected void updateReferences(final String transactionId, final FedoraId resourceId, final String user, 282 final Model model) { 283 referenceService.updateReferences(transactionId, resourceId, user, 284 fromModel(model.getResource(resourceId.getFullId()).asNode(), model)); 285 } 286 287 protected void lockArchivalGroupResource(final Transaction tx, 288 final PersistentStorageSession pSession, 289 final FedoraId fedoraId) { 290 final var headers = pSession.getHeaders(fedoraId, null); 291 if (headers.getArchivalGroupId() != null) { 292 tx.lockResource(headers.getArchivalGroupId()); 293 } 294 } 295 296 protected void lockArchivalGroupResourceFromParent(final Transaction tx, 297 final PersistentStorageSession pSession, 298 final FedoraId parentId) { 299 if (parentId != null && !parentId.isRepositoryRoot()) { 300 final var parentHeaders = pSession.getHeaders(parentId, null); 301 if (parentHeaders.isArchivalGroup()) { 302 tx.lockResource(parentId); 303 } else if (parentHeaders.getArchivalGroupId() != null) { 304 tx.lockResource(parentHeaders.getArchivalGroupId()); 305 } 306 } 307 } 308 309 /** 310 * Returns a Statement with the resource containing the acl to be the accessTo target for the given auth subject. 311 * 312 * @param authSubject - acl authorization subject uri string 313 * @return acl statement 314 */ 315 private Statement createDefaultAccessToStatement(final String authSubject) { 316 final String currentResourcePath = authSubject.substring(0, authSubject.indexOf("/" + FCR_ACL)); 317 return createStatement( 318 createResource(authSubject), 319 WEBAC_ACCESS_TO_PROPERTY, 320 createResource(currentResourcePath)); 321 } 322} 323