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 static org.apache.jena.graph.NodeFactory.createURI; 021import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 022import static org.apache.jena.rdf.model.ResourceFactory.createResource; 023import static org.apache.jena.rdf.model.ResourceFactory.createStatement; 024import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; 025import static org.fcrepo.kernel.api.RdfLexicon.DEFAULT_INTERACTION_MODEL; 026import static org.fcrepo.kernel.api.RdfLexicon.HAS_MEMBER_RELATION; 027import static org.fcrepo.kernel.api.RdfLexicon.INTERACTION_MODELS_FULL; 028import static org.fcrepo.kernel.api.RdfLexicon.IS_MEMBER_OF_RELATION; 029import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE; 030import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; 031import static org.fcrepo.kernel.api.RdfLexicon.WEBAC_ACCESS_TO; 032import static org.fcrepo.kernel.api.RdfLexicon.WEBAC_ACCESS_TO_CLASS; 033import static org.fcrepo.kernel.api.RdfLexicon.WEBAC_ACCESS_TO_PROPERTY; 034import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; 035import static org.fcrepo.kernel.api.rdf.DefaultRdfStream.fromModel; 036import static org.slf4j.LoggerFactory.getLogger; 037 038import javax.inject.Inject; 039 040import java.util.ArrayList; 041import java.util.HashSet; 042import java.util.List; 043import java.util.Set; 044import java.util.concurrent.atomic.AtomicBoolean; 045import java.util.regex.Pattern; 046 047import org.apache.jena.graph.Graph; 048import org.apache.jena.graph.Node; 049import org.apache.jena.graph.Triple; 050import org.apache.jena.rdf.model.Model; 051import org.apache.jena.rdf.model.RDFNode; 052import org.apache.jena.rdf.model.Statement; 053import org.fcrepo.kernel.api.ContainmentIndex; 054import org.fcrepo.kernel.api.RdfLexicon; 055import org.fcrepo.kernel.api.exception.ACLAuthorizationConstraintViolationException; 056import org.fcrepo.kernel.api.exception.MalformedRdfException; 057import org.fcrepo.kernel.api.exception.RequestWithAclLinkHeaderException; 058import org.fcrepo.kernel.api.exception.ServerManagedPropertyException; 059import org.fcrepo.kernel.api.identifiers.FedoraId; 060import org.fcrepo.kernel.api.observer.EventAccumulator; 061import org.fcrepo.kernel.api.operations.ResourceOperation; 062import org.fcrepo.kernel.api.services.MembershipService; 063import org.fcrepo.kernel.api.services.ReferenceService; 064import org.slf4j.Logger; 065import org.springframework.beans.factory.annotation.Autowired; 066import org.springframework.beans.factory.annotation.Qualifier; 067 068 069/** 070 * Abstract service for interacting with a kernel service 071 * 072 * @author whikloj 073 * @author bseeger 074 */ 075 076public abstract class AbstractService { 077 078 private static final Logger log = getLogger(ReplacePropertiesServiceImpl.class); 079 080 private static final Node WEBAC_ACCESS_TO_URI = createURI(WEBAC_ACCESS_TO); 081 082 private static final Node WEBAC_ACCESS_TO_CLASS_URI = createURI(WEBAC_ACCESS_TO_CLASS); 083 084 protected final List<Triple> serverManagedProperties = new ArrayList<>(); 085 086 @Autowired 087 @Qualifier("containmentIndex") 088 protected ContainmentIndex containmentIndex; 089 090 @Inject 091 private EventAccumulator eventAccumulator; 092 093 @Autowired 094 @Qualifier("referenceService") 095 protected ReferenceService referenceService; 096 097 @Inject 098 protected MembershipService membershipService; 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 if (!(RdfLexicon.DIRECT_CONTAINER.getURI().equals(interactionModel) 151 || RdfLexicon.INDIRECT_CONTAINER.getURI().equals(interactionModel))) { 152 return; 153 } 154 final var dcResc = model.getResource(fedoraId.getFullId()); 155 final AtomicBoolean hasMembershipResc = new AtomicBoolean(false); 156 final AtomicBoolean hasRelation = new AtomicBoolean(false); 157 158 dcResc.listProperties().forEachRemaining(stmt -> { 159 final var predicate = stmt.getPredicate(); 160 161 if (MEMBERSHIP_RESOURCE.equals(predicate)) { 162 if (hasMembershipResc.get()) { 163 throw new MalformedRdfException("Direct and Indirect containers must specify" 164 + " exactly one ldp:membershipResource property, multiple are present"); 165 } 166 167 if (stmt.getObject().isURIResource()) { 168 hasMembershipResc.set(true); 169 } else { 170 throw new MalformedRdfException("Direct and Indirect containers must specify" 171 + " a ldp:membershipResource property with a resource as the object"); 172 } 173 } else if (HAS_MEMBER_RELATION.equals(predicate) || IS_MEMBER_OF_RELATION.equals(predicate)) { 174 if (hasRelation.get()) { 175 throw new MalformedRdfException("Direct and Indirect containers must specify exactly one" 176 + " ldp:hasMemberRelation or ldp:isMemberOfRelation property, but multiple were present"); 177 } 178 179 final RDFNode obj = stmt.getObject(); 180 if (obj.isURIResource()) { 181 final String uri = obj.asResource().getURI(); 182 183 // Throw exception if object is a server-managed property 184 if (isManagedPredicate.test(createProperty(uri))) { 185 throw new ServerManagedPropertyException(String.format( 186 "%s cannot take a server managed property as an object: property value = %s.", 187 predicate.getLocalName(), uri)); 188 } 189 hasRelation.set(true); 190 } else { 191 throw new MalformedRdfException("Direct and Indirect containers must specify either" 192 + " ldp:hasMemberRelation or ldp:isMemberOfRelation properties," 193 + " with a predicate as the object"); 194 } 195 } 196 }); 197 198 if (!hasMembershipResc.get()) { 199 dcResc.addProperty(MEMBERSHIP_RESOURCE, dcResc); 200 } 201 if (!hasRelation.get()) { 202 dcResc.addProperty(HAS_MEMBER_RELATION, RdfLexicon.LDP_MEMBER); 203 } 204 } 205 206 /** 207 * This method does two things: 208 * - Throws an exception if an authorization has both accessTo and accessToClass 209 * - Adds a default accessTo target if an authorization has neither accessTo nor accessToClass 210 * 211 * @param inputModel to be checked and updated 212 */ 213 protected void ensureValidACLAuthorization(final Model inputModel) { 214 215 // TODO -- check ACL first 216 217 final Set<Node> uniqueAuthSubjects = new HashSet<>(); 218 inputModel.listStatements().forEachRemaining((final Statement s) -> { 219 log.debug("statement: s={}, p={}, o={}", s.getSubject(), s.getPredicate(), s.getObject()); 220 final Node subject = s.getSubject().asNode(); 221 // If subject is Authorization Hash Resource, add it to the map with its accessTo/accessToClass status. 222 if (subject.toString().contains("/" + FCR_ACL + "#")) { 223 uniqueAuthSubjects.add(subject); 224 } 225 }); 226 final Graph graph = inputModel.getGraph(); 227 uniqueAuthSubjects.forEach((final Node subject) -> { 228 if (graph.contains(subject, WEBAC_ACCESS_TO_URI, Node.ANY) && 229 graph.contains(subject, WEBAC_ACCESS_TO_CLASS_URI, Node.ANY)) { 230 throw new ACLAuthorizationConstraintViolationException( 231 String.format( 232 "Using both accessTo and accessToClass within " + 233 "a single Authorization is not allowed: %s.", 234 subject.toString().substring(subject.toString().lastIndexOf("#")))); 235 } else if (!(graph.contains(subject, WEBAC_ACCESS_TO_URI, Node.ANY) || 236 graph.contains(subject, WEBAC_ACCESS_TO_CLASS_URI, Node.ANY))) { 237 inputModel.add(createDefaultAccessToStatement(subject.toString())); 238 } 239 }); 240 } 241 242 protected void recordEvent(final String transactionId, final FedoraId fedoraId, final ResourceOperation operation) { 243 this.eventAccumulator.recordEventForOperation(transactionId, fedoraId, operation); 244 } 245 246 /** 247 * Wrapper to call the referenceService updateReference method 248 * @param transactionId the transaction ID. 249 * @param resourceId the resource's ID. 250 * @param model the model of the request body. 251 */ 252 protected void updateReferences(final String transactionId, final FedoraId resourceId, final String user, 253 final Model model) { 254 referenceService.updateReferences(transactionId, resourceId, user, 255 fromModel(model.getResource(resourceId.getFullId()).asNode(), model)); 256 } 257 258 /** 259 * Returns a Statement with the resource containing the acl to be the accessTo target for the given auth subject. 260 * 261 * @param authSubject - acl authorization subject uri string 262 * @return acl statement 263 */ 264 private Statement createDefaultAccessToStatement(final String authSubject) { 265 final String currentResourcePath = authSubject.substring(0, authSubject.indexOf("/" + FCR_ACL)); 266 return createStatement( 267 createResource(authSubject), 268 WEBAC_ACCESS_TO_PROPERTY, 269 createResource(currentResourcePath)); 270 } 271} 272