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