diff --git a/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java b/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java index 2a26c3d896..0726b73218 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java @@ -83,6 +83,11 @@ public class RequestScope implements com.yahoo.elide.core.security.RequestScope /* Used to filter across heterogeneous types during the first load */ private FilterExpression globalFilterExpression; + /** + * Used to defer inline checks for the {@link PermissionExecutor}. + */ + @Getter @Setter private boolean deferInlineChecks; + /** * Create a new RequestScope with specified update status code. * diff --git a/elide-core/src/main/java/com/yahoo/elide/core/security/executors/ActivePermissionExecutor.java b/elide-core/src/main/java/com/yahoo/elide/core/security/executors/ActivePermissionExecutor.java index 642bc7678c..5c5af049e0 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/security/executors/ActivePermissionExecutor.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/security/executors/ActivePermissionExecutor.java @@ -103,7 +103,7 @@ public ExpressionResult checkPermission( Function expressionExecutor = (expression) -> { // for newly created object in PatchRequest limit to User checks - if (resource.isNewlyCreated()) { + if (resource.isNewlyCreated() || requestScope.isDeferInlineChecks()) { return executeUserChecksDeferInline(annotationClass, expression); } return executeExpressions(expression, annotationClass, Expression.EvaluationMode.INLINE_CHECKS_ONLY); @@ -189,7 +189,7 @@ public ExpressionResult checkSpecificFieldPermissionsDefe changeSpec); Function expressionExecutor = (expression) -> { - if (requestScope.getNewPersistentResources().contains(resource)) { + if (requestScope.getNewPersistentResources().contains(resource) || requestScope.isDeferInlineChecks()) { return executeUserChecksDeferInline(expressionAnnotation, expression); } return executeExpressions(expression, expressionAnnotation, Expression.EvaluationMode.INLINE_CHECKS_ONLY); diff --git a/elide-core/src/main/java/com/yahoo/elide/jsonapi/extensions/JsonApiAtomicOperations.java b/elide-core/src/main/java/com/yahoo/elide/jsonapi/extensions/JsonApiAtomicOperations.java index 115512d4b9..43725584d5 100644 --- a/elide-core/src/main/java/com/yahoo/elide/jsonapi/extensions/JsonApiAtomicOperations.java +++ b/elide-core/src/main/java/com/yahoo/elide/jsonapi/extensions/JsonApiAtomicOperations.java @@ -456,6 +456,7 @@ private Supplier> handleRemoveOp(String path, * @param requestScope request scope */ private void postProcessRelationships(JsonApiAtomicOperationsRequestScope requestScope) { + requestScope.setDeferInlineChecks(true); actions.forEach(action -> action.postProcess(requestScope)); } diff --git a/elide-core/src/main/java/com/yahoo/elide/jsonapi/extensions/JsonApiJsonPatch.java b/elide-core/src/main/java/com/yahoo/elide/jsonapi/extensions/JsonApiJsonPatch.java index 31c75a34d1..d4b35ee90b 100644 --- a/elide-core/src/main/java/com/yahoo/elide/jsonapi/extensions/JsonApiJsonPatch.java +++ b/elide-core/src/main/java/com/yahoo/elide/jsonapi/extensions/JsonApiJsonPatch.java @@ -307,6 +307,7 @@ private Supplier> handleRemoveOp(String path, * @param requestScope request scope */ private void postProcessRelationships(JsonApiJsonPatchRequestScope requestScope) { + requestScope.setDeferInlineChecks(true); actions.forEach(action -> action.postProcess(requestScope)); } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/checks/AuthorHasUserAccessibleBooks.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/checks/AuthorHasUserAccessibleBooks.java new file mode 100644 index 0000000000..9f0b905625 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/checks/AuthorHasUserAccessibleBooks.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package example.checks; + +import com.yahoo.elide.annotation.SecurityCheck; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.filter.Operator; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.core.filter.predicates.FilterPredicate; +import com.yahoo.elide.core.security.RequestScope; +import com.yahoo.elide.core.security.checks.FilterExpressionCheck; +import com.yahoo.elide.core.type.Type; + +import example.models.jpa.PermissionAuthor; +import example.models.jpa.PermissionAuthorBook; +import example.models.jpa.PermissionBook; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Check to test that relationship updates have inline checks deferred. + */ +@SecurityCheck("author has user accessible books") +public class AuthorHasUserAccessibleBooks extends FilterExpressionCheck { + + @Override + public FilterExpression getFilterExpression(Type entityClass, RequestScope requestScope) { + Path.PathElement author = new Path.PathElement(PermissionAuthor.class, PermissionAuthorBook.class, "authorBooks"); + Path.PathElement book = new Path.PathElement(PermissionAuthorBook.class, PermissionBook.class, "book"); + Path.PathElement id = new Path.PathElement(PermissionBook.class, Long.class, "id"); + + List pathList = new ArrayList<>(); + pathList.add(author); + pathList.add(book); + pathList.add(id); + Path paths = new Path(pathList); + + // For simplicity this hard codes the book id to 1 and 2 instead of retrieving from the principal + return new FilterPredicate(paths, Operator.IN, Arrays.asList(1, 2)); + } +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionAuthor.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionAuthor.java new file mode 100644 index 0000000000..7564a1ce3b --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionAuthor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package example.models.jpa; + + +import com.yahoo.elide.annotation.Include; +import com.yahoo.elide.annotation.ReadPermission; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * Model for author. + *

+ * Used for testing deferred inline checks for + * {@link example.checks.AuthorHasUserAccessibleBooks}. + */ +@Entity +@Table(name = "permissionauthor") +@Include(name = "permissionauthor", description = "Author") +@Getter +@Setter +@ReadPermission(expression = "author has user accessible books") +public class PermissionAuthor { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + private String name; + + @OneToMany(mappedBy = "author") + private List authorBooks; +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionAuthorBook.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionAuthorBook.java new file mode 100644 index 0000000000..f18e9935a2 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionAuthorBook.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package example.models.jpa; + +import com.yahoo.elide.annotation.Include; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import lombok.Getter; +import lombok.Setter; + +/** + * Model for author book. + */ +@Entity +@Table(name = "permissionauthorbook") +@Include(name = "permissionauthorBook", description = "Author Book") +@Getter +@Setter +public class PermissionAuthorBook { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + @ManyToOne + @JoinColumn(name = "AUTHOR_ID") + private PermissionAuthor author; + + @ManyToOne + @JoinColumn(name = "BOOK_ID") + private PermissionBook book; +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionBook.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionBook.java new file mode 100644 index 0000000000..3f662e6789 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/PermissionBook.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package example.models.jpa; + +import com.yahoo.elide.annotation.Include; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * Model for books. + */ +@Entity +@Table(name = "permissionbook") +@Include(name = "permissionbook", description = "A Book") +@Getter +@Setter +public class PermissionBook { + @Id + private long id; + private String title; + + @OneToMany(mappedBy = "book") + private List authorBooks; +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/AsyncTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/AsyncTest.java index 2a36f7cba1..06be673a9d 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/AsyncTest.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/AsyncTest.java @@ -289,6 +289,7 @@ public void apiDocsDocumentTest() { .body("tags.name", containsInAnyOrder("group", "argument", "metric", "dimension", "column", "table", "asyncQuery", "timeDimensionGrain", "timeDimension", "product", "playerCountry", "version", "playerStats", - "stats", "tableExport", "namespace", "tableSource", "maintainer", "book", "publisher", "person")); + "stats", "tableExport", "namespace", "tableSource", "maintainer", "book", "publisher", "person", + "permissionauthor", "permissionbook", "permissionauthorBook")); } } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/ControllerTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/ControllerTest.java index 5ebfb1f3cf..9ae2f6a33d 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/ControllerTest.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/ControllerTest.java @@ -41,6 +41,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -76,8 +77,9 @@ @Sql( executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/test_init.sql", - statements = "INSERT INTO ArtifactGroup (name, commonName, description, deprecated) VALUES\n" - + "\t\t('com.example.repository','Example Repository','The code for this project', false);" + statements = """ + INSERT INTO ArtifactGroup (name, commonName, description, deprecated) VALUES ('com.example.repository','Example Repository','The code for this project', false); + """ ) @TestPropertySource( properties = { @@ -365,6 +367,274 @@ public void jsonApiPatchExtensionTest() { assertEquals(deleteExpected, deleteResult); } + /** + * Tests that relationship updates have inline checks deferred. + * + * @throws JsonMappingException the exception + * @throws JsonProcessingException the exception + */ + @Test + public void jsonApiPatchExtensionPermissionTest() throws JsonMappingException, JsonProcessingException { + // Setup + ExtractableResponse response = given() + .contentType(JsonApiController.JSON_API_PATCH_CONTENT_TYPE) + .accept(JsonApiController.JSON_API_PATCH_CONTENT_TYPE) + .body( + patchSet( + patchOperation(add, "/permissionauthor", + resource( + type("permissionauthor"), + id("7f14b0c3-d0ed-436c-9843-6bfbf7342df6"), + attributes( + attr("name", "Ernest Hemingway") + ) + ) + ), + patchOperation(add, "/permissionbook", + resource( + type("permissionbook"), + id("1"), + attributes( + attr("title", "The Old Man and the Sea") + ) + ) + ), + patchOperation(add, "/permissionbook", + resource( + type("permissionbook"), + id("2"), + attributes( + attr("title", "For Whom the Bell Tolls") + ) + ) + ), + patchOperation(add, "/permissionauthor/7f14b0c3-d0ed-436c-9843-6bfbf7342df6/authorBooks", + resource( + type("permissionauthorBook"), + id("e2d0e822-d94d-4e36-a592-ef4f2af89f75"), + relationships( + relation("book", + true, + resource( + type("permissionbook"), + id("1"))) + ) + ) + ) + ) + ) + .when() + .patch("/json") + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + String result = response.asString(); + assertNotNull(result); + String authorId = response.path("[0].data.id"); + String authorBookId = response.path("[3].data.id"); + + // Test inline checks deferred + // See AuthorHasUserAccessibleBooks + String path = "/permissionauthor/1/authorBooks"; + path = path.replace("1", authorId); + ExtractableResponse updateResponse = given() + .contentType(JsonApiController.JSON_API_PATCH_CONTENT_TYPE) + .accept(JsonApiController.JSON_API_PATCH_CONTENT_TYPE) + .body( + patchSet( + patchOperation(add, path, + resource( + type("permissionauthorBook"), + id("bd71bc1a-dc41-4adb-bf17-610149429f68"), + relationships( + relation("book", + true, + resource( + type("permissionbook"), + id("2"))) + ) + ) + ), + patchOperation(remove, path, + resource( + type("permissionauthorBook"), + id(authorBookId) + ) + ) + ) + ) + .when() + .patch("/json") + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + String deleteResult = updateResponse.asString(); + assertNotNull(deleteResult); + + // Cleanup + ExtractableResponse cleanupResponse = given() + .contentType(JsonApiController.JSON_API_PATCH_CONTENT_TYPE) + .accept(JsonApiController.JSON_API_PATCH_CONTENT_TYPE) + .body( + patchSet( + patchOperation(remove, "/permissionbook", + resource( + type("permissionbook"), + id("1") + ) + ), + patchOperation(remove, "/permissionbook", + resource( + type("permissionbook"), + id("2") + ) + ) + ) + ) + .when() + .patch("/json") + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + String cleanupResult = cleanupResponse.asString(); + String cleanupExpected = """ + [{"data":null},{"data":null}]"""; + assertEquals(cleanupExpected, cleanupResult); + } + + /** + * Tests that relationship updates have inline checks deferred. + */ + @Test + public void jsonApiAtomicOperationsExtensionPermissionTest() { + // Setup + ExtractableResponse response = given() + .contentType(JsonApiController.JSON_API_ATOMIC_OPERATIONS_CONTENT_TYPE) + .accept(JsonApiController.JSON_API_ATOMIC_OPERATIONS_CONTENT_TYPE) + .body( + atomicOperations( + atomicOperation(AtomicOperationCode.add, "/permissionauthor", + datum(resource( + type("permissionauthor"), + lid("7f14b0c3-d0ed-436c-9843-6bfbf7342df6"), + attributes( + attr("name", "Ernest Hemingway") + ) + )) + ), + atomicOperation(AtomicOperationCode.add, "/permissionbook", + datum(resource( + type("permissionbook"), + id("1"), + attributes( + attr("title", "The Old Man and the Sea") + ) + )) + ), + atomicOperation(AtomicOperationCode.add, "/permissionbook", + datum(resource( + type("permissionbook"), + id("2"), + attributes( + attr("title", "For Whom the Bell Tolls") + ) + )) + ), + atomicOperation(AtomicOperationCode.add, "/permissionauthor/7f14b0c3-d0ed-436c-9843-6bfbf7342df6/authorBooks", + datum(resource( + type("permissionauthorBook"), + lid("e2d0e822-d94d-4e36-a592-ef4f2af89f75"), + relationships( + relation("book", + true, + resource( + type("permissionbook"), + id("1"))) + ) + )) + ) + ) + ) + .when() + .post("/json/operations") + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + + String result = response.asString(); + String expected = """ + {"atomic:results":[{"data":{"type":"permissionauthor","id":"1","attributes":{"name":"Ernest Hemingway"},"relationships":{"authorBooks":{"links":{"self":"https://elide.io/json/permissionauthor/1/relationships/authorBooks","related":"https://elide.io/json/permissionauthor/1/authorBooks"},"data":[{"type":"permissionauthorBook","id":"1"}]}},"links":{"self":"https://elide.io/json/permissionauthor/1"}}},{"data":{"type":"permissionbook","id":"1","attributes":{"title":"The Old Man and the Sea"},"relationships":{"authorBooks":{"links":{"self":"https://elide.io/json/permissionbook/1/relationships/authorBooks","related":"https://elide.io/json/permissionbook/1/authorBooks"},"data":[{"type":"permissionauthorBook","id":"1"}]}},"links":{"self":"https://elide.io/json/permissionbook/1"}}},{"data":{"type":"permissionbook","id":"2","attributes":{"title":"For Whom the Bell Tolls"},"relationships":{"authorBooks":{"links":{"self":"https://elide.io/json/permissionbook/2/relationships/authorBooks","related":"https://elide.io/json/permissionbook/2/authorBooks"},"data":[]}},"links":{"self":"https://elide.io/json/permissionbook/2"}}},{"data":{"type":"permissionauthorBook","id":"1","relationships":{"author":{"links":{"self":"https://elide.io/json/permissionauthor/1/authorBooks/1/relationships/author","related":"https://elide.io/json/permissionauthor/1/authorBooks/1/author"},"data":{"type":"permissionauthor","id":"1"}},"book":{"links":{"self":"https://elide.io/json/permissionauthor/1/authorBooks/1/relationships/book","related":"https://elide.io/json/permissionauthor/1/authorBooks/1/book"},"data":{"type":"permissionbook","id":"1"}}},"links":{"self":"https://elide.io/json/permissionauthor/1/authorBooks/1"}}}]}"""; + assertEquals(expected, result); + + // Test inline checks deferred + // See AuthorHasUserAccessibleBooks + ExtractableResponse updateResponse = given() + .contentType(JsonApiController.JSON_API_ATOMIC_OPERATIONS_CONTENT_TYPE) + .accept(JsonApiController.JSON_API_ATOMIC_OPERATIONS_CONTENT_TYPE) + .body( + atomicOperations( + atomicOperation(AtomicOperationCode.add, "/permissionauthor/1/authorBooks", + datum(resource( + type("permissionauthorBook"), + lid("bd71bc1a-dc41-4adb-bf17-610149429f68"), + relationships( + relation("book", + true, + resource( + type("permissionbook"), + id("2"))) + ) + )) + ), + atomicOperation(AtomicOperationCode.remove, "/permissionauthor/1/authorBooks", + datum(resource( + type("permissionauthorBook"), + id("1") + )) + ) + ) + ) + .when() + .post("/json/operations") + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + String deleteResult = updateResponse.asString(); + String deleteExpected = """ + {"atomic:results":[{"data":{"type":"permissionauthorBook","id":"2","relationships":{"author":{"links":{"self":"https://elide.io/json/permissionauthor/1/authorBooks/2/relationships/author","related":"https://elide.io/json/permissionauthor/1/authorBooks/2/author"},"data":{"type":"permissionauthor","id":"1"}},"book":{"links":{"self":"https://elide.io/json/permissionauthor/1/authorBooks/2/relationships/book","related":"https://elide.io/json/permissionauthor/1/authorBooks/2/book"},"data":{"type":"permissionbook","id":"2"}}},"links":{"self":"https://elide.io/json/permissionauthor/1/authorBooks/2"}}},{"data":null}]}"""; + assertEquals(deleteExpected, deleteResult); + + // Cleanup + ExtractableResponse cleanupResponse = given() + .contentType(JsonApiController.JSON_API_ATOMIC_OPERATIONS_CONTENT_TYPE) + .accept(JsonApiController.JSON_API_ATOMIC_OPERATIONS_CONTENT_TYPE) + .body( + atomicOperations( + atomicOperation(AtomicOperationCode.remove, "/permissionbook", + datum(resource( + type("permissionbook"), + id("1") + )) + ), + atomicOperation(AtomicOperationCode.remove, "/permissionbook", + datum(resource( + type("permissionbook"), + id("2") + )) + ) + ) + ) + .when() + .post("/json/operations") + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + String cleanupResult = cleanupResponse.asString(); + String cleanupExpected = """ + {"atomic:results":[{"data":null},{"data":null}]}"""; + assertEquals(cleanupExpected, cleanupResult); + } + @Test public void jsonApiAtomicOperationsExtensionTest() { ExtractableResponse response = given() @@ -1085,7 +1355,8 @@ public void apiDocsDocumentTest() { .body("tags.name", containsInAnyOrder("group", "argument", "metric", "dimension", "column", "table", "asyncQuery", "timeDimensionGrain", "timeDimension", "product", "playerCountry", "version", "playerStats", - "stats", "namespace", "tableSource", "maintainer", "book", "publisher", "person")); + "stats", "namespace", "tableSource", "maintainer", "book", "publisher", "person", + "permissionauthor", "permissionbook", "permissionauthorBook")); } @Test diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DisableAggStoreControllerTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DisableAggStoreControllerTest.java index 8d0ee4efe8..c4ed1285e0 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DisableAggStoreControllerTest.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DisableAggStoreControllerTest.java @@ -30,6 +30,6 @@ public void apiDocsDocumentTest() { .then() .statusCode(HttpStatus.SC_OK) .body("tags.name", containsInAnyOrder("group", "asyncQuery", "product", "version", "maintainer", "book", - "publisher", "person")); + "publisher", "person", "permissionauthor", "permissionbook", "permissionauthorBook")); } } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DisableMetaDataStoreControllerTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DisableMetaDataStoreControllerTest.java index 153d8d6525..faa748eaef 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DisableMetaDataStoreControllerTest.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DisableMetaDataStoreControllerTest.java @@ -31,6 +31,7 @@ public void apiDocsDocumentTest() { .then() .statusCode(HttpStatus.SC_OK) .body("tags.name", containsInAnyOrder("playerCountry", "version", - "asyncQuery", "playerStats", "stats", "product", "group", "maintainer", "book", "publisher", "person")); + "asyncQuery", "playerStats", "stats", "product", "group", "maintainer", "book", "publisher", + "person", "permissionauthor", "permissionbook", "permissionauthorBook")); } } diff --git a/elide-test/src/main/java/com/yahoo/elide/test/jsonapi/JsonApiDSL.java b/elide-test/src/main/java/com/yahoo/elide/test/jsonapi/JsonApiDSL.java index b4c0b5d3f4..ac7fea3213 100644 --- a/elide-test/src/main/java/com/yahoo/elide/test/jsonapi/JsonApiDSL.java +++ b/elide-test/src/main/java/com/yahoo/elide/test/jsonapi/JsonApiDSL.java @@ -190,13 +190,25 @@ public static Resource resource(Type type, Lid lid, Attributes attributes) { * * @param type the type * @param id the id - * @param relationships the attributes + * @param relationships the relationships * @return the resource */ public static Resource resource(Type type, Id id, Relationships relationships) { return new Resource(id, type, null, null, relationships); } + /** + * Resource resource. + * + * @param type the type + * @param lid the lid + * @param relationships the relationships + * @return the resource + */ + public static Resource resource(Type type, Lid lid, Relationships relationships) { + return new Resource(lid, type, null, null, relationships); + } + /** * Resource resource. *