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.integration.auth.webac;
007
008import static java.nio.charset.StandardCharsets.UTF_8;
009import static java.util.Arrays.stream;
010import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
011import static javax.ws.rs.core.Response.Status.CONFLICT;
012import static javax.ws.rs.core.Response.Status.CREATED;
013import static javax.ws.rs.core.Response.Status.FORBIDDEN;
014import static javax.ws.rs.core.Response.Status.NO_CONTENT;
015import static javax.ws.rs.core.Response.Status.OK;
016import static org.apache.http.HttpHeaders.CONTENT_TYPE;
017import static org.apache.http.HttpStatus.SC_CREATED;
018import static org.apache.http.HttpStatus.SC_FORBIDDEN;
019import static org.apache.http.HttpStatus.SC_GONE;
020import static org.apache.http.HttpStatus.SC_NOT_FOUND;
021import static org.apache.http.HttpStatus.SC_NO_CONTENT;
022import static org.apache.jena.vocabulary.DC_11.title;
023import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER;
024import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
025import static org.fcrepo.kernel.api.FedoraTypes.FCR_TOMBSTONE;
026import static org.fcrepo.kernel.api.FedoraTypes.FCR_TX;
027import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
028import static org.fcrepo.kernel.api.RdfLexicon.EMBED_CONTAINED;
029import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
030import static org.fcrepo.kernel.api.RdfLexicon.MEMBERSHIP_RESOURCE;
031import static org.junit.Assert.assertEquals;
032import static org.junit.Assert.assertTrue;
033
034import java.io.IOException;
035import java.io.InputStream;
036import java.io.UnsupportedEncodingException;
037import java.nio.file.Paths;
038import java.util.Arrays;
039import java.util.Optional;
040import java.util.regex.Pattern;
041
042import javax.ws.rs.core.Link;
043import javax.ws.rs.core.Response;
044
045import org.fcrepo.auth.webac.WebACRolesProvider;
046import org.fcrepo.http.commons.test.util.CloseableDataset;
047import org.fcrepo.integration.http.api.AbstractResourceIT;
048import org.fcrepo.integration.http.api.TestIsolationExecutionListener;
049
050import org.apache.commons.codec.binary.Base64;
051import org.apache.http.Header;
052import org.apache.http.HeaderElement;
053import org.apache.http.HttpEntity;
054import org.apache.http.HttpResponse;
055import org.apache.http.HttpStatus;
056import org.apache.http.NameValuePair;
057import org.apache.http.client.config.RequestConfig;
058import org.apache.http.client.methods.CloseableHttpResponse;
059import org.apache.http.client.methods.HttpDelete;
060import org.apache.http.client.methods.HttpGet;
061import org.apache.http.client.methods.HttpHead;
062import org.apache.http.client.methods.HttpOptions;
063import org.apache.http.client.methods.HttpPatch;
064import org.apache.http.client.methods.HttpPost;
065import org.apache.http.client.methods.HttpPut;
066import org.apache.http.entity.ContentType;
067import org.apache.http.entity.InputStreamEntity;
068import org.apache.http.entity.StringEntity;
069import org.apache.http.message.AbstractHttpMessage;
070import org.apache.jena.graph.Node;
071import org.apache.jena.graph.NodeFactory;
072import org.apache.jena.sparql.core.DatasetGraph;
073import org.glassfish.grizzly.utils.Charsets;
074import org.junit.Before;
075import org.junit.Rule;
076import org.junit.Test;
077import org.junit.contrib.java.lang.system.RestoreSystemProperties;
078import org.slf4j.Logger;
079import org.slf4j.LoggerFactory;
080import org.springframework.test.context.TestExecutionListeners;
081
082/**
083 * @author Peter Eichman
084 * @author whikloj
085 * @since September 4, 2015
086 */
087@TestExecutionListeners(
088        listeners = { TestIsolationExecutionListener.class },
089        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
090public class WebACRecipesIT extends AbstractResourceIT {
091
092    private static final Logger logger = LoggerFactory.getLogger(WebACRecipesIT.class);
093
094    @Rule
095    public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
096
097    private final ContentType turtleContentType = ContentType.create("text/turtle", "UTF-8");
098
099    private final ContentType sparqlContentType = ContentType.create("application/sparql-update", "UTF-8");
100
101    private WebACRolesProvider rolesProvider;
102
103    @Before
104    public void setup() {
105        this.rolesProvider = getBean(WebACRolesProvider.class);
106        authPropsConfig.setRootAuthAclPath(null);
107        rolesProvider.setGroupBaseUri(null);
108        rolesProvider.setUserBaseUri(null);
109    }
110
111    /**
112     * Convenience method to create an ACL with 0 or more authorization resources in the repository.
113     */
114    private String ingestAcl(final String username,
115            final String aclFilePath, final String aclResourcePath) throws IOException {
116
117        // create the ACL
118        final HttpResponse aclResponse = ingestTurtleResource(username, aclFilePath, aclResourcePath);
119
120        // return the URI to the newly created resource
121        return aclResponse.getFirstHeader("Location").getValue();
122    }
123
124    /**
125     * Convenience method to POST the contents of a Turtle file to the repository to create a new resource. Returns
126     * the HTTP response from that request. Throws an IOException if the server responds with anything other than a
127     * 201 Created response code.
128     */
129    private HttpResponse ingestTurtleResource(final String username, final String path, final String requestURI)
130            throws IOException {
131        final HttpPut request = new HttpPut(requestURI);
132
133        logger.debug("PUT to {} to create {}", requestURI, path);
134
135        setAuth(request, username);
136
137        final InputStream file = this.getClass().getResourceAsStream(path);
138        final InputStreamEntity fileEntity = new InputStreamEntity(file);
139        request.setEntity(fileEntity);
140        request.setHeader("Content-Type", "text/turtle");
141
142        try (final CloseableHttpResponse response = execute(request)) {
143            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
144            return response;
145        }
146
147    }
148
149    /**
150     * Convenience method to set up a regular FedoraResource
151     *
152     * @param path Path to put the resource under
153     * @return the Location of the newly created resource
154     * @throws IOException on error
155     */
156    private String ingestObj(final String path) throws IOException {
157        final HttpPut request = putObjMethod(path.replace(serverAddress, ""));
158        setAuth(request, "fedoraAdmin");
159        try (final CloseableHttpResponse response = execute(request)) {
160            assertEquals(HttpStatus.SC_CREATED, response.getStatusLine().getStatusCode());
161            return response.getFirstHeader("Location").getValue();
162        }
163    }
164
165    private String ingestBinary(final String path, final HttpEntity body) throws IOException {
166        logger.info("Ingesting {} binary to {}", body.getContentType().getValue(), path);
167        final HttpPut request = new HttpPut(serverAddress + path);
168        setAuth(request, "fedoraAdmin");
169        request.setEntity(body);
170        request.setHeader(body.getContentType());
171        final CloseableHttpResponse response = execute(request);
172        assertEquals(HttpStatus.SC_CREATED, response.getStatusLine().getStatusCode());
173        final String location = response.getFirstHeader("Location").getValue();
174        logger.info("Created binary at {}", location);
175        return location;
176
177    }
178
179    private String ingestDatastream(final String path, final String ds) throws IOException {
180        final HttpPut request = putDSMethod(path, ds, "some not so random content");
181        setAuth(request, "fedoraAdmin");
182        try (final CloseableHttpResponse response = execute(request)) {
183            assertEquals(HttpStatus.SC_CREATED, response.getStatusLine().getStatusCode());
184            return response.getFirstHeader("Location").getValue();
185        }
186    }
187
188    /**
189     * Convenience method for applying credentials to a request
190     *
191     * @param method the request to add the credentials to
192     * @param username the username to add
193     */
194    private static void setAuth(final AbstractHttpMessage method, final String username) {
195        final String creds = username + ":password";
196        final String encCreds = new String(Base64.encodeBase64(creds.getBytes()));
197        final String basic = "Basic " + encCreds;
198        method.setHeader("Authorization", basic);
199    }
200
201    @Test
202    public void scenario1() throws IOException {
203        final String testObj = ingestObj("/rest/webacl_box1");
204
205        final String acl1 = ingestAcl("fedoraAdmin", "/acls/01/acl.ttl",
206                                      testObj + "/fcr:acl");
207        final String aclLink = Link.fromUri(acl1).rel("acl").build().toString();
208
209        final HttpGet request = getObjMethod(testObj.replace(serverAddress, ""));
210        assertEquals("Anonymous can read " + testObj, HttpStatus.SC_FORBIDDEN, getStatus(request));
211
212        setAuth(request, "user01");
213        try (final CloseableHttpResponse response = execute(request)) {
214            assertEquals("User 'user01' can't read" + testObj, HttpStatus.SC_OK, getStatus(response));
215            // This gets the Link headers and filters for the correct one (aclLink::equals) defined above.
216            final Optional<String> header = stream(response.getHeaders("Link")).map(Header::getValue)
217                    .filter(aclLink::equals).findFirst();
218            // So you either have the correct Link header or you get nothing.
219            assertTrue("Missing Link header", header.isPresent());
220        }
221
222        final String childObj = ingestObj("/rest/webacl_box1/child");
223        final HttpGet getReq = getObjMethod(childObj.replace(serverAddress, ""));
224        setAuth(getReq, "user01");
225        try (final CloseableHttpResponse response = execute(getReq)) {
226            assertEquals("User 'user01' can't read child of " + testObj, HttpStatus.SC_OK, getStatus(response));
227        }
228    }
229
230    @Test
231    public void scenario2() throws IOException {
232        final String id = "/rest/box/bag/collection";
233        final String testObj = ingestObj(id);
234        ingestAcl("fedoraAdmin", "/acls/02/acl.ttl", testObj + "/fcr:acl");
235
236        logger.debug("Anonymous can not read " + testObj);
237        final HttpGet requestGet = getObjMethod(id);
238        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet));
239
240        logger.debug("GroupId 'Editors' can read " + testObj);
241        final HttpGet requestGet2 = getObjMethod(id);
242        setAuth(requestGet2, "jones");
243        requestGet2.setHeader("some-header", "Editors");
244        assertEquals(HttpStatus.SC_OK, getStatus(requestGet2));
245
246        logger.debug("Anonymous cannot write " + testObj);
247        final HttpPatch requestPatch = patchObjMethod(id);
248        requestPatch.setEntity(new StringEntity("INSERT { <> <" + title.getURI() + "> \"Test title\" . } WHERE {}"));
249        requestPatch.setHeader("Content-type", "application/sparql-update");
250        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestPatch));
251
252        logger.debug("Editors can write " + testObj);
253        final HttpPatch requestPatch2 = patchObjMethod(id);
254        setAuth(requestPatch2, "jones");
255        requestPatch2.setHeader("some-header", "Editors");
256        requestPatch2.setEntity(
257                new StringEntity("INSERT { <> <" + title.getURI() + "> \"Different title\" . } WHERE {}"));
258        requestPatch2.setHeader("Content-type", "application/sparql-update");
259        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestPatch2));
260    }
261
262    @Test
263    public void scenario3() throws IOException {
264        final String idDark = "/rest/dark/archive";
265        final String idLight = "/rest/dark/archive/sunshine";
266        final String testObj = ingestObj(idDark);
267        final String testObj2 = ingestObjWithACL(idLight, "/acls/03/acl.ttl");
268        ingestAcl("fedoraAdmin", "/acls/03/acl.ttl", testObj + "/fcr:acl");
269
270        logger.debug("Anonymous can't read " + testObj);
271        final HttpGet requestGet = getObjMethod(idDark);
272        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet));
273
274        logger.debug("Restricted can read " + testObj);
275        final HttpGet requestGet2 = getObjMethod(idDark);
276        setAuth(requestGet2, "jones");
277        requestGet2.setHeader("some-header", "Restricted");
278        assertEquals(HttpStatus.SC_OK, getStatus(requestGet2));
279
280        logger.debug("Anonymous can read " + testObj2);
281        final HttpGet requestGet3 = getObjMethod(idLight);
282        assertEquals(HttpStatus.SC_OK, getStatus(requestGet3));
283
284        logger.debug("Restricted can read " + testObj2);
285        final HttpGet requestGet4 = getObjMethod(idLight);
286        setAuth(requestGet4, "jones");
287        requestGet4.setHeader("some-header", "Restricted");
288        assertEquals(HttpStatus.SC_OK, getStatus(requestGet4));
289    }
290
291    @Test
292    public void scenario4() throws IOException {
293        final String id = "/rest/public_collection";
294        final String testObj = ingestObjWithACL(id, "/acls/04/acl.ttl");
295
296        logger.debug("Anonymous can read " + testObj);
297        final HttpGet requestGet = getObjMethod(id);
298        assertEquals(HttpStatus.SC_OK, getStatus(requestGet));
299
300        logger.debug("Editors can read " + testObj);
301        final HttpGet requestGet2 = getObjMethod(id);
302        setAuth(requestGet2, "jones");
303        requestGet2.setHeader("some-header", "Editors");
304        assertEquals(HttpStatus.SC_OK, getStatus(requestGet2));
305
306        logger.debug("Smith can access " + testObj);
307        final HttpGet requestGet3 = getObjMethod(id);
308        setAuth(requestGet3, "smith");
309        assertEquals(HttpStatus.SC_OK, getStatus(requestGet3));
310
311        logger.debug("Anonymous can't write " + testObj);
312        final HttpPatch requestPatch = patchObjMethod(id);
313        requestPatch.setHeader("Content-type", "application/sparql-update");
314        requestPatch.setEntity(new StringEntity("INSERT { <> <" + title.getURI() + "> \"Change title\" . } WHERE {}"));
315        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestPatch));
316
317        logger.debug("Editors can write " + testObj);
318        final HttpPatch requestPatch2 = patchObjMethod(id);
319        requestPatch2.setHeader("Content-type", "application/sparql-update");
320        requestPatch2.setEntity(new StringEntity("INSERT { <> <" + title.getURI() + "> \"New title\" . } WHERE {}"));
321        setAuth(requestPatch2, "jones");
322        requestPatch2.setHeader("some-header", "Editors");
323        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestPatch2));
324
325        logger.debug("Editors can create (PUT) child objects of " + testObj);
326        final HttpPut requestPut1 = putObjMethod(id + "/child1");
327        setAuth(requestPut1, "jones");
328        requestPut1.setHeader("some-header", "Editors");
329        assertEquals(HttpStatus.SC_CREATED, getStatus(requestPut1));
330
331        final HttpGet requestGet4 = getObjMethod(id + "/child1");
332        setAuth(requestGet4, "jones");
333        requestGet4.setHeader("some-header", "Editors");
334        assertEquals(HttpStatus.SC_OK, getStatus(requestGet4));
335
336        logger.debug("Editors can create (POST) child objects of " + testObj);
337        final HttpPost requestPost1 = postObjMethod(id);
338        requestPost1.addHeader("Slug", "child2");
339        setAuth(requestPost1, "jones");
340        requestPost1.setHeader("some-header", "Editors");
341        assertEquals(HttpStatus.SC_CREATED, getStatus(requestPost1));
342
343        final HttpGet requestGet5 = getObjMethod(id + "/child2");
344        setAuth(requestGet5, "jones");
345        requestGet5.setHeader("some-header", "Editors");
346        assertEquals(HttpStatus.SC_OK, getStatus(requestGet5));
347
348        logger.debug("Editors can create nested child objects of " + testObj);
349        final HttpPut requestPut2 = putObjMethod(id + "/a/b/c/child");
350        setAuth(requestPut2, "jones");
351        requestPut2.setHeader("some-header", "Editors");
352        assertEquals(HttpStatus.SC_CREATED, getStatus(requestPut2));
353
354        final HttpGet requestGet6 = getObjMethod(id + "/a/b/c/child");
355        setAuth(requestGet6, "jones");
356        requestGet6.setHeader("some-header", "Editors");
357        assertEquals(HttpStatus.SC_OK, getStatus(requestGet6));
358
359        logger.debug("Smith can't write " + testObj);
360        final HttpPatch requestPatch3 = patchObjMethod(id);
361        requestPatch3.setHeader("Content-type", "application/sparql-update");
362        requestPatch3.setEntity(
363                new StringEntity("INSERT { <> <" + title.getURI() + "> \"Different title\" . } WHERE {}"));
364        setAuth(requestPatch3, "smith");
365        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestPatch3));
366    }
367
368    @Test
369    public void scenario5() throws IOException {
370        final String idPublic = "/rest/mixedCollection/publicObj";
371        final String idPrivate = "/rest/mixedCollection/privateObj";
372        ingestObjWithACL("/rest/mixedCollection", "/acls/05/acl.ttl");
373        final String publicObj = ingestObj(idPublic);
374        final String privateObj = ingestObj(idPrivate);
375        final HttpPatch patch = patchObjMethod(idPublic);
376
377        setAuth(patch, "fedoraAdmin");
378        patch.setHeader("Content-type", "application/sparql-update");
379        patch.setEntity(new StringEntity("INSERT { <> a <http://example.com/terms#publicImage> . } WHERE {}"));
380        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patch));
381
382
383        logger.debug("Anonymous can see eg:publicImage " + publicObj);
384        final HttpGet requestGet = getObjMethod(idPublic);
385        assertEquals(HttpStatus.SC_OK, getStatus(requestGet));
386
387        logger.debug("Anonymous can't see other resource " + privateObj);
388        final HttpGet requestGet2 = getObjMethod(idPrivate);
389        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet2));
390
391        logger.debug("Admins can see eg:publicImage " + publicObj);
392        final HttpGet requestGet3 = getObjMethod(idPublic);
393        setAuth(requestGet3, "jones");
394        requestGet3.setHeader("some-header", "Admins");
395        assertEquals(HttpStatus.SC_OK, getStatus(requestGet3));
396
397        logger.debug("Admins can see others" + privateObj);
398        final HttpGet requestGet4 = getObjMethod(idPrivate);
399        setAuth(requestGet4, "jones");
400        requestGet4.setHeader("some-header", "Admins");
401        assertEquals(HttpStatus.SC_OK, getStatus(requestGet4));
402    }
403
404    @Test
405    public void scenario9() throws IOException {
406        final String idPublic = "/rest/anotherCollection/publicObj";
407        final String groups = "/rest/group";
408        final String fooGroup = groups + "/foo";
409        final String testObj = ingestObj("/rest/anotherCollection");
410        final String publicObj = ingestObj(idPublic);
411
412        final HttpPut request = putObjMethod(fooGroup);
413        setAuth(request, "fedoraAdmin");
414
415        final InputStream file = this.getClass().getResourceAsStream("/acls/09/group.ttl");
416        final InputStreamEntity fileEntity = new InputStreamEntity(file);
417        request.setEntity(fileEntity);
418        request.setHeader("Content-Type", "text/turtle;charset=UTF-8");
419
420        assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(request));
421
422        ingestAcl("fedoraAdmin", "/acls/09/acl.ttl", testObj + "/fcr:acl");
423
424        logger.debug("Person1 can see object " + publicObj);
425        final HttpGet requestGet1 = getObjMethod(idPublic);
426        setAuth(requestGet1, "person1");
427        assertEquals(HttpStatus.SC_OK, getStatus(requestGet1));
428
429        logger.debug("Person2 can see object " + publicObj);
430        final HttpGet requestGet2 = getObjMethod(idPublic);
431        setAuth(requestGet2, "person2");
432        assertEquals(HttpStatus.SC_OK, getStatus(requestGet2));
433
434        logger.debug("Person3 user cannot see object " + publicObj);
435        final HttpGet requestGet3 = getObjMethod(idPublic);
436        setAuth(requestGet3, "person3");
437        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet3));
438    }
439
440    /**
441     * Test cases to verify authorization with only acl:Append mode configured
442     * in the acl authorization of an resource.
443     * Tests:
444     *  1. Deny(403) on GET.
445     *  2. Allow(204) on PATCH.
446     *  3. Deny(403) on DELETE.
447     *  4. Deny(403) on PATCH with SPARQL DELETE statements.
448     *  5. Allow(400) on PATCH with empty SPARQL content.
449     *  6. Deny(403) on PATCH with non-SPARQL content.
450     *
451     * @throws IOException thrown from injestObj() or *ObjMethod() calls
452     */
453    @Test
454    public void scenario18Test1() throws IOException {
455        final String testObj = ingestObj("/rest/append_only_resource");
456        final String id = "/rest/append_only_resource/" + getRandomUniqueId();
457        ingestObj(id);
458
459        logger.debug("user18 can read (has ACL:READ): {}", id);
460        final HttpGet requestGet = getObjMethod(id);
461        setAuth(requestGet, "user18");
462        assertEquals(HttpStatus.SC_OK, getStatus(requestGet));
463
464        logger.debug("user18 can't append (no ACL): {}", id);
465        final HttpPatch requestPatch = patchObjMethod(id);
466        setAuth(requestPatch, "user18");
467        requestPatch.setHeader("Content-type", "application/sparql-update");
468        requestPatch.setEntity(new StringEntity("INSERT { <> <" + title.getURI() + "> \"Test title\" . } WHERE {}"));
469        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestPatch));
470
471        logger.debug("user18 can't delete (no ACL): {}", id);
472        final HttpDelete requestDelete = deleteObjMethod(id);
473        setAuth(requestDelete, "user18");
474        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestDelete));
475
476        ingestAcl("fedoraAdmin", "/acls/18/append-only-acl.ttl", testObj + "/fcr:acl");
477
478        logger.debug("user18 still can't read (ACL append): {}", id);
479        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet));
480
481        logger.debug("user18 can patch - SPARQL INSERTs (ACL append): {}", id);
482        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestPatch));
483
484        // Alter the Content-type to include a character set, to ensure correct matching.
485        requestPatch.setHeader("Content-type", "application/sparql-update; charset=UTF-8");
486        logger.debug("user18 can patch - SPARQL INSERTs (ACL append with charset): {}", id);
487        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestPatch));
488
489        logger.debug("user18 still can't delete (ACL append): {}", id);
490        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestDelete));
491
492        requestPatch.setEntity(new StringEntity("DELETE { <> <" + title.getURI() + "> \"Test title\" . } WHERE {}"));
493
494        logger.debug("user18 can not patch - SPARQL DELETEs (ACL append): {}", id);
495        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestPatch));
496
497        requestPatch.setEntity(null);
498
499        logger.debug("user18 can patch (is authorized, but bad request) - Empty SPARQL (ACL append): {}", id);
500        assertEquals(HttpStatus.SC_BAD_REQUEST, getStatus(requestPatch));
501
502        requestPatch.setHeader("Content-type", null);
503
504        logger.debug("user18 can not patch - Non SPARQL (ACL append): {}", id);
505        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestPatch));
506
507    }
508
509    /**
510     * Test cases to verify authorization with acl:Read and acl:Append modes
511     * configured in the acl authorization of an resource.
512     * Tests:
513     *  1. Allow(200) on GET.
514     *  2. Allow(204) on PATCH.
515     *  3. Deny(403) on DELETE.
516     *
517     * @throws IOException thrown from called functions within this function
518     */
519    @Test
520    public void scenario18Test2() throws IOException {
521        final String testObj = ingestObj("/rest/read_append_resource");
522
523        final String id = "/rest/read_append_resource/" + getRandomUniqueId();
524        ingestObj(id);
525
526        logger.debug("user18 can read (has ACL:READ): {}", id);
527        final HttpGet requestGet = getObjMethod(id);
528        setAuth(requestGet, "user18");
529        assertEquals(HttpStatus.SC_OK, getStatus(requestGet));
530
531        logger.debug("user18 can't append (no ACL): {}", id);
532        final HttpPatch requestPatch = patchObjMethod(id);
533        setAuth(requestPatch, "user18");
534        requestPatch.setHeader("Content-type", "application/sparql-update");
535        requestPatch.setEntity(new StringEntity(
536                "INSERT { <> <" + title.getURI() + "> \"some title\" . } WHERE {}"));
537        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestPatch));
538
539        ingestAcl("fedoraAdmin", "/acls/18/read-append-acl.ttl", testObj + "/fcr:acl");
540
541        logger.debug("user18 can't delete (no ACL): {}", id);
542        final HttpDelete requestDelete = deleteObjMethod(id);
543        setAuth(requestDelete, "user18");
544        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestDelete));
545
546        logger.debug("user18 can read (ACL read, append): {}", id);
547        assertEquals(HttpStatus.SC_OK, getStatus(requestGet));
548
549        logger.debug("user18 can append (ACL read, append): {}", id);
550        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestPatch));
551
552        logger.debug("user18 still can't delete (ACL read, append): {}", id);
553        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestDelete));
554    }
555
556    /**
557     * Test cases to verify authorization with acl:Read, acl:Append and
558     * acl:Write modes configured in the acl authorization of an resource.
559     * Tests:
560     *  1. Allow(200) on GET.
561     *  2. Allow(204) on PATCH.
562     *  3. Allow(204) on DELETE.
563     *
564     * @throws IOException from functions called from this function
565     */
566    @Test
567    public void scenario18Test3() throws IOException {
568        final String testObj = ingestObj("/rest/read_append_write_resource");
569
570        final String id = "/rest/read_append_write_resource/" + getRandomUniqueId();
571        ingestObj(id);
572
573        logger.debug("user18 can read (has ACL:READ): {}", id);
574        final HttpGet requestGet = getObjMethod(id);
575        setAuth(requestGet, "user18");
576        assertEquals(HttpStatus.SC_OK, getStatus(requestGet));
577
578        logger.debug("user18 can't append (no ACL): {}", id);
579        final HttpPatch requestPatch = patchObjMethod(id);
580        setAuth(requestPatch, "user18");
581        requestPatch.setHeader("Content-type", "application/sparql-update");
582        requestPatch.setEntity(new StringEntity(
583                "INSERT { <> <http://purl.org/dc/elements/1.1/title> \"some title\" . } WHERE {}"));
584        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestPatch));
585
586        logger.debug("user18 can't delete (no ACL): {}", id);
587        final HttpDelete requestDelete = deleteObjMethod(id);
588        setAuth(requestDelete, "user18");
589        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestDelete));
590
591        ingestAcl("fedoraAdmin", "/acls/18/read-append-write-acl.ttl", testObj + "/fcr:acl");
592
593        logger.debug("user18 can read (ACL read, append, write): {}", id);
594        assertEquals(HttpStatus.SC_OK, getStatus(requestGet));
595
596        logger.debug("user18 can append (ACL read, append, write): {}", id);
597        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestPatch));
598
599        logger.debug("user18 can delete (ACL read, append, write): {}", id);
600        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestDelete));
601    }
602
603    @Test
604    public void testAccessToRoot() throws IOException {
605        final String id = "/rest/" + getRandomUniqueId();
606        final String testObj = ingestObj(id);
607
608        logger.debug("Anonymous can read (has ACL:READ): {}", id);
609        final HttpGet requestGet1 = getObjMethod(id);
610        assertEquals(HttpStatus.SC_OK, getStatus(requestGet1));
611
612        logger.debug("Can username 'user06a' read {} (has ACL:READ)", id);
613        final HttpGet requestGet2 = getObjMethod(id);
614        setAuth(requestGet2, "user06a");
615        assertEquals(HttpStatus.SC_OK, getStatus(requestGet2));
616
617        logger.debug("Can username 'notuser06b' read {} (has ACL:READ)", id);
618        final HttpGet requestGet3 = getObjMethod(id);
619        setAuth(requestGet3, "user06b");
620        assertEquals(HttpStatus.SC_OK, getStatus(requestGet3));
621
622        authPropsConfig.setRootAuthAclPath(Paths.get("./target/test-classes/test-root-authorization2.ttl"));
623        logger.debug("Can username 'user06a' read {} (overridden system ACL)", id);
624        final HttpGet requestGet4 = getObjMethod(id);
625        setAuth(requestGet4, "user06a");
626        assertEquals(HttpStatus.SC_OK, getStatus(requestGet4));
627        authPropsConfig.setRootAuthAclPath(null);
628
629        // Add ACL to root
630        final String rootURI = getObjMethod("/rest").getURI().toString();
631        ingestAcl("fedoraAdmin", "/acls/06/acl.ttl", rootURI + "/fcr:acl");
632
633        logger.debug("Anonymous still can't read (ACL present)");
634        final HttpGet requestGet5 = getObjMethod(id);
635        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet5));
636
637        logger.debug("Can username 'user06a' read {} (ACL present)", testObj);
638        final HttpGet requestGet6 = getObjMethod(id);
639        setAuth(requestGet6, "user06a");
640        assertEquals(HttpStatus.SC_OK, getStatus(requestGet6));
641
642        logger.debug("Can username 'user06b' read {} (ACL present)", testObj);
643        final HttpGet requestGet7 = getObjMethod(id);
644        setAuth(requestGet7, "user06b");
645        assertEquals(HttpStatus.SC_OK, getStatus(requestGet7));
646    }
647
648    @Test
649    public void scenario21TestACLNotForInheritance() throws IOException {
650        final String parentPath = "/rest/resource_acl_no_inheritance";
651        // Ingest ACL with no acl:default statement to the parent resource
652        ingestObjWithACL(parentPath, "/acls/21/acl.ttl");
653
654        final String id = parentPath + "/" + getRandomUniqueId();
655        final String testObj = ingestObj(id);
656
657
658        // Test the parent ACL with no acl:default is applied for the parent resource authorization.
659        final HttpGet requestGet1 = getObjMethod(parentPath);
660        setAuth(requestGet1, "user21");
661        assertEquals("Agent user21 can't read resource " + parentPath + " with its own ACL!",
662                HttpStatus.SC_OK, getStatus(requestGet1));
663
664        final HttpGet requestGet2 = getObjMethod(id);
665        assertEquals("Agent user21 inherits read permission from parent ACL to read resource " + testObj + "!",
666                HttpStatus.SC_OK, getStatus(requestGet2));
667
668        // Test the default root ACL is inherited for authorization while the parent ACL with no acl:default is ignored
669        authPropsConfig.setRootAuthAclPath(Paths.get("./target/test-classes/test-root-authorization2.ttl"));
670        final HttpGet requestGet3 = getObjMethod(id);
671        setAuth(requestGet3, "user06a");
672        assertEquals("Agent user06a can't inherit read persmssion from root ACL to read resource " + testObj + "!",
673                HttpStatus.SC_OK, getStatus(requestGet3));
674    }
675
676    @Test
677    public void scenario22TestACLAuthorizationNotForInheritance() throws IOException {
678        final String parentPath = "/rest/resource_mix_acl_default";
679        final String parentObj = ingestObj(parentPath);
680
681        final String id = parentPath + "/" + getRandomUniqueId();
682        final String testObj = ingestObj(id);
683
684        // Ingest ACL with mix acl:default authorization to the parent resource
685        ingestAcl("fedoraAdmin", "/acls/22/acl.ttl", parentObj + "/fcr:acl");
686
687        // Test the parent ACL is applied for the parent resource authorization.
688        final HttpGet requestGet1 = getObjMethod(parentPath);
689        setAuth(requestGet1, "user22a");
690        assertEquals("Agent user22a can't read resource " + parentPath + " with its own ACL!",
691                HttpStatus.SC_OK, getStatus(requestGet1));
692
693        final HttpGet requestGet2 = getObjMethod(parentPath);
694        setAuth(requestGet2, "user22b");
695        assertEquals("Agent user22b can't read resource " + parentPath + " with its own ACL!",
696                HttpStatus.SC_OK, getStatus(requestGet1));
697
698        // Test the parent ACL is applied for the parent resource authorization.
699        final HttpGet requestGet3 = getObjMethod(id);
700        setAuth(requestGet3, "user22a");
701        assertEquals("Agent user22a inherits read permission from parent ACL to read resource " + testObj + "!",
702                HttpStatus.SC_FORBIDDEN, getStatus(requestGet3));
703
704        final HttpGet requestGet4 = getObjMethod(id);
705        setAuth(requestGet4, "user22b");
706        assertEquals("Agent user22b can't inherits read permission from parent ACL to read resource " + testObj + "!",
707                HttpStatus.SC_OK, getStatus(requestGet4));
708    }
709
710    @Test
711    public void testAccessToBinary() throws IOException {
712        // Block access to "book"
713        final String idBook = "/rest/book";
714        final String bookURI = ingestObj(idBook);
715
716        // Open access datastream, "file"
717        final String id = idBook + "/file";
718        final String testObj = ingestDatastream(idBook, "file");
719        ingestAcl("fedoraAdmin", "/acls/07/acl.ttl", bookURI + "/fcr:acl");
720
721        logger.debug("Anonymous can't read");
722        final HttpGet requestGet1 = getObjMethod(id);
723        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet1));
724
725        logger.debug("Can username 'user07' read {}", testObj);
726        final HttpGet requestGet2 = getObjMethod(id);
727
728        setAuth(requestGet2, "user07");
729        assertEquals(HttpStatus.SC_OK, getStatus(requestGet2));
730    }
731
732    @Test
733    public void testAccessToVersionedResources() throws IOException {
734        final String idVersion = "/rest/versionResource";
735        final String idVersionUri = ingestObj(idVersion);
736
737        final HttpPatch requestPatch1 = patchObjMethod(idVersion);
738        setAuth(requestPatch1, "fedoraAdmin");
739        requestPatch1.addHeader("Content-type", "application/sparql-update");
740        requestPatch1.setEntity(
741                new StringEntity("PREFIX pcdm: <http://pcdm.org/models#> INSERT { <> a pcdm:Object } WHERE {}"));
742        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestPatch1));
743
744        ingestAcl("fedoraAdmin", "/acls/10/acl.ttl", idVersionUri + "/fcr:acl");
745
746        final HttpGet requestGet1 = getObjMethod(idVersion);
747        setAuth(requestGet1, "user10");
748        assertEquals("user10 can't read object", HttpStatus.SC_OK, getStatus(requestGet1));
749
750        final HttpPost requestPost1 = postObjMethod(idVersion + "/fcr:versions");
751        setAuth(requestPost1, "fedoraAdmin");
752        assertEquals("Unable to create a new version", HttpStatus.SC_CREATED, getStatus(requestPost1));
753
754        final HttpGet requestGet2 = getObjMethod(idVersion);
755        setAuth(requestGet2, "user10");
756        assertEquals("user10 can't read versioned object", HttpStatus.SC_OK, getStatus(requestGet2));
757    }
758
759    @Test
760    public void testDelegatedUserAccess() throws IOException {
761        logger.debug("testing delegated authentication");
762        final String targetPath = "/rest/foo";
763        final String targetResource = ingestObj(targetPath);
764
765        ingestAcl("fedoraAdmin", "/acls/11/acl.ttl", targetResource + "/fcr:acl");
766
767        final HttpGet adminGet = getObjMethod(targetPath);
768        setAuth(adminGet, "fedoraAdmin");
769        assertEquals("admin can read object", HttpStatus.SC_OK, getStatus(adminGet));
770
771        final HttpGet adminDelegatedGet = getObjMethod(targetPath);
772        setAuth(adminDelegatedGet, "fedoraAdmin");
773        adminDelegatedGet.addHeader("On-Behalf-Of", "user11");
774        assertEquals("delegated user can read object", HttpStatus.SC_OK, getStatus(adminDelegatedGet));
775
776        final HttpGet adminUnauthorizedDelegatedGet = getObjMethod(targetPath);
777        setAuth(adminUnauthorizedDelegatedGet, "fedoraAdmin");
778        adminUnauthorizedDelegatedGet.addHeader("On-Behalf-Of", "fakeuser");
779        assertEquals("delegated fakeuser cannot read object", HttpStatus.SC_FORBIDDEN,
780                getStatus(adminUnauthorizedDelegatedGet));
781
782        final HttpGet adminDelegatedGet2 = getObjMethod(targetPath);
783        setAuth(adminDelegatedGet2, "fedoraAdmin");
784        adminDelegatedGet2.addHeader("On-Behalf-Of", "info:user/user2");
785        assertEquals("delegated user can read object", HttpStatus.SC_OK, getStatus(adminDelegatedGet2));
786
787        final HttpGet adminUnauthorizedDelegatedGet2 = getObjMethod(targetPath);
788        setAuth(adminUnauthorizedDelegatedGet2, "fedoraAdmin");
789        adminUnauthorizedDelegatedGet2.addHeader("On-Behalf-Of", "info:user/fakeuser");
790        assertEquals("delegated fakeuser cannot read object", HttpStatus.SC_FORBIDDEN,
791                getStatus(adminUnauthorizedDelegatedGet2));
792
793        // Now test with the system property in effect
794        rolesProvider.setUserBaseUri("info:user/");
795        rolesProvider.setGroupBaseUri("info:group/");
796
797        final HttpGet adminDelegatedGet3 = getObjMethod(targetPath);
798        setAuth(adminDelegatedGet3, "fedoraAdmin");
799        adminDelegatedGet3.addHeader("On-Behalf-Of", "info:user/user2");
800        assertEquals("delegated user can read object", HttpStatus.SC_OK, getStatus(adminDelegatedGet3));
801
802        final HttpGet adminUnauthorizedDelegatedGet3 = getObjMethod(targetPath);
803        setAuth(adminUnauthorizedDelegatedGet3, "fedoraAdmin");
804        adminUnauthorizedDelegatedGet3.addHeader("On-Behalf-Of", "info:user/fakeuser");
805        assertEquals("delegated fakeuser cannot read object", HttpStatus.SC_FORBIDDEN,
806                getStatus(adminUnauthorizedDelegatedGet3));
807    }
808
809    @Test
810    public void testAccessByUriToVersionedResources() throws IOException {
811        final String idVersionPath = "rest/versionResourceUri";
812        final String idVersionResource = ingestObj(idVersionPath);
813
814        ingestAcl("fedoraAdmin", "/acls/12/acl.ttl", idVersionResource + "/fcr:acl");
815
816        final HttpGet requestGet1 = getObjMethod(idVersionPath);
817        setAuth(requestGet1, "user12");
818        assertEquals("testuser can't read object", HttpStatus.SC_OK, getStatus(requestGet1));
819
820        final HttpPost requestPost1 = postObjMethod(idVersionPath + "/fcr:versions");
821        setAuth(requestPost1, "user12");
822        final String mementoLocation;
823        try (final CloseableHttpResponse response = execute(requestPost1)) {
824            assertEquals("Unable to create a new version", HttpStatus.SC_CREATED, getStatus(response));
825            mementoLocation = getLocation(response);
826        }
827
828        final HttpGet requestGet2 = new HttpGet(mementoLocation);
829        setAuth(requestGet2, "user12");
830        assertEquals("testuser can't read versioned object", HttpStatus.SC_OK, getStatus(requestGet2));
831    }
832
833    @Test
834    public void testAgentAsUri() throws IOException {
835        final String id = "/rest/" + getRandomUniqueId();
836        final String testObj = ingestObj(id);
837
838        logger.debug("Anonymous can read (has ACL:READ): {}", id);
839        final HttpGet requestGet1 = getObjMethod(id);
840        assertEquals(HttpStatus.SC_OK, getStatus(requestGet1));
841
842        logger.debug("Can username 'smith123' read {} (no ACL)", id);
843        final HttpGet requestGet2 = getObjMethod(id);
844        setAuth(requestGet2, "smith123");
845        assertEquals(HttpStatus.SC_OK, getStatus(requestGet2));
846
847        rolesProvider.setUserBaseUri("info:user/");
848        rolesProvider.setGroupBaseUri("info:group/");
849
850        logger.debug("Can username 'smith123' read {} (overridden system ACL)", id);
851        final HttpGet requestGet3 = getObjMethod(id);
852        setAuth(requestGet3, "smith123");
853        assertEquals(HttpStatus.SC_OK, getStatus(requestGet3));
854
855        logger.debug("Can username 'group123' read {} (overridden system ACL)", id);
856        final HttpGet requestGet4 = getObjMethod(id);
857        setAuth(requestGet4, "group123");
858        assertEquals(HttpStatus.SC_OK, getStatus(requestGet4));
859
860        rolesProvider.setUserBaseUri(null);
861        rolesProvider.setGroupBaseUri(null);
862
863        // Add ACL to object
864        ingestAcl("fedoraAdmin", "/acls/16/acl.ttl", testObj + "/fcr:acl");
865
866        logger.debug("Anonymous still can't read (ACL present)");
867        final HttpGet requestGet5 = getObjMethod(id);
868        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet5));
869
870        logger.debug("Can username 'smith123' read {} (ACL present, no system properties)", testObj);
871        final HttpGet requestGet6 = getObjMethod(id);
872        setAuth(requestGet6, "smith123");
873        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(requestGet6));
874
875        rolesProvider.setUserBaseUri("info:user/");
876        rolesProvider.setGroupBaseUri("info:group/");
877
878        logger.debug("Can username 'smith123' read {} (ACL, system properties present)", id);
879        final HttpGet requestGet7 = getObjMethod(id);
880        setAuth(requestGet7, "smith123");
881        assertEquals(HttpStatus.SC_OK, getStatus(requestGet7));
882
883        logger.debug("Can groupname 'group123' read {} (ACL, system properties present)", id);
884        final HttpGet requestGet8 = getObjMethod(id);
885        setAuth(requestGet8, "group123");
886        assertEquals(HttpStatus.SC_OK, getStatus(requestGet8));
887    }
888
889    @Test
890    public void testRegisterNamespace() throws IOException {
891        final String testObj = ingestObj("/rest/test_namespace");
892        ingestAcl("fedoraAdmin", "/acls/13/acl.ttl", testObj + "/fcr:acl");
893
894        final String id = "/rest/test_namespace/" + getRandomUniqueId();
895        ingestObj(id);
896
897        final HttpPatch patchReq = patchObjMethod(id);
898        setAuth(patchReq, "user13");
899        patchReq.addHeader("Content-type", "application/sparql-update");
900        patchReq.setEntity(new StringEntity("PREFIX novel: <info://" + getRandomUniqueId() + ">\n"
901                + "INSERT DATA { <> novel:value 'test' }"));
902        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchReq));
903    }
904
905    @Test
906    public void testRegisterNodeType() throws IOException {
907        final String testObj = ingestObj("/rest/test_nodetype");
908        ingestAcl("fedoraAdmin", "/acls/14/acl.ttl", testObj + "/fcr:acl");
909
910        final String id = "/rest/test_nodetype/" + getRandomUniqueId();
911        ingestObj(id);
912
913        final HttpPatch patchReq = patchObjMethod(id);
914        setAuth(patchReq, "user14");
915        patchReq.addHeader("Content-type", "application/sparql-update");
916        patchReq.setEntity(new StringEntity("PREFIX dc: <http://purl.org/dc/elements/1.1/>\n"
917                + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n"
918                + "INSERT DATA { <> rdf:type dc:type }"));
919        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchReq));
920    }
921
922
923    @Test
924    public void testDeletePropertyAsUser() throws IOException {
925        final String testObj = ingestObj("/rest/test_delete");
926        ingestAcl("fedoraAdmin", "/acls/15/acl.ttl", testObj + "/fcr:acl");
927
928        final String id = "/rest/test_delete/" + getRandomUniqueId();
929        ingestObj(id);
930
931        HttpPatch patchReq = patchObjMethod(id);
932        setAuth(patchReq, "user15");
933        patchReq.addHeader("Content-type", "application/sparql-update");
934        patchReq.setEntity(new StringEntity("PREFIX dc: <http://purl.org/dc/elements/1.1/>\n"
935                + "INSERT DATA { <> dc:title 'title' . " +
936                "                <> dc:rights 'rights' . }"));
937        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchReq));
938
939        patchReq = patchObjMethod(id);
940        setAuth(patchReq, "user15");
941        patchReq.addHeader("Content-type", "application/sparql-update");
942        patchReq.setEntity(new StringEntity("PREFIX dc: <http://purl.org/dc/elements/1.1/>\n"
943                + "DELETE { <> dc:title ?any . } WHERE { <> dc:title ?any . }"));
944        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchReq));
945
946        patchReq = patchObjMethod(id);
947        setAuth(patchReq, "notUser15");
948        patchReq.addHeader("Content-type", "application/sparql-update");
949        patchReq.setEntity(new StringEntity("PREFIX dc: <http://purl.org/dc/elements/1.1/>\n"
950                + "DELETE { <> dc:rights ?any . } WHERE { <> dc:rights ?any . }"));
951        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(patchReq));
952    }
953
954    @Test
955    public void testHeadWithReadOnlyUser() throws IOException {
956        final String testObj = ingestObj("/rest/test_head");
957        ingestAcl("fedoraAdmin", "/acls/19/acl.ttl", testObj + "/fcr:acl");
958
959        final HttpHead headReq = new HttpHead(testObj);
960        setAuth(headReq, "user19");
961        assertEquals(HttpStatus.SC_OK, getStatus(headReq));
962    }
963
964    @Test
965    public void testOptionsWithReadOnlyUser() throws IOException {
966        final String testObj = ingestObj("/rest/test_options");
967        ingestAcl("fedoraAdmin", "/acls/20/acl.ttl", testObj + "/fcr:acl");
968
969        final HttpOptions optionsReq = new HttpOptions(testObj);
970        setAuth(optionsReq, "user20");
971        assertEquals(HttpStatus.SC_OK, getStatus(optionsReq));
972    }
973
974    private static HttpResponse HEAD(final String requestURI) throws IOException {
975        return HEAD(requestURI, "fedoraAdmin");
976    }
977
978    private static HttpResponse HEAD(final String requestURI, final String username) throws IOException {
979        final HttpHead req = new HttpHead(requestURI);
980        setAuth(req, username);
981        return execute(req);
982    }
983
984    private static HttpResponse PUT(final String requestURI) throws IOException {
985        return PUT(requestURI, "fedoraAdmin");
986    }
987
988    private static HttpResponse PUT(final String requestURI, final String username) throws IOException {
989        final HttpPut req = new HttpPut(requestURI);
990        setAuth(req, username);
991        return execute(req);
992    }
993
994    private static HttpResponse DELETE(final String requestURI, final String username) throws IOException {
995        final HttpDelete req = new HttpDelete(requestURI);
996        setAuth(req, username);
997        return execute(req);
998    }
999
1000    private static HttpResponse GET(final String requestURI, final String username) throws IOException {
1001        final HttpGet req = new HttpGet(requestURI);
1002        setAuth(req, username);
1003        return execute(req);
1004    }
1005
1006    private static HttpResponse PATCH(final String requestURI, final HttpEntity body, final String username)
1007            throws IOException {
1008        final HttpPatch req = new HttpPatch(requestURI);
1009        setAuth(req, username);
1010        if (body != null) {
1011            req.setEntity(body);
1012        }
1013        return execute(req);
1014    }
1015
1016    private static String getLink(final HttpResponse res) {
1017        for (final Header h : res.getHeaders("Link")) {
1018            final HeaderElement link = h.getElements()[0];
1019            for (final NameValuePair param : link.getParameters()) {
1020                if (param.getName().equals("rel") && param.getValue().equals("acl")) {
1021                    return link.getName().replaceAll("^<|>$", "");
1022                }
1023            }
1024        }
1025        return null;
1026    }
1027
1028    private String ingestObjWithACL(final String path, final String aclResourcePath) throws IOException {
1029        final String newURI = ingestObj(path);
1030        final HttpResponse res = HEAD(newURI);
1031        final String aclURI = getLink(res);
1032
1033        logger.debug("Creating ACL at {}", aclURI);
1034        ingestAcl("fedoraAdmin", aclResourcePath, aclURI);
1035
1036        return newURI;
1037    }
1038
1039    @Test
1040    public void testControl() throws IOException {
1041        final String controlObj = ingestObjWithACL("/rest/control", "/acls/25/control.ttl");
1042        final String readwriteObj = ingestObjWithACL("/rest/readwrite", "/acls/25/readwrite.ttl");
1043
1044        final String rwChildACL = getLink(PUT(readwriteObj + "/child"));
1045        assertEquals(SC_FORBIDDEN, getStatus(HEAD(rwChildACL, "testuser")));
1046        assertEquals(SC_FORBIDDEN, getStatus(GET(rwChildACL, "testuser")));
1047        assertEquals(SC_FORBIDDEN, getStatus(PUT(rwChildACL, "testuser")));
1048        assertEquals(SC_FORBIDDEN, getStatus(DELETE(rwChildACL, "testuser")));
1049
1050        final String controlChildACL = getLink(PUT(controlObj + "/child"));
1051        assertEquals(SC_NOT_FOUND, getStatus(HEAD(controlChildACL, "testuser")));
1052        assertEquals(SC_NOT_FOUND, getStatus(GET(controlChildACL, "testuser")));
1053
1054        ingestAcl("testuser", "/acls/25/child-control.ttl", controlChildACL);
1055        final StringEntity sparqlUpdate = new StringEntity(
1056                "PREFIX acl: <http://www.w3.org/ns/auth/acl#>  INSERT { <#restricted> acl:mode acl:Read } WHERE { }",
1057                ContentType.create("application/sparql-update"));
1058        assertEquals(SC_NO_CONTENT, getStatus(PATCH(controlChildACL, sparqlUpdate, "testuser")));
1059
1060        assertEquals(SC_NO_CONTENT, getStatus(DELETE(controlChildACL, "testuser")));
1061    }
1062
1063    @Test
1064    public void testAppendOnlyToContainer() throws IOException {
1065        final String testObj = ingestObj("/rest/test_append");
1066        ingestAcl("fedoraAdmin", "/acls/23/acl.ttl", testObj + "/fcr:acl");
1067        final String username = "user23";
1068
1069        final HttpOptions optionsReq = new HttpOptions(testObj);
1070        setAuth(optionsReq, username);
1071        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(optionsReq));
1072
1073        final HttpHead headReq = new HttpHead(testObj);
1074        setAuth(headReq, username);
1075        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(headReq));
1076
1077        final HttpGet getReq = new HttpGet(testObj);
1078        setAuth(getReq, username);
1079        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(getReq));
1080
1081        final HttpPut putReq = new HttpPut(testObj);
1082        setAuth(putReq, username);
1083        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(putReq));
1084
1085        final HttpDelete deleteReq = new HttpDelete(testObj);
1086        setAuth(deleteReq, username);
1087        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(deleteReq));
1088
1089        final HttpPost postReq = new HttpPost(testObj);
1090        setAuth(postReq, username);
1091        assertEquals(HttpStatus.SC_CREATED, getStatus(postReq));
1092
1093        final String[] legalSPARQLQueries = new String[] {
1094            "INSERT DATA { <> <http://purl.org/dc/terms/title> \"Test23\" . }",
1095            "INSERT { <> <http://purl.org/dc/terms/alternative> \"Test XXIII\" . } WHERE {}",
1096            "DELETE {} INSERT { <> <http://purl.org/dc/terms/description> \"Test append only\" . } WHERE {}"
1097        };
1098        for (final String query : legalSPARQLQueries) {
1099            final HttpPatch patchReq = new HttpPatch(testObj);
1100            setAuth(patchReq, username);
1101            patchReq.setEntity(new StringEntity(query));
1102            patchReq.setHeader("Content-Type", "application/sparql-update");
1103            logger.debug("Testing SPARQL update: {}", query);
1104            assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchReq));
1105        }
1106
1107        final String[] illegalSPARQLQueries = new String[] {
1108            "DELETE DATA { <> <http://purl.org/dc/terms/title> \"Test23\" . }",
1109            "DELETE { <> <http://purl.org/dc/terms/alternative> \"Test XXIII\" . } WHERE {}",
1110            "DELETE { <> <http://purl.org/dc/terms/description> \"Test append only\" . } INSERT {} WHERE {}"
1111        };
1112        for (final String query : illegalSPARQLQueries) {
1113            final HttpPatch patchReq = new HttpPatch(testObj);
1114            setAuth(patchReq, username);
1115            patchReq.setEntity(new StringEntity(query));
1116            patchReq.setHeader("Content-Type", "application/sparql-update");
1117            logger.debug("Testing SPARQL update: {}", query);
1118            assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(patchReq));
1119        }
1120        final String[] allowedDeleteSPARQLQueries = new String[] {
1121            "DELETE DATA {}",
1122            "DELETE { } WHERE {}",
1123            "DELETE { } INSERT {} WHERE {}"
1124        };
1125        for (final String query : allowedDeleteSPARQLQueries) {
1126            final HttpPatch patchReq = new HttpPatch(testObj);
1127            setAuth(patchReq, username);
1128            patchReq.setEntity(new StringEntity(query));
1129            patchReq.setHeader("Content-Type", "application/sparql-update");
1130            logger.debug("Testing SPARQL update: {}", query);
1131            assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchReq));
1132        }
1133
1134    }
1135
1136    @Test
1137    public void testAppendOnlyToBinary() throws IOException {
1138        final String testObj = ingestBinary("/rest/test_append_binary", new StringEntity("foo"));
1139        ingestAcl("fedoraAdmin", "/acls/24/acl.ttl", testObj + "/fcr:acl");
1140        final String username = "user24";
1141
1142        final HttpOptions optionsReq = new HttpOptions(testObj);
1143        setAuth(optionsReq, username);
1144        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(optionsReq));
1145
1146        final HttpHead headReq = new HttpHead(testObj);
1147        setAuth(headReq, username);
1148        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(headReq));
1149
1150        final HttpGet getReq = new HttpGet(testObj);
1151        setAuth(getReq, username);
1152        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(getReq));
1153
1154        final HttpPut putReq = new HttpPut(testObj);
1155        setAuth(putReq, username);
1156        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(putReq));
1157
1158        final HttpDelete deleteReq = new HttpDelete(testObj);
1159        setAuth(deleteReq, username);
1160        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(deleteReq));
1161
1162        final HttpPost postReq = new HttpPost(testObj);
1163        setAuth(postReq, username);
1164        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(postReq));
1165    }
1166
1167    @Test
1168    public void testFoafAgent() throws IOException {
1169        final String path = ingestObj("/rest/foaf-agent");
1170        ingestAcl("fedoraAdmin", "/acls/26/foaf-agent.ttl", path + "/fcr:acl");
1171        final String username = "user1";
1172
1173        final HttpGet req = new HttpGet(path);
1174
1175        //NB: Actually no authentication headers should be set for this test
1176        //since the point of foaf:Agent is to allow unauthenticated access for everyone.
1177        //However at this time the test integration test server requires callers to
1178        //authenticate.
1179        setAuth(req, username);
1180
1181        assertEquals(HttpStatus.SC_OK, getStatus(req));
1182    }
1183
1184    @Test
1185    public void testAuthenticatedAgent() throws IOException {
1186        final String path = ingestObj("/rest/authenticated-agent");
1187        ingestAcl("fedoraAdmin", "/acls/26/authenticated-agent.ttl", path + "/fcr:acl");
1188        final String username = "user1";
1189
1190        final HttpGet darkReq = new HttpGet(path);
1191        setAuth(darkReq, username);
1192        assertEquals(HttpStatus.SC_OK, getStatus(darkReq));
1193    }
1194
1195    @Test
1196    public void testAgentGroupWithHashUris() throws Exception {
1197        ingestTurtleResource("fedoraAdmin", "/acls/agent-group-list.ttl",
1198                             serverAddress + "/rest/agent-group-list");
1199        //check that the authorized are authorized.
1200        final String authorized = ingestObj("/rest/agent-group-with-hash-uri-authorized");
1201        ingestAcl("fedoraAdmin", "/acls/agent-group-with-hash-uri-authorized.ttl", authorized + "/fcr:acl");
1202
1203        final HttpGet getAuthorized = new HttpGet(authorized);
1204        setAuth(getAuthorized, "testuser");
1205        assertEquals(HttpStatus.SC_OK, getStatus(getAuthorized));
1206
1207        //check that the unauthorized are unauthorized.
1208        final String unauthorized = ingestObj("/rest/agent-group-with-hash-uri-unauthorized");
1209        ingestAcl("fedoraAdmin", "/acls/agent-group-with-hash-uri-unauthorized.ttl", unauthorized + "/fcr:acl");
1210
1211        final HttpGet getUnauthorized = new HttpGet(unauthorized);
1212        setAuth(getUnauthorized, "testuser");
1213        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(getUnauthorized));
1214    }
1215
1216    @Test
1217    public void testAgentGroupWithMembersAsURIs() throws Exception {
1218        rolesProvider.setUserBaseUri("http://example.com/");
1219        ingestTurtleResource("fedoraAdmin", "/acls/agent-group-list-with-member-uris.ttl",
1220                             serverAddress + "/rest/agent-group-list-with-member-uris");
1221        final String authorized = ingestObj("/rest/agent-group-with-vcard-member-as-uri");
1222        ingestAcl("fedoraAdmin", "/acls/agent-group-with-vcard-member-as-uri.ttl", authorized + "/fcr:acl");
1223        //check that test user is authorized to write
1224        final HttpPut childPut = new HttpPut(authorized + "/child");
1225        setAuth(childPut, "testuser");
1226        assertEquals(HttpStatus.SC_CREATED, getStatus(childPut));
1227    }
1228
1229    @Test
1230    public void testAgentGroup() throws Exception {
1231        ingestTurtleResource("fedoraAdmin", "/acls/agent-group-list-flat.ttl",
1232                             serverAddress + "/rest/agent-group-list-flat");
1233        //check that the authorized are authorized.
1234        final String flat = ingestObj("/rest/agent-group-flat");
1235        ingestAcl("fedoraAdmin", "/acls/agent-group-flat.ttl", flat + "/fcr:acl");
1236
1237        final HttpGet getFlat = new HttpGet(flat);
1238        setAuth(getFlat, "testuser");
1239        assertEquals(HttpStatus.SC_OK, getStatus(getFlat));
1240    }
1241
1242    @Test
1243    public void testAclAppendPermissions() throws Exception {
1244        final String testObj = ingestBinary("/rest/test-read-append", new StringEntity("foo"));
1245        ingestAcl("fedoraAdmin", "/acls/27/read-append.ttl", testObj + "/fcr:acl");
1246        final String username = "user27";
1247
1248        final HttpOptions optionsReq = new HttpOptions(testObj);
1249        setAuth(optionsReq, username);
1250        assertEquals(HttpStatus.SC_OK, getStatus(optionsReq));
1251
1252        final HttpHead headReq = new HttpHead(testObj);
1253        setAuth(headReq, username);
1254        assertEquals(HttpStatus.SC_OK, getStatus(headReq));
1255
1256        final HttpGet getReq = new HttpGet(testObj);
1257        setAuth(getReq, username);
1258        final String descriptionUri;
1259        try (final CloseableHttpResponse response = execute(getReq)) {
1260            assertEquals(HttpStatus.SC_OK, getStatus(response));
1261            descriptionUri = Arrays.stream(response.getHeaders("Link"))
1262                    .flatMap(header -> Arrays.stream(header.getValue().split(","))).map(linkStr -> Link.valueOf(
1263                            linkStr))
1264                    .filter(link -> link.getRels().contains("describedby")).map(link -> link.getUri().toString())
1265                    .findFirst().orElse(null);
1266        }
1267
1268
1269        final HttpPut putReq = new HttpPut(testObj);
1270        setAuth(putReq, username);
1271        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(putReq));
1272
1273        final HttpDelete deleteReq = new HttpDelete(testObj);
1274        setAuth(deleteReq, username);
1275        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(deleteReq));
1276
1277        final HttpPost postReq = new HttpPost(testObj);
1278        setAuth(postReq, username);
1279        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(postReq));
1280
1281        if (descriptionUri != null) {
1282            final HttpOptions optionsDescReq = new HttpOptions(descriptionUri);
1283            setAuth(optionsDescReq, username);
1284            assertEquals(HttpStatus.SC_OK, getStatus(optionsDescReq));
1285
1286            final HttpHead headDescReq = new HttpHead(descriptionUri);
1287            setAuth(headDescReq, username);
1288            assertEquals(HttpStatus.SC_OK, getStatus(headDescReq));
1289
1290            final HttpGet getDescReq = new HttpGet(descriptionUri);
1291            setAuth(getDescReq, username);
1292            assertEquals(HttpStatus.SC_OK, getStatus(getDescReq));
1293
1294            final HttpPut putDescReq = new HttpPut(descriptionUri);
1295            setAuth(putDescReq, username);
1296            assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(putDescReq));
1297
1298            final HttpDelete deleteDescReq = new HttpDelete(descriptionUri);
1299            setAuth(deleteDescReq, username);
1300            assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(deleteDescReq));
1301
1302            final HttpPost postDescReq = new HttpPost(descriptionUri);
1303            setAuth(postDescReq, username);
1304            assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(postDescReq));
1305        }
1306    }
1307
1308    @Test
1309    public void testCreateAclWithAccessToClassForBinary() throws Exception {
1310        final String id = getRandomUniqueId();
1311        final String subjectUri = serverAddress + id;
1312        ingestObj(subjectUri);
1313        ingestAcl("fedoraAdmin", "/acls/agent-access-to-class.ttl", subjectUri + "/fcr:acl");
1314
1315        final String binaryUri = ingestBinary("/rest/" + id + "/binary", new StringEntity("foo"));
1316
1317        final HttpHead headBinary = new HttpHead(binaryUri);
1318        setAuth(headBinary, "testuser");
1319        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(headBinary));
1320
1321        final HttpHead headDesc = new HttpHead(binaryUri + "/fcr:metadata");
1322        setAuth(headDesc, "testuser");
1323        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(headDesc));
1324
1325        // Add type to binary
1326        final HttpPatch requestPatch = patchObjMethod(id + "/binary/fcr:metadata");
1327        setAuth(requestPatch, "fedoraAdmin");
1328        final String sparql = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
1329                "PREFIX foaf: <http://xmlns.com/foaf/0.1/>  \n" +
1330                "INSERT { <> rdf:type foaf:Document } WHERE {}";
1331        requestPatch.setEntity(new StringEntity(sparql));
1332        requestPatch.setHeader("Content-type", "application/sparql-update");
1333        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(requestPatch));
1334
1335        final HttpHead headBinary2 = new HttpHead(binaryUri);
1336        setAuth(headBinary2, "testuser");
1337        assertEquals(HttpStatus.SC_OK, getStatus(headBinary2));
1338
1339        final HttpHead headDesc2 = new HttpHead(binaryUri + "/fcr:metadata");
1340        setAuth(headDesc2, "testuser");
1341        assertEquals(HttpStatus.SC_OK, getStatus(headDesc2));
1342    }
1343
1344    @Test
1345    public void testIndirectRelationshipForbidden() throws IOException {
1346        final String targetResource = "/rest/" + getRandomUniqueId();
1347        final String writeableResource = "/rest/" + getRandomUniqueId();
1348        final String username = "user28";
1349
1350        final String targetUri = ingestObj(targetResource);
1351
1352        final String readonlyString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1353                "<#readauthz> a acl:Authorization ;\n" +
1354                "   acl:agent \"" + username + "\" ;\n" +
1355                "   acl:mode acl:Read ;\n" +
1356                "   acl:accessTo <" + targetResource + "> .";
1357        ingestAclString(targetUri, readonlyString, "fedoraAdmin");
1358
1359        // User can read target resource.
1360        final HttpGet get1 = getObjMethod(targetResource);
1361        setAuth(get1, username);
1362        assertEquals(HttpStatus.SC_OK, getStatus(get1));
1363
1364        // User can't patch target resource.
1365        final String patch = "INSERT DATA { <> <http://purl.org/dc/elements/1.1/title> \"Changed it\"}";
1366        final HttpEntity patchEntity = new StringEntity(patch, sparqlContentType);
1367        try (final CloseableHttpResponse resp = (CloseableHttpResponse) PATCH(targetUri, patchEntity,
1368                username)) {
1369            assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(resp));
1370        }
1371
1372        // Make a user writable container.
1373        final String writeableUri = ingestObj(writeableResource);
1374        final String writeableAcl = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1375                "<#writeauth> a acl:Authorization ;\n" +
1376                "   acl:agent \"" + username + "\" ;\n" +
1377                "   acl:mode acl:Read, acl:Write ;\n" +
1378                "   acl:accessTo <" + writeableResource + "> ;\n" +
1379                "   acl:default <" + writeableResource + "> .";
1380        ingestAclString(writeableUri, writeableAcl, "fedoraAdmin");
1381
1382        // Ensure we can still POST/PUT to writeable resource.
1383        testCanWrite(writeableResource, username);
1384
1385        // Try to create indirect container referencing readonly resource with POST.
1386        final HttpPost userPost = postObjMethod(writeableResource);
1387        setAuth(userPost, username);
1388        userPost.addHeader("Link", "<" + INDIRECT_CONTAINER.toString() + ">; rel=type");
1389        final String indirect = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1390                "@prefix example: <http://www.example.org/example1#> .\n" +
1391                "@prefix dc: <http://purl.org/dc/elements/1.1/> .\n" +
1392                "<> ldp:insertedContentRelation <http://example.org/test#something> ;\n" +
1393                "ldp:membershipResource <" + targetResource + "> ;\n" +
1394                "ldp:hasMemberRelation <http://example.org/test#predicateToCreate> ;\n" +
1395                "dc:title \"The indirect container\" .";
1396        final HttpEntity indirectEntity = new StringEntity(indirect, turtleContentType);
1397        userPost.setEntity(indirectEntity);
1398        userPost.setHeader(CONTENT_TYPE, "text/turtle");
1399        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(userPost));
1400
1401        // Try to create indirect container referencing readonly resource with PUT.
1402        final String indirectString = getRandomUniqueId();
1403        final HttpPut userPut = putObjMethod(writeableResource + "/" + indirectString);
1404        setAuth(userPut, username);
1405        userPut.addHeader("Link", "<" + INDIRECT_CONTAINER.toString() + ">; rel=type");
1406        userPut.setEntity(indirectEntity);
1407        userPut.setHeader(CONTENT_TYPE, "text/turtle");
1408        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(userPut));
1409
1410        // Create an user writeable resource.
1411        final HttpPost targetPost = postObjMethod(writeableResource);
1412        setAuth(targetPost, username);
1413        final String tempTarget;
1414        try (final CloseableHttpResponse resp = execute(targetPost)) {
1415            assertEquals(HttpStatus.SC_CREATED, getStatus(resp));
1416            tempTarget = getLocation(resp);
1417        }
1418
1419        // Try to create indirect container referencing an available resource.
1420        final String indirect_ok = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1421                "@prefix example: <http://www.example.org/example1#> .\n" +
1422                "@prefix dc: <http://purl.org/dc/elements/1.1/> .\n" +
1423                "<> ldp:insertedContentRelation <http://example.org/test#something> ;\n" +
1424                "ldp:membershipResource <" + tempTarget + "> ;\n" +
1425                "ldp:hasMemberRelation <http://example.org/test#predicateToCreate> ;\n" +
1426                "dc:title \"The indirect container\" .";
1427        final HttpPost userPatchPost = postObjMethod(writeableResource);
1428        setAuth(userPatchPost, username);
1429        userPatchPost.addHeader("Link", "<" + INDIRECT_CONTAINER.toString() + ">; rel=type");
1430        final HttpEntity in_ok = new StringEntity(indirect_ok, turtleContentType);
1431        userPatchPost.setEntity(in_ok);
1432        userPatchPost.setHeader(CONTENT_TYPE, "text/turtle");
1433        final String indirectUri;
1434        try (final CloseableHttpResponse resp = execute(userPatchPost)) {
1435            assertEquals(HttpStatus.SC_CREATED, getStatus(resp));
1436            indirectUri = getLocation(resp);
1437        }
1438
1439        // Then PATCH to the readonly resource.
1440        final HttpPatch patchIndirect = new HttpPatch(indirectUri);
1441        setAuth(patchIndirect, username);
1442        final String patch_text = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1443                "DELETE { <> ldp:membershipResource ?o } \n" +
1444                "INSERT { <> ldp:membershipResource <" + targetResource + "> } \n" +
1445                "WHERE { <> ldp:membershipResource ?o }";
1446        patchIndirect.setEntity(new StringEntity(patch_text, sparqlContentType));
1447        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(patchIndirect));
1448
1449        // Delete the ldp:membershipRelation and add it with INSERT DATA {}
1450        final String patch_delete_relation = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1451                "DELETE DATA { <> ldp:membershipResource <" + tempTarget + "> }";
1452        final HttpPatch patchIndirect2 = new HttpPatch(indirectUri);
1453        setAuth(patchIndirect2, username);
1454        patchIndirect2.setEntity(new StringEntity(patch_delete_relation, sparqlContentType));
1455        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchIndirect2));
1456
1457        final String patch_insert_relation = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1458                "INSERT DATA { <> ldp:membershipResource <" + targetResource + "> }";
1459        final HttpPatch patchIndirect3 = new HttpPatch(indirectUri);
1460        setAuth(patchIndirect3, username);
1461        patchIndirect3.setEntity(new StringEntity(patch_insert_relation, sparqlContentType));
1462        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(patchIndirect3));
1463
1464        // Patch the indirect to the readonly target as admin
1465        final HttpPatch patchAsAdmin = new HttpPatch(indirectUri);
1466        setAuth(patchAsAdmin, "fedoraAdmin");
1467        patchAsAdmin.setEntity(new StringEntity(patch_text, sparqlContentType));
1468        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchAsAdmin));
1469
1470        // Ensure the patching happened.
1471        final HttpGet verifyGet = new HttpGet(indirectUri);
1472        setAuth(verifyGet, "fedoraAdmin");
1473        try (final CloseableHttpResponse response = execute(verifyGet)) {
1474            final CloseableDataset dataset = getDataset(response);
1475            final DatasetGraph graph = dataset.asDatasetGraph();
1476            assertTrue("Can't find " + targetUri + " in graph",
1477                    graph.contains(
1478                            Node.ANY,
1479                            NodeFactory.createURI(indirectUri),
1480                            MEMBERSHIP_RESOURCE.asNode(),
1481                            NodeFactory.createURI(targetUri)
1482                    )
1483            );
1484        }
1485
1486        // Try to POST a child as user
1487        final HttpPost postChild = new HttpPost(indirectUri);
1488        final String postTarget = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1489                "@prefix test: <http://example.org/test#> .\n\n" +
1490                "<> test:something <" + tempTarget + "> .";
1491        final HttpEntity putPostChild = new StringEntity(postTarget, turtleContentType);
1492        setAuth(postChild, username);
1493        postChild.setEntity(putPostChild);
1494        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(postChild));
1495
1496        // Try to PUT a child as user
1497        final String id = getRandomUniqueId();
1498        final HttpPut putChild = new HttpPut(indirectUri + "/" + id);
1499        setAuth(putChild, username);
1500        putChild.setEntity(putPostChild);
1501        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(putChild));
1502
1503        // Put the child as Admin
1504        setAuth(putChild, "fedoraAdmin");
1505        assertEquals(HttpStatus.SC_CREATED, getStatus(putChild));
1506
1507        // Try to delete the child as user
1508        final HttpDelete deleteChild = new HttpDelete(indirectUri + "/" + id);
1509        setAuth(deleteChild, username);
1510        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(deleteChild));
1511
1512        // Try to delete the indirect container
1513        final HttpDelete deleteIndirect = new HttpDelete(indirectUri);
1514        setAuth(deleteIndirect, username);
1515        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(deleteIndirect));
1516
1517        // Ensure we can still write to the writeable resource.
1518        testCanWrite(writeableResource, username);
1519
1520    }
1521
1522    @Test
1523    public void testIndirectRelationshipOK() throws IOException {
1524        final String targetResource = "/rest/" + getRandomUniqueId();
1525        final String writeableResource = "/rest/" + getRandomUniqueId();
1526        final String username = "user28";
1527
1528        final String targetUri = ingestObj(targetResource);
1529
1530        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1531                "<#readauthz> a acl:Authorization ;\n" +
1532                "   acl:agent \"" + username + "\" ;\n" +
1533                "   acl:mode acl:Read, acl:Write ;\n" +
1534                "   acl:accessTo <" + targetResource + "> .";
1535        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
1536
1537        // User can read target resource.
1538        final HttpGet get1 = getObjMethod(targetResource);
1539        setAuth(get1, username);
1540        assertEquals(HttpStatus.SC_OK, getStatus(get1));
1541
1542        // User can patch target resource.
1543        final String patch = "INSERT DATA { <> <http://purl.org/dc/elements/1.1/title> \"Changed it\"}";
1544        final HttpEntity patchEntity = new StringEntity(patch, sparqlContentType);
1545        try (final CloseableHttpResponse resp = (CloseableHttpResponse) PATCH(targetUri, patchEntity,
1546                username)) {
1547            assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(resp));
1548        }
1549
1550        // Make a user writable container.
1551        final String writeableUri = ingestObj(writeableResource);
1552        final String writeableAcl = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1553                "<#writeauth> a acl:Authorization ;\n" +
1554                "   acl:agent \"" + username + "\" ;\n" +
1555                "   acl:mode acl:Read, acl:Write ;\n" +
1556                "   acl:accessTo <" + writeableResource + "> ;\n" +
1557                "   acl:default <" + writeableResource + "> .";
1558        ingestAclString(writeableUri, writeableAcl, "fedoraAdmin");
1559
1560        // Ensure we can write to the writeable resource.
1561        testCanWrite(writeableResource, username);
1562
1563        // Try to create indirect container referencing writeable resource with POST.
1564        final HttpPost userPost = postObjMethod(writeableResource);
1565        setAuth(userPost, username);
1566        userPost.addHeader("Link", "<" + INDIRECT_CONTAINER.toString() + ">; rel=type");
1567        final String indirect = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1568                "@prefix test: <http://example.org/test#> .\n\n" +
1569                "<> ldp:insertedContentRelation test:something ;" +
1570                "ldp:membershipResource <" + targetResource + "> ;" +
1571                "ldp:hasMemberRelation test:predicateToCreate .";
1572        final HttpEntity indirectEntity = new StringEntity(indirect, turtleContentType);
1573        userPost.setEntity(new StringEntity(indirect, turtleContentType));
1574        userPost.setHeader("Content-type", "text/turtle");
1575        assertEquals(HttpStatus.SC_CREATED, getStatus(userPost));
1576
1577        // Try to create indirect container referencing writeable resource with PUT.
1578        final String indirectString = getRandomUniqueId();
1579        final HttpPut userPut = putObjMethod(writeableResource + "/" + indirectString);
1580        setAuth(userPut, username);
1581        userPut.addHeader("Link", "<" + INDIRECT_CONTAINER.toString() + ">; rel=type");
1582        userPut.setEntity(indirectEntity);
1583        userPut.setHeader("Content-type", "text/turtle");
1584        assertEquals(HttpStatus.SC_CREATED, getStatus(userPut));
1585
1586        // Create an user writeable resource.
1587        final HttpPost targetPost = postObjMethod(writeableResource);
1588        setAuth(targetPost, username);
1589        final String tempTarget;
1590        try (final CloseableHttpResponse resp = execute(targetPost)) {
1591            assertEquals(HttpStatus.SC_CREATED, getStatus(resp));
1592            tempTarget = getLocation(resp);
1593        }
1594
1595        // Try to create indirect container referencing an available resource.
1596        final String indirect_ok = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1597                "@prefix test: <http://example.org/test#> .\n\n" +
1598                "<> ldp:insertedContentRelation test:something ;" +
1599                "ldp:membershipResource <" + tempTarget + "> ;" +
1600                "ldp:hasMemberRelation test:predicateToCreate .";
1601        final HttpPost userPatchPost = postObjMethod(writeableResource);
1602        setAuth(userPatchPost, username);
1603        userPatchPost.addHeader("Link", "<" + INDIRECT_CONTAINER.toString() + ">; rel=type");
1604        userPatchPost.setEntity(new StringEntity(indirect_ok, turtleContentType));
1605        userPatchPost.setHeader("Content-type", "text/turtle");
1606        final String indirectUri;
1607        try (final CloseableHttpResponse resp = execute(userPatchPost)) {
1608            assertEquals(HttpStatus.SC_CREATED, getStatus(resp));
1609            indirectUri = getLocation(resp);
1610        }
1611
1612        // Then PATCH to the writeable resource.
1613        final HttpPatch patchIndirect = new HttpPatch(indirectUri);
1614        setAuth(patchIndirect, username);
1615        final String patch_text = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1616                "DELETE { <> ldp:membershipResource ?o } \n" +
1617                "INSERT { <> ldp:membershipResource <" + targetResource + "> } \n" +
1618                "WHERE { <> ldp:membershipResource ?o }";
1619        patchIndirect.setEntity(new StringEntity(patch_text, sparqlContentType));
1620        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchIndirect));
1621
1622        // Delete the ldp:membershipRelation and add it back
1623        final String patch_delete_relation = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1624                "DELETE DATA { <> ldp:membershipResource <" + targetResource + "> }";
1625        final HttpPatch patchIndirect2 = new HttpPatch(indirectUri);
1626        setAuth(patchIndirect2, username);
1627        patchIndirect2.setEntity(new StringEntity(patch_delete_relation, sparqlContentType));
1628        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchIndirect2));
1629
1630        // Cannot insert membershipResource without deleting the default value
1631        final String patch_insert_relation = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1632                "DELETE { <> ldp:membershipResource ?o } \n" +
1633                "INSERT { <> ldp:membershipResource <" + targetResource + "> } \n" +
1634                "WHERE { <> ldp:membershipResource ?o }";
1635        final HttpPatch patchIndirect3 = new HttpPatch(indirectUri);
1636        setAuth(patchIndirect3, username);
1637        patchIndirect3.setEntity(new StringEntity(patch_insert_relation, sparqlContentType));
1638        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchIndirect3));
1639
1640        // Ensure we can still write to the writeable resource.
1641        testCanWrite(writeableResource, username);
1642
1643    }
1644
1645    @Test
1646    public void testDirectRelationshipForbidden() throws IOException {
1647        final String targetResource = "/rest/" + getRandomUniqueId();
1648        final String writeableResource = "/rest/" + getRandomUniqueId();
1649        final String username = "user28";
1650
1651        final String targetUri = ingestObj(targetResource);
1652
1653        final String readonlyString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1654                "<#readauthz> a acl:Authorization ;\n" +
1655                "   acl:agent \"" + username + "\" ;\n" +
1656                "   acl:mode acl:Read ;\n" +
1657                "   acl:accessTo <" + targetResource + "> .";
1658        ingestAclString(targetUri, readonlyString, "fedoraAdmin");
1659
1660        // User can read target resource.
1661        final HttpGet get1 = getObjMethod(targetResource);
1662        setAuth(get1, username);
1663        assertEquals(HttpStatus.SC_OK, getStatus(get1));
1664
1665        // User can't patch target resource.
1666        final String patch = "INSERT DATA { <> <http://purl.org/dc/elements/1.1/title> \"Changed it\"}";
1667        final HttpEntity patchEntity = new StringEntity(patch, sparqlContentType);
1668        try (final CloseableHttpResponse resp = (CloseableHttpResponse) PATCH(targetUri, patchEntity,
1669                username)) {
1670            assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(resp));
1671        }
1672
1673        // Make a user writable container.
1674        final String writeableUri = ingestObj(writeableResource);
1675        final String writeableAcl = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1676                "<#writeauth> a acl:Authorization ;\n" +
1677                "   acl:agent \"" + username + "\" ;\n" +
1678                "   acl:mode acl:Read, acl:Write ;\n" +
1679                "   acl:accessTo <" + writeableResource + "> ;\n" +
1680                "   acl:default <" + writeableResource + "> .";
1681        ingestAclString(writeableUri, writeableAcl, "fedoraAdmin");
1682
1683        // Ensure we can write to writeable resource.
1684        testCanWrite(writeableResource, username);
1685
1686        // Try to create direct container referencing readonly resource with POST.
1687        final HttpPost userPost = postObjMethod(writeableResource);
1688        setAuth(userPost, username);
1689        userPost.addHeader("Link", "<" + DIRECT_CONTAINER.toString() + ">; rel=type");
1690        final String direct = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1691                "@prefix test: <http://example.org/test#> .\n\n" +
1692                "<> ldp:membershipResource <" + targetResource + "> ;" +
1693                "ldp:hasMemberRelation test:predicateToCreate .";
1694        final HttpEntity directEntity = new StringEntity(direct, turtleContentType);
1695        userPost.setEntity(directEntity);
1696        userPost.setHeader("Content-type", "text/turtle");
1697        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(userPost));
1698
1699        // Try to create direct container referencing readonly resource with PUT.
1700        final String indirectString = getRandomUniqueId();
1701        final HttpPut userPut = putObjMethod(writeableResource + "/" + indirectString);
1702        setAuth(userPut, username);
1703        userPut.addHeader("Link", "<" + DIRECT_CONTAINER.toString() + ">; rel=type");
1704        userPut.setEntity(directEntity);
1705        userPut.setHeader("Content-type", "text/turtle");
1706        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(userPut));
1707
1708        // Create an user writeable resource.
1709        final HttpPost targetPost = postObjMethod(writeableResource);
1710        setAuth(targetPost, username);
1711        final String tempTarget;
1712        try (final CloseableHttpResponse resp = execute(targetPost)) {
1713            assertEquals(HttpStatus.SC_CREATED, getStatus(resp));
1714            tempTarget = getLocation(resp);
1715        }
1716
1717        // Try to create direct container referencing an available resource.
1718        final String direct_ok = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1719                "@prefix test: <http://example.org/test#> .\n\n" +
1720                "<> ldp:membershipResource <" + tempTarget + "> ;\n" +
1721                "ldp:hasMemberRelation test:predicateToCreate .";
1722        final HttpPost userPatchPost = postObjMethod(writeableResource);
1723        setAuth(userPatchPost, username);
1724        userPatchPost.addHeader("Link", "<" + DIRECT_CONTAINER.toString() + ">; rel=type");
1725        userPatchPost.setEntity(new StringEntity(direct_ok, turtleContentType));
1726        userPatchPost.setHeader("Content-type", "text/turtle");
1727        final String directUri;
1728        try (final CloseableHttpResponse resp = execute(userPatchPost)) {
1729            assertEquals(HttpStatus.SC_CREATED, getStatus(resp));
1730            directUri = getLocation(resp);
1731        }
1732
1733        // Then PATCH to the readonly resource.
1734        final HttpPatch patchDirect = new HttpPatch(directUri);
1735        setAuth(patchDirect, username);
1736        final String patch_text = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1737                "DELETE { <> ldp:membershipResource ?o } \n" +
1738                "INSERT { <> ldp:membershipResource <" + targetResource + "> } \n" +
1739                "WHERE { <> ldp:membershipResource ?o }";
1740        patchDirect.setEntity(new StringEntity(patch_text, sparqlContentType));
1741        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(patchDirect));
1742
1743        // Delete the ldp:membershipRelation and add it with INSERT DATA {}
1744        final String patch_delete_relation = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1745                "DELETE DATA { <> ldp:membershipResource <" + tempTarget + "> }";
1746        final HttpPatch patchDirect2 = new HttpPatch(directUri);
1747        setAuth(patchDirect2, username);
1748        patchDirect2.setEntity(new StringEntity(patch_delete_relation, sparqlContentType));
1749        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchDirect2));
1750
1751        final String patch_insert_relation = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1752                "INSERT DATA { <> ldp:membershipResource <" + targetResource + "> }";
1753        final HttpPatch patchDirect3 = new HttpPatch(directUri);
1754        setAuth(patchDirect3, username);
1755        patchDirect3.setEntity(new StringEntity(patch_insert_relation, sparqlContentType));
1756        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(patchDirect3));
1757
1758        // Patch the indirect to the readonly target as admin
1759        final HttpPatch patchAsAdmin = new HttpPatch(directUri);
1760        setAuth(patchAsAdmin, "fedoraAdmin");
1761        patchAsAdmin.setEntity(new StringEntity(patch_text, sparqlContentType));
1762        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchAsAdmin));
1763
1764        // Ensure the patching happened.
1765        final HttpGet verifyGet = new HttpGet(directUri);
1766        setAuth(verifyGet, "fedoraAdmin");
1767        try (final CloseableHttpResponse response = execute(verifyGet)) {
1768            final CloseableDataset dataset = getDataset(response);
1769            final DatasetGraph graph = dataset.asDatasetGraph();
1770            assertTrue("Can't find " + targetUri + " in graph",
1771                    graph.contains(
1772                        Node.ANY,
1773                        NodeFactory.createURI(directUri),
1774                        MEMBERSHIP_RESOURCE.asNode(),
1775                        NodeFactory.createURI(targetUri)
1776                    )
1777            );
1778        }
1779
1780        // Try to POST a child as user
1781        final HttpPost postChild = new HttpPost(directUri);
1782        final String postTarget = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1783                "@prefix test: <http://example.org/test#> .\n" +
1784                "<> test:something <" + tempTarget + "> .";
1785        final HttpEntity putPostChild = new StringEntity(postTarget, turtleContentType);
1786        setAuth(postChild, username);
1787        postChild.setEntity(putPostChild);
1788        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(postChild));
1789
1790        // Try to PUT a child as user
1791        final String id = getRandomUniqueId();
1792        final HttpPut putChild = new HttpPut(directUri + "/" + id);
1793        setAuth(putChild, username);
1794        putChild.setEntity(putPostChild);
1795        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(putChild));
1796
1797        // Put the child as Admin
1798        setAuth(putChild, "fedoraAdmin");
1799        assertEquals(HttpStatus.SC_CREATED, getStatus(putChild));
1800
1801        // Try to delete the child as user
1802        final HttpDelete deleteChild = new HttpDelete(directUri + "/" + id);
1803        setAuth(deleteChild, username);
1804        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(deleteChild));
1805
1806        // Try to delete the indirect container
1807        final HttpDelete deleteIndirect = new HttpDelete(directUri);
1808        setAuth(deleteIndirect, username);
1809        assertEquals(HttpStatus.SC_FORBIDDEN, getStatus(deleteIndirect));
1810
1811        // Ensure we can still write to the writeable resource.
1812        testCanWrite(writeableResource, username);
1813
1814    }
1815
1816    @Test
1817    public void testDirectRelationshipsOk() throws IOException {
1818        final String targetResource = "/rest/" + getRandomUniqueId();
1819        final String writeableResource = "/rest/" + getRandomUniqueId();
1820        final String username = "user28";
1821
1822        final String targetUri = ingestObj(targetResource);
1823
1824        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1825                "<#readauthz> a acl:Authorization ;\n" +
1826                "   acl:agent \"" + username + "\" ;\n" +
1827                "   acl:mode acl:Read, acl:Write ;\n" +
1828                "   acl:accessTo <" + targetResource + "> .";
1829        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
1830
1831        // User can read target resource.
1832        final HttpGet get1 = getObjMethod(targetResource);
1833        setAuth(get1, username);
1834        assertEquals(HttpStatus.SC_OK, getStatus(get1));
1835
1836        // User can patch target resource.
1837        final String patch = "INSERT DATA { <> <http://purl.org/dc/elements/1.1/title> \"Changed it\"}";
1838        final HttpEntity patchEntity = new StringEntity(patch, sparqlContentType);
1839        try (final CloseableHttpResponse resp = (CloseableHttpResponse) PATCH(targetUri, patchEntity,
1840                username)) {
1841            assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(resp));
1842        }
1843
1844        // Make a user writable container.
1845        final String writeableUri = ingestObj(writeableResource);
1846        final String writeableAcl = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1847                "<#writeauth> a acl:Authorization ;\n" +
1848                "   acl:agent \"" + username + "\" ;\n" +
1849                "   acl:mode acl:Read, acl:Write ;\n" +
1850                "   acl:accessTo <" + writeableResource + "> ;\n" +
1851                "   acl:default <" + writeableResource + "> .";
1852        ingestAclString(writeableUri, writeableAcl, "fedoraAdmin");
1853
1854        // Ensure we can write to the writeable resource.
1855        testCanWrite(writeableResource, username);
1856
1857        // Try to create direct container referencing writeable resource with POST.
1858        final HttpPost userPost = postObjMethod(writeableResource);
1859        setAuth(userPost, username);
1860        userPost.addHeader("Link", "<" + DIRECT_CONTAINER.toString() + ">; rel=type");
1861        final String indirect = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1862                "@prefix test: <http://example.org/test#> .\n\n" +
1863                "<> ldp:membershipResource <" + targetResource + "> ;\n" +
1864                "ldp:hasMemberRelation test:predicateToCreate .";
1865        final HttpEntity directEntity = new StringEntity(indirect, turtleContentType);
1866        userPost.setEntity(new StringEntity(indirect, turtleContentType));
1867        userPost.setHeader("Content-type", "text/turtle");
1868        assertEquals(HttpStatus.SC_CREATED, getStatus(userPost));
1869
1870        // Try to create direct container referencing writeable resource with PUT.
1871        final String directString = getRandomUniqueId();
1872        final HttpPut userPut = putObjMethod(writeableResource + "/" + directString);
1873        setAuth(userPut, username);
1874        userPut.addHeader("Link", "<" + DIRECT_CONTAINER.toString() + ">; rel=type");
1875        userPut.setEntity(directEntity);
1876        userPut.setHeader("Content-type", "text/turtle");
1877        assertEquals(HttpStatus.SC_CREATED, getStatus(userPut));
1878
1879        // Create an user writeable resource.
1880        final HttpPost targetPost = postObjMethod(writeableResource);
1881        setAuth(targetPost, username);
1882        final String tempTarget;
1883        try (final CloseableHttpResponse resp = execute(targetPost)) {
1884            assertEquals(HttpStatus.SC_CREATED, getStatus(resp));
1885            tempTarget = getLocation(resp);
1886        }
1887
1888        // Try to create direct container referencing an available resource.
1889        final String direct_ok = "@prefix ldp: <http://www.w3.org/ns/ldp#> .\n" +
1890                "@prefix test: <http://example.org/test#> .\n\n" +
1891                "<> ldp:membershipResource <" + tempTarget + "> ;\n" +
1892                "ldp:hasMemberRelation test:predicateToCreate .";
1893        final HttpPost userPatchPost = postObjMethod(writeableResource);
1894        setAuth(userPatchPost, username);
1895        userPatchPost.addHeader("Link", "<" + DIRECT_CONTAINER.toString() + ">; rel=type");
1896        userPatchPost.setEntity(new StringEntity(direct_ok, turtleContentType));
1897        userPatchPost.setHeader("Content-type", "text/turtle");
1898        final String directUri;
1899        try (final CloseableHttpResponse resp = execute(userPatchPost)) {
1900            assertEquals(HttpStatus.SC_CREATED, getStatus(resp));
1901            directUri = getLocation(resp);
1902        }
1903
1904        // Then PATCH to the readonly resource.
1905        final HttpPatch patchDirect = new HttpPatch(directUri);
1906        setAuth(patchDirect, username);
1907        final String patch_text = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1908                "DELETE { <> ldp:membershipResource ?o } \n" +
1909                "INSERT { <> ldp:membershipResource <" + targetResource + "> } \n" +
1910                "WHERE { <> ldp:membershipResource ?o }";
1911        patchDirect.setEntity(new StringEntity(patch_text, sparqlContentType));
1912        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchDirect));
1913
1914        // Delete the ldp:membershipRelation and add it with INSERT
1915        final String patch_delete_relation = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1916                "DELETE DATA { <> ldp:membershipResource <" + targetResource + "> }";
1917        final HttpPatch patchDirect2 = new HttpPatch(directUri);
1918        setAuth(patchDirect2, username);
1919        patchDirect2.setEntity(new StringEntity(patch_delete_relation, sparqlContentType));
1920        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchDirect2));
1921
1922        // Cannot insert membershipResource without deleting the default value
1923        final String patch_insert_relation = "prefix ldp: <http://www.w3.org/ns/ldp#> \n" +
1924                "DELETE { <> ldp:membershipResource ?o } \n" +
1925                "INSERT { <> ldp:membershipResource <" + targetResource + "> } \n" +
1926                "WHERE { <> ldp:membershipResource ?o }";
1927        final HttpPatch patchDirect3 = new HttpPatch(directUri);
1928        setAuth(patchDirect3, username);
1929        patchDirect3.setEntity(new StringEntity(patch_insert_relation, sparqlContentType));
1930        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(patchDirect3));
1931
1932        // Ensure we can write to the writeable resource.
1933        testCanWrite(writeableResource, username);
1934    }
1935
1936    @Test
1937    public void testSameInTransaction() throws Exception {
1938        final String targetResource = "/rest/" + getRandomUniqueId();
1939        final String username = "user28";
1940        // Make a basic container.
1941        final String targetUri = ingestObj(targetResource);
1942        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
1943                "<#readauthz> a acl:Authorization ;\n" +
1944                "   acl:agent \"" + username + "\" ;\n" +
1945                "   acl:mode acl:Read, acl:Write ;\n" +
1946                "   acl:accessTo <" + targetResource + "> .";
1947        // Allow user28 to read and write this object.
1948        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
1949        // Test that user28 can read target resource.
1950        final HttpGet getAllowed1 = getObjMethod(targetResource);
1951        setAuth(getAllowed1, username);
1952        assertEquals(HttpStatus.SC_OK, getStatus(getAllowed1));
1953        // Test that user28 can patch target resource.
1954        final HttpPatch patchAllowed1 = patchObjMethod(targetResource);
1955        final String patchString = "prefix dc: <http://purl.org/dc/elements/1.1/> INSERT { <> dc:title " +
1956            "\"new title\" } WHERE {}";
1957        final StringEntity patchEntity = new StringEntity(patchString, Charsets.UTF8_CHARSET);
1958        patchAllowed1.setEntity(patchEntity);
1959        patchAllowed1.setHeader(CONTENT_TYPE, "application/sparql-update");
1960        setAuth(patchAllowed1, username);
1961        assertEquals(SC_NO_CONTENT, getStatus(patchAllowed1));
1962        // Test that user28 can post to target resource.
1963        final HttpPost postAllowed1 = postObjMethod(targetResource);
1964        setAuth(postAllowed1, username);
1965        final String childResource;
1966        try (final CloseableHttpResponse response = execute(postAllowed1)) {
1967            assertEquals(SC_CREATED, getStatus(postAllowed1));
1968            childResource = getLocation(response);
1969        }
1970        // Test that user28 cannot patch the child resource (ACL is not acl:default).
1971        final HttpPatch patchDisallowed1 = new HttpPatch(childResource);
1972        patchDisallowed1.setEntity(patchEntity);
1973        patchDisallowed1.setHeader(CONTENT_TYPE, "application/sparql-update");
1974        setAuth(patchDisallowed1, username);
1975        assertEquals(SC_FORBIDDEN, getStatus(patchDisallowed1));
1976        // Test that user28 cannot post to a child resource.
1977        final HttpPost postDisallowed1 = new HttpPost(childResource);
1978        setAuth(postDisallowed1, username);
1979        assertEquals(SC_FORBIDDEN, getStatus(postDisallowed1));
1980        // Test another user cannot access the target resource.
1981        final HttpGet getDisallowed1 = getObjMethod(targetResource);
1982        setAuth(getDisallowed1, "user400");
1983        assertEquals(SC_FORBIDDEN, getStatus(getDisallowed1));
1984        // Get the transaction endpoint.
1985        final HttpGet getTransactionEndpoint = getObjMethod("/rest");
1986        setAuth(getTransactionEndpoint, "fedoraAdmin");
1987        final String transactionEndpoint;
1988        final Pattern linkHeaderMatcher = Pattern.compile("<([^>]+)>");
1989        try (final CloseableHttpResponse response = execute(getTransactionEndpoint)) {
1990            final var linkheaders = getLinkHeaders(response);
1991            transactionEndpoint = linkheaders.stream()
1992                    .filter(t -> t.contains("http://fedora.info/definitions/v4/transaction#endpoint"))
1993                    .map(t -> {
1994                        final var matches = linkHeaderMatcher.matcher(t);
1995                        matches.find();
1996                        return matches.group(1);
1997                    })
1998                    .findFirst()
1999                    .orElseThrow(Exception::new);
2000        }
2001        // Create a transaction.
2002        final HttpPost postTransaction = new HttpPost(transactionEndpoint);
2003        setAuth(postTransaction, "fedoraAdmin");
2004        final String transactionId;
2005        try (final CloseableHttpResponse response = execute(postTransaction)) {
2006            assertEquals(SC_CREATED, getStatus(response));
2007            transactionId = getLocation(response);
2008        }
2009        // Test user28 can post to  target resource in a transaction.
2010        final HttpPost postChildInTx = postObjMethod(targetResource);
2011        setAuth(postChildInTx, username);
2012        postChildInTx.setHeader(ATOMIC_ID_HEADER, transactionId);
2013        final String txChild;
2014        try (final CloseableHttpResponse response = execute(postChildInTx)) {
2015            assertEquals(SC_CREATED, getStatus(response));
2016            txChild = getLocation(response);
2017        }
2018        // Test user28 cannot post to the child in a transaction.
2019        final HttpPost postDisallowed2 = new HttpPost(txChild);
2020        setAuth(postDisallowed2, username);
2021        postDisallowed2.setHeader(ATOMIC_ID_HEADER, transactionId);
2022        assertEquals(SC_FORBIDDEN, getStatus(postDisallowed2));
2023    }
2024
2025    @Test
2026    public void testBinaryAndDescriptionAllowed() throws Exception {
2027        final String targetResource = "/rest/" + getRandomUniqueId();
2028        final String username = "user88";
2029        // Make a basic container.
2030        final String targetUri = ingestObj(targetResource);
2031        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2032                "<#readauthz> a acl:Authorization ;\n" +
2033                "   acl:agent \"" + username + "\" ;\n" +
2034                "   acl:mode acl:Read, acl:Write ;\n" +
2035                "   acl:default <" + targetResource + "> ;" +
2036                "   acl:accessTo <" + targetResource + "> .";
2037        // Allow user to read and write this object.
2038        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
2039        // user creates a binary
2040        final HttpPost newBinary = postObjMethod(targetResource);
2041        setAuth(newBinary, username);
2042        newBinary.setHeader(CONTENT_TYPE, "text/plain");
2043        final StringEntity stringData = new StringEntity("This is some data", Charsets.UTF8_CHARSET);
2044        newBinary.setEntity(stringData);
2045        final String binaryLocation;
2046        try (final CloseableHttpResponse response = execute(newBinary)) {
2047            assertEquals(SC_CREATED, getStatus(response));
2048            binaryLocation = getLocation(response);
2049        }
2050        // Try PUTting a new binary
2051        final HttpPut putAgain = new HttpPut(binaryLocation);
2052        setAuth(putAgain, username);
2053        putAgain.setHeader(CONTENT_TYPE, "text/plain");
2054        final StringEntity newStringData = new StringEntity("Some other data", Charsets.UTF8_CHARSET);
2055        putAgain.setEntity(newStringData);
2056        assertEquals(SC_NO_CONTENT, getStatus(putAgain));
2057        // Try PUTting to binary description
2058        final HttpPut putDesc = new HttpPut(binaryLocation + "/" + FCR_METADATA);
2059        setAuth(putDesc, username);
2060        putDesc.setHeader(CONTENT_TYPE, "text/turtle");
2061        final StringEntity putDescData = new StringEntity("<> <http://purl.org/dc/elements/1.1/title> \"Some title\".",
2062                Charsets.UTF8_CHARSET);
2063        putDesc.setEntity(putDescData);
2064        assertEquals(SC_NO_CONTENT, getStatus(putDesc));
2065        // Check the title
2066        assertPredicateValue(binaryLocation + "/" + FCR_METADATA, "http://purl.org/dc/elements/1.1/title",
2067                "Some title");
2068        // Try PATCHing to binary description
2069        final HttpPatch patchDesc = new HttpPatch(binaryLocation + "/" + FCR_METADATA);
2070        setAuth(patchDesc, username);
2071        patchDesc.setHeader(CONTENT_TYPE, "application/sparql-update");
2072        final StringEntity patchDescData = new StringEntity("PREFIX dc: <http://purl.org/dc/elements/1.1/> " +
2073                "DELETE { <> dc:title ?o } INSERT { <> dc:title \"Some different title\" } WHERE { <> dc:title ?o }",
2074                Charsets.UTF8_CHARSET);
2075        patchDesc.setEntity(patchDescData);
2076        assertEquals(SC_NO_CONTENT, getStatus(patchDesc));
2077        // Check the title
2078        assertPredicateValue(binaryLocation + "/" + FCR_METADATA, "http://purl.org/dc/elements/1.1/title",
2079                "Some different title");
2080
2081    }
2082
2083    @Test
2084    public void testRequestWithEmptyPath() throws Exception {
2085        // Ensure HttpClient does not remove empty paths
2086        final RequestConfig config = RequestConfig.custom().setNormalizeUri(false).build();
2087
2088        final String username = "testUser92";
2089        final String parent = getRandomUniqueId();
2090        final HttpPost postParent = postObjMethod();
2091        postParent.setHeader("Slug", parent);
2092        setAuth(postParent, "fedoraAdmin");
2093        final String parentUri;
2094        try (final CloseableHttpResponse response = execute(postParent)) {
2095            assertEquals(CREATED.getStatusCode(), getStatus(response));
2096            parentUri = getLocation(response);
2097        }
2098        // Make parent only accessible to fedoraAdmin
2099        final String parentAcl = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2100                "<#readauthz> a acl:Authorization ;\n" +
2101                "   acl:agent \"fedoraAdmin\" ;\n" +
2102                "   acl:mode acl:Read, acl:Write ;\n" +
2103                "   acl:accessTo <" + parentUri + "> .";
2104        ingestAclString(parentUri, parentAcl, "fedoraAdmin");
2105        // Admin can see parent
2106        final HttpGet getAdminParent = getObjMethod(parent);
2107        setAuth(getAdminParent, "fedoraAdmin");
2108        assertEquals(OK.getStatusCode(), getStatus(getAdminParent));
2109        final HttpGet getParent = getObjMethod(parent);
2110        setAuth(getParent, username);
2111        // testUser92 cannot see parent.
2112        assertEquals(FORBIDDEN.getStatusCode(), getStatus(getParent));
2113
2114        final String child = getRandomUniqueId();
2115        final HttpPost postChild = postObjMethod(parent);
2116        postChild.setHeader("Slug", child);
2117        setAuth(postChild, "fedoraAdmin");
2118        final String childUri;
2119        try (final CloseableHttpResponse response = execute(postChild)) {
2120            assertEquals(CREATED.getStatusCode(), getStatus(response));
2121            childUri = getLocation(response);
2122        }
2123        // Make child accessible to testUser92
2124        final String childAcl = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2125                "<#readauthz> a acl:Authorization ;\n" +
2126                "   acl:agent \"" + username + "\" ;\n" +
2127                "   acl:mode acl:Read, acl:Write ;\n" +
2128                "   acl:accessTo <" + childUri + "> .";
2129        ingestAclString(childUri, childAcl, "fedoraAdmin");
2130        // Admin can see child.
2131        final HttpGet getAdminChild = getObjMethod(parent + "/" + child);
2132        setAuth(getAdminChild, "fedoraAdmin");
2133        assertEquals(OK.getStatusCode(), getStatus(getAdminChild));
2134
2135        // testUser92 can see child.
2136        final HttpGet getChild = getObjMethod(parent + "/" + child);
2137        setAuth(getChild, username);
2138        assertEquals(OK.getStatusCode(), getStatus(getChild));
2139
2140        // Admin bypasses ACL resolution gets 409.
2141        final HttpGet getAdminRequest = getObjMethod(parent + "//" + child);
2142        setAuth(getAdminRequest, "fedoraAdmin");
2143        getAdminRequest.setConfig(config);
2144        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(getAdminRequest));
2145        // User
2146        final HttpGet getUserRequest = getObjMethod(parent + "//" + child);
2147        setAuth(getUserRequest, username);
2148        getUserRequest.setConfig(config);
2149        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(getUserRequest));
2150    }
2151
2152    @Test
2153    public void testGetWithEmbeddedResourcesOk() throws Exception {
2154        final String targetResource = "/rest/" + getRandomUniqueId();
2155        final String childResource = targetResource + "/" + getRandomUniqueId();
2156        final String username = "user88";
2157        // Make a basic container.
2158        final String targetUri = ingestObj(targetResource);
2159        ingestObj(childResource);
2160        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2161                "<#readauthz> a acl:Authorization ;\n" +
2162                "   acl:agent \"" + username + "\" ;\n" +
2163                "   acl:mode acl:Read, acl:Write ;\n" +
2164                "   acl:default <" + targetResource + "> ;" +
2165                "   acl:accessTo <" + targetResource + "> .";
2166        // Allow user to read and write this object.
2167        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
2168
2169        final HttpGet getAdminChild = new HttpGet(targetUri);
2170        setAuth(getAdminChild, username);
2171        getAdminChild.addHeader("Prefer", "return=representation; include=\"" + EMBED_CONTAINED + "\"");
2172        assertEquals(OK.getStatusCode(), getStatus(getAdminChild));
2173    }
2174
2175    @Test
2176    public void testGetWithEmbeddedResourceDenied() throws Exception {
2177        final String targetResource = "/rest/" + getRandomUniqueId();
2178        final String childResource = targetResource + "/" + getRandomUniqueId();
2179        final String username = "user88";
2180        // Make a basic container.
2181        final String targetUri = ingestObj(targetResource);
2182        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2183                "<#readauthz> a acl:Authorization ;\n" +
2184                "   acl:agent \"" + username + "\" ;\n" +
2185                "   acl:mode acl:Read, acl:Write ;\n" +
2186                "   acl:accessTo <" + targetResource + "> .";
2187        // Allow user to read and write this object.
2188        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
2189
2190        final String childUri = ingestObj(childResource);
2191        final String noAccessString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2192                "<#readauthz> a acl:Authorization ;\n" +
2193                "   acl:agent \"fedoraAdmin\" ;\n" +
2194                "   acl:mode acl:Read, acl:Write ;\n" +
2195                "   acl:accessTo <" + childResource + "> .";
2196        ingestAclString(childUri, noAccessString, "fedoraAdmin");
2197
2198        // Can get the target.
2199        final HttpGet getTarget = new HttpGet(targetUri);
2200        setAuth(getTarget, username);
2201        assertEquals(OK.getStatusCode(), getStatus(getTarget));
2202
2203        // Can't get the child.
2204        final HttpGet getChild = new HttpGet(childUri);
2205        setAuth(getChild, username);
2206        assertEquals(FORBIDDEN.getStatusCode(), getStatus(getChild));
2207
2208        // So you can't get the target with embedded resources.
2209        final HttpGet getAdminChild = new HttpGet(targetUri);
2210        setAuth(getAdminChild, username);
2211        getAdminChild.addHeader("Prefer", "return=representation; include=\"" + EMBED_CONTAINED + "\"");
2212        assertEquals(FORBIDDEN.getStatusCode(), getStatus(getAdminChild));
2213    }
2214
2215    @Test
2216    public void testDeepDeleteAllowed() throws Exception {
2217        final String targetResource = "/rest/" + getRandomUniqueId();
2218        final String username = "user88";
2219        // Make a basic container.
2220        final String targetUri = ingestObj(targetResource);
2221        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2222                "<#readauthz> a acl:Authorization ;\n" +
2223                "   acl:agent \"" + username + "\" ;\n" +
2224                "   acl:mode acl:Read, acl:Write ;\n" +
2225                "   acl:accessTo <" + targetResource + "> ;\n" +
2226                "   acl:default <" + targetResource + "> .";
2227        // Allow user to read and write this object.
2228        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
2229
2230        final String child1 = targetResource + "/" + getRandomUniqueId();
2231        final String child2 = targetResource + "/" + getRandomUniqueId();
2232        final String child1_1 = child1 + "/" + getRandomUniqueId();
2233        final String child2_1 = child2 + "/" + getRandomUniqueId();
2234        ingestObj(child1);
2235        ingestObj(child2);
2236        ingestObj(child1_1);
2237        ingestObj(child2_1);
2238
2239        assertGetRequest(targetUri, username, OK);
2240        assertGetRequest(serverAddress + child1, username, OK);
2241        assertGetRequest(serverAddress + child2, username, OK);
2242        assertGetRequest(serverAddress + child1_1, username, OK);
2243        assertGetRequest(serverAddress + child2_1, username, OK);
2244
2245        final var delete = new HttpDelete(targetUri);
2246        setAuth(delete, username);
2247        assertEquals(NO_CONTENT.getStatusCode(), getStatus(delete));
2248    }
2249
2250    @Test
2251    public void testDeepDeleteFailed() throws Exception {
2252        final String targetResource = "/rest/" + getRandomUniqueId();
2253        final String username = "user88";
2254        // Make a basic container.
2255        final String targetUri = ingestObj(targetResource);
2256        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2257                "<#readauthz> a acl:Authorization ;\n" +
2258                "   acl:agent \"" + username + "\" ;\n" +
2259                "   acl:mode acl:Read, acl:Write ;\n" +
2260                "   acl:accessTo <" + targetResource + "> ;\n" +
2261                "   acl:default <" + targetResource + "> .";
2262        // Allow user to read and write this object.
2263        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
2264
2265        final String child1 = targetResource + "/" + getRandomUniqueId();
2266        final String child2 = targetResource + "/" + getRandomUniqueId();
2267        final String child1_1 = child1 + "/" + getRandomUniqueId();
2268        final String child2_1 = child2 + "/" + getRandomUniqueId();
2269        ingestObj(child1);
2270        ingestObj(child2);
2271        ingestObj(child1_1);
2272        final String child2_1_URI = ingestObj(child2_1);
2273        final String noAccessString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2274                "<#readauthz> a acl:Authorization ;\n" +
2275                "   acl:agent \"fedoraAdmin\" ;\n" +
2276                "   acl:mode acl:Read, acl:Write ;\n" +
2277                "   acl:accessTo <" + child2_1 + "> .";
2278        ingestAclString(child2_1_URI, noAccessString, "fedoraAdmin");
2279
2280        assertGetRequest(targetUri, username, OK);
2281        assertGetRequest(serverAddress + child1, username, OK);
2282        assertGetRequest(serverAddress + child2, username, OK);
2283        assertGetRequest(serverAddress + child1_1, username, OK);
2284        assertGetRequest(serverAddress + child2_1, username, FORBIDDEN);
2285
2286        final var delete = new HttpDelete(targetUri);
2287        setAuth(delete, username);
2288        assertEquals(FORBIDDEN.getStatusCode(), getStatus(delete));
2289    }
2290
2291    @Test
2292    public void testTransactionExceptions() throws Exception {
2293        // Ensure both admin and users get 409 for invalid transaction ids.
2294        final var invalidIx = serverAddress + FCR_TX + "/fake";
2295        assertGetRequest(serverAddress, "fedoraAdmin", invalidIx, CONFLICT);
2296        assertGetRequest(serverAddress, "testuser", invalidIx, CONFLICT);
2297        // Ensure both admin and users get 409 for a non-existant transactions.
2298        final var fakeTx = serverAddress + FCR_TX + "/" + getRandomUniqueId();
2299        assertGetRequest(serverAddress, "fedoraAdmin", fakeTx, CONFLICT);
2300        assertGetRequest(serverAddress, "testuser", fakeTx, CONFLICT);
2301        // Create a transaction.
2302        final var postTx = postObjMethod(FCR_TX);
2303        setAuth(postTx, "fedoraAdmin");
2304        final String txId;
2305        try (final var response = execute(postTx)) {
2306            assertEquals(SC_CREATED, getStatus(response));
2307            txId = getLocation(response);
2308        }
2309        // Create an object in the transaction.
2310        final var postObj = postObjMethod();
2311        setAuth(postObj, "fedoraAdmin");
2312        addTxTo(postObj, txId);
2313        final String targetUri;
2314        try (final var response = execute(postObj)) {
2315            assertEquals(SC_CREATED, getStatus(response));
2316            targetUri = getLocation(response);
2317        }
2318        // Test the transaction works.
2319        assertGetRequest(targetUri, "testuser", txId, OK);
2320        // Commit the transaction
2321        final var commit = new HttpPut(txId);
2322        setAuth(commit, "fedoraAdmin");
2323        assertEquals(SC_NO_CONTENT, getStatus(commit));
2324        // Now try to get the transaction again, expect 409 Conflict..
2325        assertGetRequest(targetUri, "fedoraAdmin", txId, CONFLICT);
2326        assertGetRequest(targetUri, "testuser", txId, CONFLICT);
2327    }
2328
2329    private void assertGetRequest(final String uri, final String username, final Response.Status expectedResponse) {
2330        assertGetRequest(uri, username, null, expectedResponse);
2331    }
2332
2333    private void assertGetRequest(final String uri, final String username, final String txId,
2334                                  final Response.Status expectedResponse) {
2335        final var getTarget = new HttpGet(uri);
2336        setAuth(getTarget, username);
2337        if (txId != null) {
2338            addTxTo(getTarget, txId);
2339        }
2340        assertEquals(expectedResponse.getStatusCode(), getStatus(getTarget));
2341    }
2342
2343    /**
2344     * Check the graph has the predicate with the value.
2345     * @param targetUri Full URI of the resource to check.
2346     * @param predicateUri Full URI of the predicate to check.
2347     * @param predicateValue Literal value to look for.
2348     * @throws Exception if problems performing the GET.
2349     */
2350    private void assertPredicateValue(final String targetUri, final String predicateUri, final String predicateValue)
2351            throws Exception {
2352        final HttpGet verifyGet = new HttpGet(targetUri);
2353        setAuth(verifyGet, "fedoraAdmin");
2354        try (final CloseableHttpResponse response = execute(verifyGet)) {
2355            final CloseableDataset dataset = getDataset(response);
2356            final DatasetGraph graph = dataset.asDatasetGraph();
2357            assertTrue("Can't find " + predicateValue + " for predicate " + predicateUri + " in graph",
2358                    graph.contains(
2359                            Node.ANY,
2360                            Node.ANY,
2361                            NodeFactory.createURI(predicateUri),
2362                            NodeFactory.createLiteral(predicateValue)
2363                    )
2364            );
2365        }
2366    }
2367
2368
2369    /**
2370     * Utility function to ingest a ACL from a string.
2371     *
2372     * @param resourcePath Path to the resource if doesn't end with "/fcr:acl" it is added.
2373     * @param acl the text/turtle ACL as a string
2374     * @param username user to ingest as
2375     * @return the response from the ACL ingest.
2376     * @throws IOException on StringEntity encoding or client execute
2377     */
2378    private HttpResponse ingestAclString(final String resourcePath, final String acl, final String username)
2379            throws IOException {
2380        final String aclPath = (resourcePath.endsWith("/fcr:acl") ? resourcePath : resourcePath + "/fcr:acl");
2381        final HttpPut putReq = new HttpPut(aclPath);
2382        setAuth(putReq, username);
2383        putReq.setHeader("Content-type", "text/turtle");
2384        putReq.setEntity(new StringEntity(acl, turtleContentType));
2385        return execute(putReq);
2386    }
2387
2388    /**
2389     * Ensure that a writeable resource is still writeable
2390     *
2391     * @param writeableResource the URI of the writeable resource.
2392     * @param username the user will write access.
2393     * @throws UnsupportedEncodingException if default charset for String Entity is unsupported
2394     */
2395    private void testCanWrite(final String writeableResource, final String username)
2396            throws UnsupportedEncodingException {
2397        // Try to create a basic container inside the writeable resource with POST.
2398        final HttpPost okPost = postObjMethod(writeableResource);
2399        setAuth(okPost, username);
2400        assertEquals(HttpStatus.SC_CREATED, getStatus(okPost));
2401
2402        // Try to PATCH the writeableResource
2403        final HttpPatch okPatch = patchObjMethod(writeableResource);
2404        final String patchString = "PREFIX dc: <http://purl.org/dc/elements/1.1/> DELETE { <> dc:title ?o1 } " +
2405                "INSERT { <> dc:title \"Changed title\" }  WHERE { <> dc:title ?o1 }";
2406        final HttpEntity patchEntity = new StringEntity(patchString, sparqlContentType);
2407        setAuth(okPatch, username);
2408        okPatch.setHeader("Content-type", "application/sparql-update");
2409        okPatch.setEntity(patchEntity);
2410        assertEquals(HttpStatus.SC_NO_CONTENT, getStatus(okPatch));
2411    }
2412
2413    @Test
2414    public void testAuthenticatedUserCanCreateTransaction() {
2415        final HttpPost txnCreatePost = postObjMethod("rest/fcr:tx");
2416        setAuth(txnCreatePost, "testUser92");
2417        assertEquals(SC_CREATED, getStatus(txnCreatePost));
2418    }
2419
2420    @Test
2421    public void testAlterAclWithPatch() throws IOException {
2422        final String targetResource = "/rest/" + getRandomUniqueId();
2423        final String username = "user88";
2424        // Make a basic container.
2425        final String targetUri = ingestObj(targetResource);
2426        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2427                "<#readauthz> a acl:Authorization ;\n" +
2428                "   acl:agent \"" + username + "\" ;\n" +
2429                "   acl:mode acl:Read, acl:Write, acl:Append ;\n" +
2430                "   acl:accessTo <" + targetResource + "> ;\n" +
2431                "   acl:default <" + targetResource + "> .";
2432        // Allow user to read and write this object.
2433        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
2434
2435        // Test we can append a container as username
2436        final HttpPost okPost = postObjMethod(targetResource);
2437        setAuth(okPost, username);
2438        assertEquals(SC_CREATED, getStatus(okPost));
2439
2440        // Change the ACL to read-only for username
2441        final String readonlyString = "prefix acl: <http://www.w3.org/ns/auth/acl#> " +
2442                " DELETE { <#readauthz> acl:mode acl:Write . <#readauthz> acl:mode acl:Append .} WHERE {}";
2443        final HttpPatch httpPatch = patchObjMethod(targetResource + "/fcr:acl");
2444        setAuth(httpPatch, "fedoraAdmin");
2445        httpPatch.setEntity(new StringEntity(readonlyString, UTF_8));
2446        httpPatch.setHeader(CONTENT_TYPE, "application/sparql-update");
2447        assertEquals(SC_NO_CONTENT, getStatus(httpPatch));
2448
2449        // Test we can't append a container as username
2450        final HttpPost noPost = postObjMethod(targetResource);
2451        setAuth(noPost, username);
2452        assertEquals(SC_FORBIDDEN, getStatus(noPost));
2453    }
2454
2455    @Test
2456    public void testAlterAclWithPut() throws IOException {
2457        final String targetResource = "/rest/" + getRandomUniqueId();
2458        final String username = "user88";
2459        // Make a basic container.
2460        final String targetUri = ingestObj(targetResource);
2461        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2462                "<#readauthz> a acl:Authorization ;\n" +
2463                "   acl:agent \"" + username + "\" ;\n" +
2464                "   acl:mode acl:Read, acl:Write, acl:Append ;\n" +
2465                "   acl:accessTo <" + targetResource + "> ;\n" +
2466                "   acl:default <" + targetResource + "> .";
2467        // Allow user to read and write this object.
2468        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
2469
2470        // Test we can append a container as username
2471        final HttpPost okPost = postObjMethod(targetResource);
2472        setAuth(okPost, username);
2473        assertEquals(SC_CREATED, getStatus(okPost));
2474
2475        // Change the ACL to read-only for username
2476        final String readonlyString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2477                "<#readauthz> a acl:Authorization ;\n" +
2478                "   acl:agent \"" + username + "\" ;\n" +
2479                "   acl:mode acl:Read ;\n" +
2480                "   acl:accessTo <" + targetResource + "> ;\n" +
2481                "   acl:default <" + targetResource + "> .";
2482        final HttpPut httpPut = putObjMethod(targetResource + "/fcr:acl");
2483        setAuth(httpPut, "fedoraAdmin");
2484        httpPut.setEntity(new StringEntity(readonlyString, UTF_8));
2485        httpPut.setHeader(CONTENT_TYPE, "text/turtle");
2486        httpPut.setHeader("Prefer", "handling=lenient");
2487        assertEquals(SC_NO_CONTENT, getStatus(httpPut));
2488
2489        // Test we can't append a container as username
2490        final HttpPost noPost = postObjMethod(targetResource);
2491        setAuth(noPost, username);
2492        assertEquals(SC_FORBIDDEN, getStatus(noPost));
2493    }
2494
2495    @Test
2496    public void testDeleteTombstone() throws IOException {
2497        final String targetResource = "/rest/" + getRandomUniqueId();
2498        final String username = "user99";
2499        // Make a basic container.
2500        final String targetUri = ingestObj(targetResource);
2501        final String readwriteString = "@prefix acl: <http://www.w3.org/ns/auth/acl#> .\n" +
2502                "<#readauthz> a acl:Authorization ;\n" +
2503                "   acl:agent \"" + username + "\" ;\n" +
2504                "   acl:mode acl:Read, acl:Write, acl:Append ;\n" +
2505                "   acl:accessTo <" + targetResource + "> ;\n" +
2506                "   acl:default <" + targetResource + "> .";
2507        // Allow user to read and write this object.
2508        ingestAclString(targetUri, readwriteString, "fedoraAdmin");
2509
2510        // Test we can append a container as username
2511        final HttpPost okPost = postObjMethod(targetResource);
2512        setAuth(okPost, username);
2513        final String location;
2514        try (final var response = execute(okPost)) {
2515            assertEquals(SC_CREATED, getStatus(response));
2516            location = getLocation(response);
2517        }
2518        // Delete the container
2519        final HttpDelete deleteContainer = new HttpDelete(location);
2520        setAuth(deleteContainer, username);
2521        assertEquals(SC_NO_CONTENT, getStatus(deleteContainer));
2522        // Test we have a tombstone
2523        final HttpGet getContainer = new HttpGet(location);
2524        setAuth(getContainer, username);
2525        assertEquals(SC_GONE, getStatus(getContainer));
2526        // Delete the tombstone
2527        final HttpDelete deleteTombstone = new HttpDelete(location + "/" + FCR_TOMBSTONE);
2528        setAuth(deleteTombstone, username);
2529        assertEquals(SC_NO_CONTENT, getStatus(deleteTombstone));
2530        // Test we have a nothing now.
2531        final HttpGet getContainer2 = new HttpGet(location);
2532        setAuth(getContainer2, username);
2533        assertEquals(SC_NOT_FOUND, getStatus(getContainer2));
2534    }
2535}