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