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