diff --git a/src/inttest/java/com/faforever/api/data/MapVersionElideTest.java b/src/inttest/java/com/faforever/api/data/MapVersionElideTest.java new file mode 100644 index 000000000..7c879ae1b --- /dev/null +++ b/src/inttest/java/com/faforever/api/data/MapVersionElideTest.java @@ -0,0 +1,91 @@ +package com.faforever.api.data; + +import com.faforever.api.AbstractIntegrationTest; +import org.junit.Test; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepDefaultUser.sql") +@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepMapVersion.sql") +@Sql(executionPhase = ExecutionPhase.AFTER_TEST_METHOD, scripts = "classpath:sql/cleanMapVersion.sql") +public class MapVersionElideTest extends AbstractIntegrationTest { + + private static final String MAP_VERSION_HIDE_FALSE_ID_1 = "{\n" + + " \"data\": {\n" + + " \"type\": \"mapVersion\",\n" + + " \"id\": \"1\",\n" + + " \"attributes\": {\n" + + " \t\"hidden\": false\n" + + " }\n" + + " } \n" + + "}"; + private static final String MAP_VERSION_HIDE_TRUE_ID_1 = "{\n" + + " \"data\": {\n" + + " \"type\": \"mapVersion\",\n" + + " \"id\": \"1\",\n" + + " \"attributes\": {\n" + + " \t\"hidden\": true\n" + + " }\n" + + " } \n" + + "}"; + private static final String MAP_VERSION_RANKED_FALSE_ID_1 = "{\n" + + " \"data\": {\n" + + " \"type\": \"mapVersion\",\n" + + " \"id\": \"1\",\n" + + " \"attributes\": {\n" + + " \t\"ranked\": false\n" + + " }\n" + + " } \n" + + "}"; + private static final String MAP_VERSION_RANKED_TRUE_ID_1 = "{\n" + + " \"data\": {\n" + + " \"type\": \"mapVersion\",\n" + + " \"id\": \"1\",\n" + + " \"attributes\": {\n" + + " \t\"ranked\": true\n" + + " }\n" + + " } \n" + + "}"; + + + @WithUserDetails(AUTH_USER) + @Test + public void testUpdateHideToTrueDoesWork() throws Exception { + mockMvc.perform( + patch("/data/mapVersion/1") + .content(MAP_VERSION_HIDE_TRUE_ID_1)) + .andExpect(status().isNoContent()); + } + + @WithUserDetails(AUTH_USER) + @Test + public void testUpdateHideToFalseDoesNotWork() throws Exception { + mockMvc.perform( + patch("/data/mapVersion/1") + .content(MAP_VERSION_HIDE_FALSE_ID_1)) + .andExpect(status().isForbidden()); + } + + + @WithUserDetails(AUTH_USER) + @Test + public void testUpdateRankedToFalseDoesWork() throws Exception { + mockMvc.perform( + patch("/data/mapVersion/1") + .content(MAP_VERSION_RANKED_FALSE_ID_1)) + .andExpect(status().isNoContent()); + } + + @WithUserDetails(AUTH_USER) + @Test + public void testUpdateRankedToTrueDoesNotWork() throws Exception { + mockMvc.perform( + patch("/data/mapVersion/1") + .content(MAP_VERSION_RANKED_TRUE_ID_1)) + .andExpect(status().isForbidden()); + } +} diff --git a/src/inttest/resources/sql/cleanMapVersion.sql b/src/inttest/resources/sql/cleanMapVersion.sql new file mode 100644 index 000000000..4eaf3ef5b --- /dev/null +++ b/src/inttest/resources/sql/cleanMapVersion.sql @@ -0,0 +1,2 @@ +DELETE FROM map_version; +DELETE FROM map; diff --git a/src/inttest/resources/sql/prepMapVersion.sql b/src/inttest/resources/sql/prepMapVersion.sql new file mode 100644 index 000000000..584422771 --- /dev/null +++ b/src/inttest/resources/sql/prepMapVersion.sql @@ -0,0 +1,6 @@ +DELETE FROM map_version; +DELETE FROM map; + +INSERT INTO map (id, display_name, map_type, battle_type, author) VALUES (1, 'display name', 'mtype', 'btype', 1); +INSERT INTO map_version (id, description, max_players, width, height, version, filename, map_id, hidden, ranked) +VALUES (1, 'des', 2, 2, 2, 1, 'map/ghb.zip', 1, 0, 1); diff --git a/src/main/java/com/faforever/api/config/elide/ElideConfig.java b/src/main/java/com/faforever/api/config/elide/ElideConfig.java index 961cc8c55..64251a7ec 100644 --- a/src/main/java/com/faforever/api/config/elide/ElideConfig.java +++ b/src/main/java/com/faforever/api/config/elide/ElideConfig.java @@ -1,6 +1,7 @@ package com.faforever.api.config.elide; import com.faforever.api.config.ApplicationProfile; +import com.faforever.api.data.checks.BooleanChange; import com.faforever.api.data.checks.IsAuthenticated; import com.faforever.api.data.checks.IsClanMembershipDeletable; import com.faforever.api.data.checks.IsEntityOwner; @@ -96,7 +97,8 @@ public EntityDictionary entityDictionary() { checks.put(HasBanRead.EXPRESSION, HasBanRead.Inline.class); checks.put(HasBanUpdate.EXPRESSION, HasBanUpdate.Inline.class); checks.put(HasLadder1v1Update.EXPRESSION, HasLadder1v1Update.Inline.class); - + checks.put(BooleanChange.TO_FALSE_EXPRESSION, BooleanChange.ToFalse.class); + checks.put(BooleanChange.TO_TRUE_EXPRESSION, BooleanChange.ToTrue.class); return new EntityDictionary(checks); } } diff --git a/src/main/java/com/faforever/api/data/checks/BooleanChange.java b/src/main/java/com/faforever/api/data/checks/BooleanChange.java new file mode 100644 index 000000000..8c6d05ec2 --- /dev/null +++ b/src/main/java/com/faforever/api/data/checks/BooleanChange.java @@ -0,0 +1,46 @@ +package com.faforever.api.data.checks; + +import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.RequestScope; +import com.yahoo.elide.security.checks.OperationCheck; + +import java.util.Optional; + +public class BooleanChange { + + public static final String TO_FALSE_EXPRESSION = "boolean changed to false"; + public static final String TO_TRUE_EXPRESSION = "boolean changed to true"; + + + public static class ToFalse extends OperationCheck { + + @Override + public boolean ok(Object entity, RequestScope requestScope, Optional optionalChangeSpec) { + if (!optionalChangeSpec.isPresent()) { + return true; + } + ChangeSpec changeSpec = optionalChangeSpec.get(); + if (!(changeSpec.getModified() instanceof Boolean)) { + throw new IllegalStateException("This Expression can only be applied to boolean fields"); + } + return !((Boolean) changeSpec.getModified()); + } + + } + + public static class ToTrue extends OperationCheck { + + @Override + public boolean ok(Object entity, RequestScope requestScope, Optional optionalChangeSpec) { + if (!optionalChangeSpec.isPresent()) { + return true; + } + ChangeSpec changeSpec = optionalChangeSpec.get(); + if (!(changeSpec.getModified() instanceof Boolean)) { + throw new IllegalStateException("This Expression can only be applied to boolean fields"); + } + return ((Boolean) changeSpec.getModified()); + } + + } +} diff --git a/src/main/java/com/faforever/api/data/checks/IsEntityOwner.java b/src/main/java/com/faforever/api/data/checks/IsEntityOwner.java index 50dc978bb..cd145484e 100644 --- a/src/main/java/com/faforever/api/data/checks/IsEntityOwner.java +++ b/src/main/java/com/faforever/api/data/checks/IsEntityOwner.java @@ -18,6 +18,7 @@ public static class Inline extends InlineCheck { public boolean ok(OwnableEntity entity, RequestScope requestScope, Optional changeSpec) { Object opaqueUser = requestScope.getUser().getOpaqueUser(); return opaqueUser instanceof FafUserDetails + && entity.getEntityOwner() != null && entity.getEntityOwner().getId() == ((FafUserDetails) opaqueUser).getId(); } diff --git a/src/main/java/com/faforever/api/data/domain/MapVersion.java b/src/main/java/com/faforever/api/data/domain/MapVersion.java index 5a1f89453..3425ed98b 100644 --- a/src/main/java/com/faforever/api/data/domain/MapVersion.java +++ b/src/main/java/com/faforever/api/data/domain/MapVersion.java @@ -1,5 +1,7 @@ package com.faforever.api.data.domain; +import com.faforever.api.data.checks.BooleanChange; +import com.faforever.api.data.checks.IsEntityOwner; import com.faforever.api.data.checks.permission.IsModerator; import com.faforever.api.data.listeners.MapVersionEnricher; import com.yahoo.elide.annotation.Audit; @@ -28,7 +30,7 @@ @EntityListeners(MapVersionEnricher.class) @Table(name = "map_version") @Include(rootLevel = true, type = MapVersion.TYPE_NAME) -public class MapVersion extends AbstractEntity { +public class MapVersion extends AbstractEntity implements OwnableEntity { public static final String TYPE_NAME = "mapVersion"; @@ -85,14 +87,14 @@ public String getFilename() { return filename; } - @UpdatePermission(expression = IsModerator.EXPRESSION) + @UpdatePermission(expression = IsModerator.EXPRESSION + " or (" + IsEntityOwner.EXPRESSION + " and " + BooleanChange.TO_FALSE_EXPRESSION + ")") @Audit(action = Action.UPDATE, logStatement = "Updated map version `{0}` attribute ranked to: {1}", logExpressions = {"${mapVersion.id}", "${mapVersion.ranked}"}) @Column(name = "ranked") public boolean isRanked() { return ranked; } - @UpdatePermission(expression = IsModerator.EXPRESSION) + @UpdatePermission(expression = IsModerator.EXPRESSION + " or (" + IsEntityOwner.EXPRESSION + " and " + BooleanChange.TO_TRUE_EXPRESSION + ")") @Audit(action = Action.UPDATE, logStatement = "Updated map version `{0}` attribute hidden to: {1}", logExpressions = {"${mapVersion.id}", "${mapVersion.hidden}"}) @Column(name = "hidden") public boolean isHidden() { @@ -147,4 +149,10 @@ public List getReviews() { public MapVersionReviewsSummary getReviewsSummary() { return reviewsSummary; } + + @Transient + @Override + public Login getEntityOwner() { + return map.getAuthor(); + } }