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