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