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