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