Skip to content

Commit b1e15d8

Browse files
committed
feat(elide): limit page size based on role
1 parent 9b4ca6c commit b1e15d8

10 files changed

Lines changed: 287 additions & 8 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.faforever.api.data;
2+
3+
import com.faforever.api.AbstractIntegrationTest;
4+
import com.faforever.api.data.domain.GroupPermission;
5+
import com.faforever.api.error.ErrorCode;
6+
import org.junit.jupiter.api.Test;
7+
import org.springframework.test.context.jdbc.Sql;
8+
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
9+
import org.springframework.test.web.servlet.MvcResult;
10+
11+
import static org.hamcrest.Matchers.hasSize;
12+
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
13+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
14+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
15+
16+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/truncateTables.sql")
17+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepDefaultData.sql")
18+
@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepModData.sql")
19+
class ElidePageSizeTest extends AbstractIntegrationTest {
20+
21+
@Test
22+
void normalUserValidPageSize() throws Exception {
23+
mockMvc.perform(
24+
get("/data/mod")
25+
.queryParam("page[size]", "100")
26+
.with(getOAuthTokenWithActiveUser(NO_SCOPE, GroupPermission.ROLE_USER))
27+
)
28+
.andExpect(status().isOk())
29+
.andExpect(jsonPath("$.data").isArray());
30+
}
31+
32+
@Test
33+
void normalUserInvalidPageSize() throws Exception {
34+
final MvcResult result = mockMvc.perform(
35+
get("/data/mod")
36+
.queryParam("page[limit]", "101")
37+
.with(getOAuthTokenWithActiveUser(NO_SCOPE, GroupPermission.ROLE_USER))
38+
)
39+
.andExpect(status().isUnprocessableEntity()).andReturn();
40+
41+
assertApiError(result, ErrorCode.QUERY_INVALID_PAGE_SIZE);
42+
}
43+
44+
@Test
45+
void scraperUserValidPageSize() throws Exception {
46+
mockMvc.perform(
47+
get("/data/mod")
48+
.queryParam("page[size]", "9000")
49+
.with(getOAuthTokenWithActiveUser(NO_SCOPE, GroupPermission.ROLE_SCRAPER))
50+
)
51+
.andExpect(status().isOk())
52+
.andExpect(jsonPath("$.data").isArray());
53+
}
54+
55+
@Test
56+
void scraperUserInvalidPageSize() throws Exception {
57+
final MvcResult result = mockMvc.perform(
58+
get("/data/mod")
59+
.queryParam("page[limit]", "10001")
60+
.with(getOAuthTokenWithActiveUser(NO_SCOPE, GroupPermission.ROLE_SCRAPER))
61+
)
62+
.andExpect(status().isUnprocessableEntity()).andReturn();
63+
64+
assertApiError(result, ErrorCode.QUERY_INVALID_PAGE_SIZE);
65+
}
66+
67+
@Test
68+
void nanPageSize() throws Exception {
69+
mockMvc.perform(
70+
get("/data/mod")
71+
.queryParam("page[limit]", "invalid-not-a-number")
72+
.with(getOAuthTokenWithActiveUser(NO_SCOPE, GroupPermission.ROLE_SCRAPER))
73+
)
74+
.andExpect(status().isBadRequest())
75+
.andExpect(jsonPath("$.errors[*]", hasSize(1)));
76+
}
77+
}

src/inttest/resources/config/application.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,6 @@ logging:
110110
level:
111111
org.hibernate.SQL: DEBUG
112112
org.hibernate.engine.spi.EntityEntry: TRACE
113+
114+
elide:
115+
default-page-size: 100

src/main/java/com/faforever/api/FafApiApplication.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.faforever.api.config.FafApiProperties;
44
import com.yahoo.elide.spring.config.ElideAutoConfiguration;
5+
import com.yahoo.elide.spring.config.ElideConfigProperties;
56
import org.springframework.boot.SpringApplication;
67
import org.springframework.boot.autoconfigure.SpringBootApplication;
78
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -11,7 +12,7 @@
1112
exclude = {ElideAutoConfiguration.class}
1213
)
1314
@EnableTransactionManagement
14-
@EnableConfigurationProperties({FafApiProperties.class})
15+
@EnableConfigurationProperties({FafApiProperties.class, ElideConfigProperties.class})
1516
public class FafApiApplication {
1617

1718
public static void main(String[] args) {

src/main/java/com/faforever/api/config/elide/ElideConfig.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.yahoo.elide.jsonapi.JsonApi;
1717
import com.yahoo.elide.jsonapi.JsonApiMapper;
1818
import com.yahoo.elide.jsonapi.JsonApiSettings;
19+
import com.yahoo.elide.spring.config.ElideConfigProperties;
1920
import org.apache.commons.beanutils.ConvertUtils;
2021
import org.apache.commons.beanutils.Converter;
2122
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
@@ -42,7 +43,8 @@ MultiplexManager multiplexDataStore(
4243
}
4344

4445
@Bean
45-
public Elide elide(DataStore multiplexDataStore, ObjectMapper objectMapper, EntityDictionary entityDictionary, ExtendedAuditLogger extendedAuditLogger) {
46+
public Elide elide(DataStore multiplexDataStore, ObjectMapper objectMapper, EntityDictionary entityDictionary, ExtendedAuditLogger extendedAuditLogger,
47+
ElideConfigProperties elideConfigProperties) {
4648
RSQLFilterDialect rsqlFilterDialect = new RSQLFilterDialect(entityDictionary, new CaseSensitivityStrategy.UseColumnCollation(), true);
4749

4850
registerAdditionalConverters();
@@ -54,6 +56,8 @@ public Elide elide(DataStore multiplexDataStore, ObjectMapper objectMapper, Enti
5456
.joinFilterDialect(rsqlFilterDialect)
5557
.subqueryFilterDialect(rsqlFilterDialect)
5658
)
59+
.maxPageSize(elideConfigProperties.getMaxPageSize())
60+
.defaultPageSize(elideConfigProperties.getDefaultPageSize())
5761
.auditLogger(extendedAuditLogger)
5862
.entityDictionary(entityDictionary)
5963
.build();

src/main/java/com/faforever/api/data/DataController.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.faforever.api.data;
22

3+
import com.faforever.api.data.util.ElidePageSizeUtil;
34
import com.faforever.api.security.ElideUser;
45
import com.yahoo.elide.ElideResponse;
6+
import com.yahoo.elide.ElideSettings;
57
import com.yahoo.elide.core.request.route.Route;
68
import com.yahoo.elide.core.security.User;
79
import com.yahoo.elide.jsonapi.JsonApi;
10+
import lombok.RequiredArgsConstructor;
811
import org.springframework.cache.annotation.Cacheable;
912
import org.springframework.cache.interceptor.KeyGenerator;
1013
import org.springframework.http.HttpHeaders;
@@ -40,17 +43,14 @@
4043
*/
4144
@RestController
4245
@RequestMapping(path = DataController.PATH_PREFIX)
46+
@RequiredArgsConstructor
4347
public class DataController {
4448

4549
public static final String PATH_PREFIX = "/data";
4650
public static final String API_VERSION = "";
4751

4852
private final JsonApi jsonApi;
4953

50-
public DataController(JsonApi jsonApi) {
51-
this.jsonApi = jsonApi;
52-
}
53-
5454
//!!! No @Transactional - transactions are being handled by Elide
5555
@GetMapping(value = {"/{entity}", "/{entity}/**"}, produces = JSON_API_MEDIA_TYPE)
5656
@Cacheable(cacheResolver = "elideCacheResolver", keyGenerator = GetCacheKeyGenerator.NAME)
@@ -60,14 +60,17 @@ public ResponseEntity<String> get(
6060
final HttpServletRequest request,
6161
final Authentication authentication
6262
) {
63+
final User principal = getPrincipal(authentication);
64+
validatePageSize(allRequestParams, principal);
65+
6366
ElideResponse<String> response = jsonApi.get(
6467
Route.builder()
6568
.baseUrl(getBaseUrlEndpoint())
6669
.path(getJsonApiPath(request))
6770
.apiVersion(API_VERSION)
6871
.parameters(allRequestParams)
6972
.build(),
70-
getPrincipal(authentication),
73+
principal,
7174
UUID.randomUUID()
7275
);
7376
return wrapResponse(response);
@@ -179,6 +182,14 @@ private static User getPrincipal(final Authentication authentication) {
179182
return new ElideUser(authentication);
180183
}
181184

185+
private void validatePageSize(MultiValueMap<String, String> allRequestParams, User principal) {
186+
final ElideSettings elideSettings = jsonApi.getElide().getElideSettings();
187+
final int defaultPageSize = elideSettings.getDefaultPageSize();
188+
final int maxPageSize = elideSettings.getMaxPageSize();
189+
190+
ElidePageSizeUtil.validatePageSize(allRequestParams, principal, defaultPageSize, maxPageSize);
191+
}
192+
182193
private ResponseEntity<String> wrapResponse(ElideResponse<String> response) {
183194
return ResponseEntity.status(response.getStatus()).body(response.getBody());
184195
}

src/main/java/com/faforever/api/data/domain/GroupPermission.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class GroupPermission extends AbstractEntity<GroupPermission> implements
5959
public static final String ROLE_ADMIN_MOD = "ADMIN_MOD";
6060
public static final String ROLE_WRITE_MESSAGE = "WRITE_MESSAGE";
6161
public static final String ROLE_USER = "USER";
62+
public static final String ROLE_SCRAPER = "SCRAPER";
6263

6364
private String technicalName;
6465
private String nameKey;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.faforever.api.data.util;
2+
3+
import com.faforever.api.error.ApiException;
4+
import com.faforever.api.error.ErrorCode;
5+
import com.yahoo.elide.core.security.User;
6+
import lombok.experimental.UtilityClass;
7+
import org.apache.commons.lang3.StringUtils;
8+
import org.springframework.util.MultiValueMap;
9+
10+
import java.util.Optional;
11+
12+
import static com.faforever.api.data.domain.GroupPermission.ROLE_SCRAPER;
13+
14+
@UtilityClass
15+
public class ElidePageSizeUtil {
16+
17+
static final String PAGE_SIZE_PARAM = "page[size]";
18+
static final String PAGE_LIMIT_PARAM = "page[limit]";
19+
20+
21+
public void validatePageSize(final MultiValueMap<String, String> allRequestParams,
22+
final User principal,
23+
final int elideDefaultPageSize,
24+
final int elideMaxPageSize) {
25+
26+
final int rolePageSizeLimit = getRolePageSizeLimit(principal, elideDefaultPageSize,
27+
elideMaxPageSize);
28+
final int pageSizeInRequest = getParamsPageSize(allRequestParams).orElse(elideDefaultPageSize);
29+
30+
if (pageSizeInRequest > rolePageSizeLimit) {
31+
throw ApiException.of(ErrorCode.QUERY_INVALID_PAGE_SIZE, pageSizeInRequest,
32+
rolePageSizeLimit);
33+
}
34+
}
35+
36+
private int getRolePageSizeLimit(final User principal, final int elideDefaultPageSize,
37+
final int elideMaxPageSize) {
38+
if (principal.isInRole(ROLE_SCRAPER)) {
39+
return elideMaxPageSize;
40+
}
41+
return elideDefaultPageSize;
42+
}
43+
44+
/**
45+
* @implNote Invalid numbers should be handled by Elide. That's why they are filtered out with
46+
* {@link StringUtils::isNumeric}
47+
*/
48+
private Optional<Integer> getParamsPageSize(MultiValueMap<String, String> allRequestParams) {
49+
if (allRequestParams.containsKey(PAGE_SIZE_PARAM)) {
50+
return Optional.ofNullable(allRequestParams.getFirst(PAGE_SIZE_PARAM))
51+
.filter(StringUtils::isNumeric).map(Integer::parseInt);
52+
} else if (allRequestParams.containsKey(PAGE_LIMIT_PARAM)) {
53+
return Optional.ofNullable(allRequestParams.getFirst(PAGE_LIMIT_PARAM))
54+
.filter(StringUtils::isNumeric).map(Integer::parseInt);
55+
}
56+
return Optional.empty();
57+
}
58+
}

src/main/java/com/faforever/api/error/ErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public enum ErrorCode {
2020
MAP_SIZE_MISSING(113, "Missing map size", "The scenario file must specify a map size."),
2121
MAP_VERSION_MISSING(114, "Missing map version", "The scenario file must specify a map version."),
2222
QUERY_INVALID_SORT_FIELD(115, "Invalid sort field", "Sorting by ''{0}'' is not supported"),
23-
QUERY_INVALID_PAGE_SIZE(116, "Invalid page size", "Page size is not valid: {0, number}"),
23+
QUERY_INVALID_PAGE_SIZE(116, "Invalid page size", "Page size is not valid: {0, number}. Maximum page size is {1, number}."),
2424
QUERY_INVALID_PAGE_NUMBER(117, "Invalid page number", "Page number is not valid: {0, number}"),
2525
MOD_NAME_MISSING(118, "Missing mod name", "The file mod_info.lua must contain a property 'name'."),
2626
MOD_UID_MISSING(119, "Missing mod UID", "The file mod_info.lua must contain a property 'uid'."),

src/main/resources/config/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ spring:
155155
jwt:
156156
issuer-uri: ${JWT_FAF_HYDRA_ISSUER:https://hydra.${FAF_DOMAIN}/}
157157

158+
elide:
159+
default-page-size: ${ELIDE_DEFAULT_PAGE_SIZE:100}
160+
max-page-size: ${ELIDE_MAX_PAGE_SIZE:10000}
161+
158162
server:
159163
# Mind that this is configured in the docker compose file as well (that is, in the gradle script that generates it)
160164
port: ${API_PORT:8010}

0 commit comments

Comments
 (0)