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