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