Skip to content

Commit

Permalink
Per-entity CRUD filters for update/delete operations #502
Browse files Browse the repository at this point in the history
.. create/update/delete authorizers
  • Loading branch information
andrus committed Dec 5, 2021
1 parent 08a7743 commit 2b8cd60
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ protected <T extends DataObject> void collectCreateOps(
List<ChangeOperation<T>> createOps = new ArrayList<>(noKeyCreate.size() + withKeyCreate.size());

for (EntityUpdate<T> u : noKeyCreate) {
createOps.add(new ChangeOperation<>(ChangeOperationType.CREATE, null, u));
createOps.add(new ChangeOperation<>(ChangeOperationType.CREATE, u.getEntity(), null, u));
}

for (EntityUpdate<T> u : withKeyCreate) {
createOps.add(new ChangeOperation<>(ChangeOperationType.CREATE, null, u));
createOps.add(new ChangeOperation<>(ChangeOperationType.CREATE, u.getEntity(), null, u));
}

context.setChangeOperations(ChangeOperationType.CREATE, createOps);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ public class CayenneMapCreateStage extends CayenneMapChangesStage {
@Override
protected <T extends DataObject> void map(UpdateContext<T> context) {

List<ChangeOperation<T>> ops = new ArrayList<>();
List<ChangeOperation<T>> ops = new ArrayList<>(context.getUpdates().size());
for (EntityUpdate<T> u : context.getUpdates()) {

// TODO: when EntityUpdate contains id, there may be multiple updates for the same key
// that need to be merged in a single operation to avoid commit errors... I suppose for
// now the users must use "createOrUpdate" if that's anticipated instead of "create"
ops.add(new ChangeOperation<>(ChangeOperationType.CREATE, null, u));
ops.add(new ChangeOperation<>(ChangeOperationType.CREATE, u.getEntity(), null, u));
}

context.setChangeOperations(ChangeOperationType.CREATE, ops);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public class CayenneMapIdempotentFullSyncStage extends CayenneMapIdempotentCreat

@Override
protected <T extends DataObject> void collectUpdateDeleteOps(
UpdateContext<T> context, ObjectMapper<T> mapper,
UpdateContext<T> context,
ObjectMapper<T> mapper,
UpdateMap<T> updateMap) {

List<T> existing = existingObjects(context, updateMap.getIds(), mapper);
Expand All @@ -39,12 +40,12 @@ protected <T extends DataObject> void collectUpdateDeleteOps(
for (T o : existing) {
Object key = mapper.keyForObject(o);

EntityUpdate<T> updates = updateMap.remove(key);
EntityUpdate<T> update = updateMap.remove(key);

if (updates == null) {
deleteOps.add(new ChangeOperation<>(ChangeOperationType.DELETE, o, null));
if (update == null) {
deleteOps.add(new ChangeOperation<>(ChangeOperationType.DELETE, context.getEntity().getAgEntity(), o, null));
} else {
updateOps.add(new ChangeOperation<>(ChangeOperationType.UPDATE, o, updates));
updateOps.add(new ChangeOperation<>(ChangeOperationType.UPDATE, update.getEntity(), o, update));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ protected <T extends DataObject> void collectUpdateDeleteOps(
throw AgException.internalServerError("Invalid key item: %s", key);
}

updateOps.add(new ChangeOperation<>(ChangeOperationType.UPDATE, o, update));
updateOps.add(new ChangeOperation<>(ChangeOperationType.UPDATE, update.getEntity(), o, update));
}

context.setChangeOperations(ChangeOperationType.UPDATE, updateOps);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.agrest.cayenne;

import io.agrest.Ag;
import io.agrest.EntityUpdate;
import io.agrest.SimpleResponse;
import io.agrest.cayenne.cayenne.main.E2;
import io.agrest.cayenne.cayenne.main.E3;
import io.agrest.cayenne.cayenne.main.E4;
import io.agrest.cayenne.unit.AgCayenneTester;
import io.agrest.cayenne.unit.DbTest;
import io.agrest.meta.AgEntity;
import io.bootique.junit5.BQTestTool;
import org.junit.jupiter.api.Test;

import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import java.util.List;

public class PUT_CreateAuthorizerIT extends DbTest {

@BQTestTool
static final AgCayenneTester tester = tester(Resource.class)
.entities(E2.class, E3.class, E4.class)
.agCustomizer(ab -> ab
.entityOverlay(AgEntity.overlay(E2.class).createAuthorizer(u -> !"blocked".equals(u.getValues().get("name"))))
).build();

@Test
public void testInStack_Allowed() {

tester.target("/e2_stack_authorizer")
.put("[{\"name\":\"Bb\"},{\"name\":\"Aa\"}]")
.wasCreated();

tester.e2().matcher().assertMatches(2);
tester.e2().matcher().eq("name", "Aa").assertOneMatch();
tester.e2().matcher().eq("name", "Bb").assertOneMatch();
}

@Test
public void testInStack_Blocked() {

tester.target("/e2_stack_authorizer")
.put("[{\"name\":\"Bb\"},{\"name\":\"blocked\"}]")
.wasForbidden();

tester.e2().matcher().assertNoMatches();
}

@Test
public void testInRequestAndStack_Allowed() {

tester.target("/e2_request_and_stack_authorizer/not_this")
.put("[{\"name\":\"Bb\"},{\"name\":\"Aa\"}]")
.wasCreated();

tester.e2().matcher().assertMatches(2);
tester.e2().matcher().eq("name", "Aa").assertOneMatch();
tester.e2().matcher().eq("name", "Bb").assertOneMatch();
}

@Test
public void testInRequestAndStack_Blocked() {

tester.target("/e2_request_and_stack_authorizer/not_this")
.put("[{\"name\":\"Bb\"},{\"name\":\"blocked\"}]")
.wasForbidden();

tester.e2().matcher().assertNoMatches();

tester.target("/e2_request_and_stack_authorizer/not_this")
.put("[{\"name\":\"not_this\"},{\"name\":\"Aa\"}]")
.wasForbidden();

tester.e2().matcher().assertNoMatches();
}

@Path("")
public static class Resource {

@Context
private Configuration config;

@PUT
@Path("e2_stack_authorizer")
public SimpleResponse putE2StackFilter(@Context UriInfo uriInfo, List<EntityUpdate<E2>> updates) {
return Ag.service(config).createOrUpdate(E2.class).uri(uriInfo).sync(updates);
}

@PUT
@Path("e2_request_and_stack_authorizer/{name}")
public SimpleResponse putE2RequestAndStackFilter(
@Context UriInfo uriInfo,
@PathParam("name") String name,
List<EntityUpdate<E2>> updates) {

return Ag.service(config).createOrUpdate(E2.class)
.uri(uriInfo)
.createAuthorizer(E2.class, u -> !name.equals(u.getValues().get("name")))
.sync(updates);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package io.agrest.cayenne;

import io.agrest.Ag;
import io.agrest.EntityUpdate;
import io.agrest.SimpleResponse;
import io.agrest.cayenne.cayenne.main.E2;
import io.agrest.cayenne.cayenne.main.E3;
import io.agrest.cayenne.cayenne.main.E4;
import io.agrest.cayenne.unit.AgCayenneTester;
import io.agrest.cayenne.unit.DbTest;
import io.agrest.meta.AgEntity;
import io.bootique.junit5.BQTestTool;
import org.junit.jupiter.api.Test;

import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Context;
import java.util.List;

public class PUT_DeleteAuthorizerIT extends DbTest {

@BQTestTool
static final AgCayenneTester tester = tester(Resource.class)
.entities(E2.class, E3.class, E4.class)
.agCustomizer(ab -> ab
.entityOverlay(AgEntity.overlay(E2.class).deleteAuthorizer(o -> !"dont_delete".equals(o.getName())))
).build();

@Test
public void testInStack_Allowed() {

tester.e2().insertColumns("id_", "name")
.values(1, "a")
.values(2, "b")
.exec();

tester.target("/e2_stack_authorizer")
.put("[{\"id\":2,\"name\":\"Bb\"}]")
.wasOk();

tester.e2().matcher().assertMatches(1);
tester.e2().matcher().eq("name", "Bb").assertOneMatch();
}

@Test
public void testInStack_Blocked() {
tester.e2().insertColumns("id_", "name")
.values(1, "dont_delete")
.values(2, "b")
.exec();

tester.target("/e2_stack_authorizer")
.put("[{\"id\":2,\"name\":\"Bb\"}]")
.wasForbidden();

tester.e2().matcher().assertMatches(2);
tester.e2().matcher().eq("name", "dont_delete").assertOneMatch();
tester.e2().matcher().eq("name", "b").assertOneMatch();
}

@Test
public void testInRequestAndStack_Allowed() {

tester.e2().insertColumns("id_", "name")
.values(1, "a")
.values(2, "b")
.exec();

tester.target("/e2_request_and_stack_authorizer/not_this")
.put("[{\"id\":2,\"name\":\"Bb\"}]")
.wasOk();

tester.e2().matcher().assertMatches(1);
tester.e2().matcher().eq("name", "Bb").assertOneMatch();
}

@Test
public void testInRequestAndStack_Blocked() {
tester.e2().insertColumns("id_", "name")
.values(1, "dont_delete_this_either")
.values(2, "b")
.exec();

tester.target("/e2_request_and_stack_authorizer/dont_delete_this_either")
.put("[{\"id\":2,\"name\":\"Bb\"}]")
.wasForbidden();

tester.e2().matcher().assertMatches(2);
tester.e2().matcher().eq("name", "dont_delete_this_either").assertOneMatch();
tester.e2().matcher().eq("name", "b").assertOneMatch();
}

@Path("")
public static class Resource {

@Context
private Configuration config;

@PUT
@Path("e2_stack_authorizer")
public SimpleResponse putE2StackFilter(List<EntityUpdate<E2>> updates) {
return Ag.service(config).idempotentFullSync(E2.class).sync(updates);
}

@PUT
@Path("e2_request_and_stack_authorizer/{name}")
public SimpleResponse putE2RequestAndStackFilter(
@PathParam("name") String name,
List<EntityUpdate<E2>> updates) {

return Ag.service(config)
.idempotentFullSync(E2.class)
.deleteAuthorizer(E2.class, o -> !name.equals(o.getName()))
.sync(updates);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ static <T extends DataObject> ReadFilter<T> oddFilter() {
}

@Test
public void testFilter_InStack() {
public void testInStack() {

tester.e2().insertColumns("id_", "name")
.values(1, "a")
Expand All @@ -66,7 +66,7 @@ public void testFilter_InStack() {
}

@Test
public void testFilter_InStack_Nested() {
public void testInStack_Nested() {

tester.e2().insertColumns("id_", "name")
.values(1, "a")
Expand All @@ -88,8 +88,7 @@ public void testFilter_InStack_Nested() {
}

@Test
public void testFilter_InStackAndRequest() {

public void testInStackAndRequest() {

tester.e2().insertColumns("id_", "name")
.values(1, "a")
Expand Down
Loading

0 comments on commit 2b8cd60

Please sign in to comment.