SEBSERV-178 added request reate limits for user reg page

This commit is contained in:
anhefti 2021-03-11 17:24:36 +01:00
parent ed9ded57db
commit c0c5a4556b
11 changed files with 196 additions and 11 deletions

View file

@ -341,6 +341,11 @@
<artifactId>commons-text</artifactId> <artifactId>commons-text</artifactId>
<version>1.8</version> <version>1.8</version>
</dependency> </dependency>
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>4.10.0</version>
</dependency>
<!-- Testing --> <!-- Testing -->

View file

@ -0,0 +1,13 @@
/*
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.api;
public class TooManyRegistrations {
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.api;
public class TooManyRequests extends RuntimeException {
private static final long serialVersionUID = 3303246002774619224L;
public static enum Code {
INCOMMING,
REGISTRATION
}
public final Code code;
public TooManyRequests() {
super("TooManyRequests");
this.code = Code.INCOMMING;
}
public TooManyRequests(final Code code) {
super("TooManyRequests");
this.code = code;
}
}

View file

@ -232,7 +232,7 @@ public class RegisterPage implements TemplateComposer {
registerButton.addListener(SWT.Selection, event -> { registerButton.addListener(SWT.Selection, event -> {
registerForm.getForm().clearErrors(); registerForm.getForm().clearErrors();
final Result<UserInfo> onError = this.pageService final Result<UserInfo> result = this.pageService
.getRestService() .getRestService()
.getBuilder(RegisterNewUser.class) .getBuilder(RegisterNewUser.class)
.withRestTemplate(this.restTemplate) .withRestTemplate(this.restTemplate)
@ -240,7 +240,7 @@ public class RegisterPage implements TemplateComposer {
.call() .call()
.onError(registerForm::handleError); .onError(registerForm::handleError);
if (onError.hasError()) { if (result.hasError()) {
return; return;
} }

View file

@ -18,6 +18,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.TooManyRequests;
import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
@ -38,6 +39,8 @@ public class FormHandle<T extends Entity> {
private static final Logger log = LoggerFactory.getLogger(FormHandle.class); private static final Logger log = LoggerFactory.getLogger(FormHandle.class);
public static final String FIELD_VALIDATION_LOCTEXT_PREFIX = "sebserver.form.validation.fieldError."; public static final String FIELD_VALIDATION_LOCTEXT_PREFIX = "sebserver.form.validation.fieldError.";
static final LocTextKey MESSAGE_TOO_MANY_REQUESTS_TEXT =
new LocTextKey("sebserver.error.tooManyRequests");
private final PageService pageService; private final PageService pageService;
private final PageContext pageContext; private final PageContext pageContext;
@ -147,14 +150,32 @@ public class FormHandle<T extends Entity> {
fieldAccessor -> showValidationError(fieldAccessor, fve))); fieldAccessor -> showValidationError(fieldAccessor, fve)));
return true; return true;
} else { } else {
log.error("Unexpected error while trying to post form: {}", error.getMessage()); log.error("Unexpected error while trying to post form: {}", error.getMessage());
final EntityType resultType = this.post.getEntityType();
if (resultType != null) { try {
this.pageContext.notifySaveError(resultType, error);
} else { if (error instanceof TooManyRequests) {
this.pageContext.notifyError( final TooManyRequests.Code code = ((TooManyRequests) error).code;
new LocTextKey(PageContext.GENERIC_SAVE_ERROR_TEXT_KEY, Constants.EMPTY_NOTE), if (code != null) {
error); this.pageContext.publishInfo(new LocTextKey(MESSAGE_TOO_MANY_REQUESTS_TEXT.name + "." + code));
} else {
this.pageContext.publishInfo(MESSAGE_TOO_MANY_REQUESTS_TEXT);
}
return false;
}
final EntityType resultType = this.post.getEntityType();
if (resultType != null) {
this.pageContext.notifySaveError(resultType, error);
} else {
this.pageContext.notifyError(
new LocTextKey(PageContext.GENERIC_SAVE_ERROR_TEXT_KEY, Constants.EMPTY_NOTE),
error);
}
} catch (final Exception e) {
log.error("Failed to handle error: ", e);
} }
return false; return false;

View file

@ -40,6 +40,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.api.TooManyRequests;
import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder; import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
@ -134,6 +135,15 @@ public abstract class RestCall<T> {
} }
} catch (final RestClientResponseException responseError) { } catch (final RestClientResponseException responseError) {
if (responseError.getRawStatusCode() == HttpStatus.TOO_MANY_REQUESTS.value()) {
final String code = responseError.getResponseBodyAsString();
if (StringUtils.isNotBlank(code)) {
return Result.ofError(new TooManyRequests(TooManyRequests.Code.valueOf(code)));
} else {
return Result.ofError(new TooManyRequests());
}
}
final RestCallError restCallError = new RestCallError("Unexpected error while rest call", responseError); final RestCallError restCallError = new RestCallError("Unexpected error while rest call", responseError);
try { try {

View file

@ -33,6 +33,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.api.TooManyRequests;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
@ -87,6 +88,15 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }
@ExceptionHandler(TooManyRequests.class)
public ResponseEntity<Object> handleToManyRequests(
final TooManyRequests ex,
final WebRequest request) {
return ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.body(String.valueOf(ex.code));
}
@ExceptionHandler(RuntimeException.class) @ExceptionHandler(RuntimeException.class)
public ResponseEntity<Object> handleRuntimeException( public ResponseEntity<Object> handleRuntimeException(
final RuntimeException ex, final RuntimeException ex,

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.time.Duration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import io.github.bucket4j.local.LocalBucket;
@Lazy
@Service
@WebServiceProfile
public class RateLimitService {
private final int requestLimit;
private final int requestLimitInterval;
private final int requestLimitRefill;
private final int createLimit;
private final int createLimitInterval;
private final int createLimitRefill;
public RateLimitService(final Environment env) {
this.requestLimit = env.getProperty(
"sebserver.webservice.api.admin.request.limit", Integer.class, 10);
this.requestLimitInterval =
env.getProperty("sebserver.webservice.api.admin.request.limit.interval.min", Integer.class, 10);
this.requestLimitRefill =
env.getProperty("sebserver.webservice.api.admin.request.limit.refill", Integer.class, 2);
this.createLimit = env.getProperty(
"sebserver.webservice.api.admin.create.limit", Integer.class, 10);
this.createLimitInterval =
env.getProperty("sebserver.webservice.api.admin.create.limit.interval.min", Integer.class, 3600);
this.createLimitRefill =
env.getProperty("sebserver.webservice.api.admin.create.limit.refill", Integer.class, 10);
}
public LocalBucket createRequestLimitBucker() {
final Bandwidth limit = Bandwidth.classic(
this.requestLimit,
Refill.intervally(this.requestLimitRefill, Duration.ofMinutes(this.requestLimitInterval)));
return Bucket4j.builder()
.addLimit(limit)
.build();
}
public LocalBucket createCreationLimitBucker() {
final Bandwidth limit = Bandwidth.classic(
this.createLimit,
Refill.intervally(this.createLimitRefill, Duration.ofMinutes(this.createLimitInterval)));
return Bucket4j.builder()
.addLimit(limit)
.build();
}
}

View file

@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.api.TooManyRequests;
import ch.ethz.seb.sebserver.gbl.model.Domain.USER_ROLE; import ch.ethz.seb.sebserver.gbl.model.Domain.USER_ROLE;
import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange; import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
@ -38,6 +39,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
import io.github.bucket4j.local.LocalBucket;
@WebServiceProfile @WebServiceProfile
@RestController @RestController
@ -47,17 +49,23 @@ public class RegisterUserController {
private final UserActivityLogDAO userActivityLogDAO; private final UserActivityLogDAO userActivityLogDAO;
private final UserDAO userDAO; private final UserDAO userDAO;
private final BeanValidationService beanValidationService; private final BeanValidationService beanValidationService;
private final LocalBucket requestRateLimitBucket;
private final LocalBucket createRateLimitBucket;
protected RegisterUserController( protected RegisterUserController(
final InstitutionDAO institutionDAO, final InstitutionDAO institutionDAO,
final UserActivityLogDAO userActivityLogDAO, final UserActivityLogDAO userActivityLogDAO,
final UserDAO userDAO, final UserDAO userDAO,
final BeanValidationService beanValidationService, final BeanValidationService beanValidationService,
final RateLimitService rateLimitService,
@Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder userPasswordEncoder) { @Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder userPasswordEncoder) {
this.userActivityLogDAO = userActivityLogDAO; this.userActivityLogDAO = userActivityLogDAO;
this.userDAO = userDAO; this.userDAO = userDAO;
this.beanValidationService = beanValidationService; this.beanValidationService = beanValidationService;
this.requestRateLimitBucket = rateLimitService.createRequestLimitBucker();
this.createRateLimitBucket = rateLimitService.createCreationLimitBucker();
} }
@RequestMapping( @RequestMapping(
@ -68,6 +76,10 @@ public class RegisterUserController {
@RequestParam final MultiValueMap<String, String> allRequestParams, @RequestParam final MultiValueMap<String, String> allRequestParams,
final HttpServletRequest request) { final HttpServletRequest request) {
if (!this.requestRateLimitBucket.tryConsume(1)) {
throw new TooManyRequests();
}
final POSTMapper postMap = new POSTMapper(allRequestParams, request.getQueryString()) final POSTMapper postMap = new POSTMapper(allRequestParams, request.getQueryString())
.putIfAbsent(USER_ROLE.REFERENCE_NAME, UserRole.EXAM_SUPPORTER.name()); .putIfAbsent(USER_ROLE.REFERENCE_NAME, UserRole.EXAM_SUPPORTER.name());
final UserMod userMod = new UserMod(null, postMap); final UserMod userMod = new UserMod(null, postMap);
@ -88,8 +100,11 @@ public class RegisterUserController {
throw new APIMessageException(errors); throw new APIMessageException(errors);
} }
return userAccount; if (!this.createRateLimitBucket.tryConsume(1)) {
throw new TooManyRequests(TooManyRequests.Code.REGISTRATION);
}
return userAccount;
}) })
.flatMap(this.userDAO::createNew) .flatMap(this.userDAO::createNew)
.flatMap(account -> this.userDAO.setActive(account, true)) .flatMap(account -> this.userDAO.setActive(account, true))

View file

@ -44,6 +44,12 @@ sebserver.webservice.api.admin.clientId=guiClient
sebserver.webservice.api.admin.endpoint=/admin-api/v1 sebserver.webservice.api.admin.endpoint=/admin-api/v1
sebserver.webservice.api.admin.accessTokenValiditySeconds=3600 sebserver.webservice.api.admin.accessTokenValiditySeconds=3600
sebserver.webservice.api.admin.refreshTokenValiditySeconds=25200 sebserver.webservice.api.admin.refreshTokenValiditySeconds=25200
sebserver.webservice.api.admin.request.limit=10
sebserver.webservice.api.admin.request.limit.interval.min=10
sebserver.webservice.api.admin.request.limit.refill=2
sebserver.webservice.api.admin.create.limit=10
sebserver.webservice.api.admin.create.limit.interval.min=3600
sebserver.webservice.api.admin.create.limit.refill=10
sebserver.webservice.api.exam.config.init.permittedProcesses=config/initialPermittedProcesses.xml sebserver.webservice.api.exam.config.init.permittedProcesses=config/initialPermittedProcesses.xml
sebserver.webservice.api.exam.config.init.prohibitedProcesses=config/initialProhibitedProcesses.xml sebserver.webservice.api.exam.config.init.prohibitedProcesses=config/initialProhibitedProcesses.xml
sebserver.webservice.api.exam.endpoint=/exam-api sebserver.webservice.api.exam.endpoint=/exam-api
@ -56,4 +62,5 @@ sebserver.webservice.api.pagination.maxPageSize=500
# comma separated list of known possible OpenEdX API access token request endpoints # comma separated list of known possible OpenEdX API access token request endpoints
sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php
sebserver.webservice.lms.address.alias= sebserver.webservice.lms.address.alias=

View file

@ -107,6 +107,8 @@ sebserver.error.save.entity=Failed to save {0}.<br/> Please try again or contact
sebserver.error.exam.seb.restriction=<br/><br/>Failed to automatically set Safe Exam Browser restriction on/off for this exam on the corresponding LMS.<br/> Please check the LMS Setup and try again or contact a system administrator if this error persists sebserver.error.exam.seb.restriction=<br/><br/>Failed to automatically set Safe Exam Browser restriction on/off for this exam on the corresponding LMS.<br/> Please check the LMS Setup and try again or contact a system administrator if this error persists
sebserver.error.import=Failed to import {0}.<br/> Please try again or contact a system administrator if this error persists sebserver.error.import=Failed to import {0}.<br/> Please try again or contact a system administrator if this error persists
sebserver.error.logout=Failed to logout properly.<br/> Please try again or contact a system administrator if this error persists sebserver.error.logout=Failed to logout properly.<br/> Please try again or contact a system administrator if this error persists
sebserver.error.tooManyRequests.INCOMMING=This request has been blocked as there are too many incoming request at the moment for this page.<br/><br/> Please try again later.
sebserver.error.tooManyRequests.REGISTRATION=This request has been blocked as the maximum user registration per day limit has reached.<br/><br/> Please try again next day or call an administrator to create a user account.
################################ ################################
# Login Page # Login Page
################################ ################################