Merge remote-tracking branch 'origin/dev-1.1.0' into development
Conflicts: src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java
This commit is contained in:
commit
bbcbc318a9
11 changed files with 196 additions and 10 deletions
5
pom.xml
5
pom.xml
|
@ -346,6 +346,11 @@
|
|||
<artifactId>commons-text</artifactId>
|
||||
<version>1.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.vladimir-bukhtoyarov</groupId>
|
||||
<artifactId>bucket4j-core</artifactId>
|
||||
<version>4.10.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Testing -->
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -232,7 +232,7 @@ public class RegisterPage implements TemplateComposer {
|
|||
registerButton.addListener(SWT.Selection, event -> {
|
||||
|
||||
registerForm.getForm().clearErrors();
|
||||
final Result<UserInfo> onError = this.pageService
|
||||
final Result<UserInfo> result = this.pageService
|
||||
.getRestService()
|
||||
.getBuilder(RegisterNewUser.class)
|
||||
.withRestTemplate(this.restTemplate)
|
||||
|
@ -240,7 +240,7 @@ public class RegisterPage implements TemplateComposer {
|
|||
.call()
|
||||
.onError(registerForm::handleError);
|
||||
|
||||
if (onError.hasError()) {
|
||||
if (result.hasError()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,13 @@ import java.util.function.Predicate;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
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.util.Result;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
|
@ -35,7 +38,11 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
|
|||
|
||||
public class FormHandle<T extends Entity> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FormHandle.class);
|
||||
|
||||
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 PageContext pageContext;
|
||||
|
@ -169,12 +176,27 @@ public class FormHandle<T extends Entity> {
|
|||
}
|
||||
|
||||
private void handleUnexpectedError(final Exception error) {
|
||||
if (this.post != null && this.post.getEntityType() != null) {
|
||||
this.pageContext.notifySaveError(this.post.getEntityType(), error);
|
||||
} else {
|
||||
this.pageContext.notifyError(
|
||||
new LocTextKey(PageContext.GENERIC_SAVE_ERROR_TEXT_KEY, StringUtils.EMPTY),
|
||||
error);
|
||||
try {
|
||||
if (error instanceof TooManyRequests) {
|
||||
final TooManyRequests.Code code = ((TooManyRequests) error).code;
|
||||
if (code != null) {
|
||||
this.pageContext.publishInfo(new LocTextKey(MESSAGE_TOO_MANY_REQUESTS_TEXT.name + "." + code));
|
||||
} else {
|
||||
this.pageContext.publishInfo(MESSAGE_TOO_MANY_REQUESTS_TEXT);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.post != null && this.post.getEntityType() != null) {
|
||||
this.pageContext.notifySaveError(this.post.getEntityType(), error);
|
||||
} else {
|
||||
this.pageContext.notifyError(
|
||||
new LocTextKey(PageContext.GENERIC_SAVE_ERROR_TEXT_KEY, StringUtils.EMPTY),
|
||||
error);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to handle unexpected error: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.EntityType;
|
||||
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.Page;
|
||||
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
|
||||
|
@ -134,6 +135,15 @@ public abstract class RestCall<T> {
|
|||
}
|
||||
} 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);
|
||||
try {
|
||||
|
||||
|
|
|
@ -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.APIMessageException;
|
||||
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.webservice.servicelayer.authorization.PermissionDeniedException;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
|
||||
|
@ -87,6 +88,15 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
|
|||
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)
|
||||
public ResponseEntity<Object> handleRuntimeException(
|
||||
final RuntimeException ex,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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.APIMessageException;
|
||||
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.user.PasswordChange;
|
||||
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.UserDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
|
||||
@WebServiceProfile
|
||||
@RestController
|
||||
|
@ -47,17 +49,23 @@ public class RegisterUserController {
|
|||
private final UserActivityLogDAO userActivityLogDAO;
|
||||
private final UserDAO userDAO;
|
||||
private final BeanValidationService beanValidationService;
|
||||
private final LocalBucket requestRateLimitBucket;
|
||||
private final LocalBucket createRateLimitBucket;
|
||||
|
||||
protected RegisterUserController(
|
||||
final InstitutionDAO institutionDAO,
|
||||
final UserActivityLogDAO userActivityLogDAO,
|
||||
final UserDAO userDAO,
|
||||
final BeanValidationService beanValidationService,
|
||||
final RateLimitService rateLimitService,
|
||||
@Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder userPasswordEncoder) {
|
||||
|
||||
this.userActivityLogDAO = userActivityLogDAO;
|
||||
this.userDAO = userDAO;
|
||||
this.beanValidationService = beanValidationService;
|
||||
|
||||
this.requestRateLimitBucket = rateLimitService.createRequestLimitBucker();
|
||||
this.createRateLimitBucket = rateLimitService.createCreationLimitBucker();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
|
@ -68,6 +76,10 @@ public class RegisterUserController {
|
|||
@RequestParam final MultiValueMap<String, String> allRequestParams,
|
||||
final HttpServletRequest request) {
|
||||
|
||||
if (!this.requestRateLimitBucket.tryConsume(1)) {
|
||||
throw new TooManyRequests();
|
||||
}
|
||||
|
||||
final POSTMapper postMap = new POSTMapper(allRequestParams, request.getQueryString())
|
||||
.putIfAbsent(USER_ROLE.REFERENCE_NAME, UserRole.EXAM_SUPPORTER.name());
|
||||
final UserMod userMod = new UserMod(null, postMap);
|
||||
|
@ -88,8 +100,11 @@ public class RegisterUserController {
|
|||
throw new APIMessageException(errors);
|
||||
}
|
||||
|
||||
return userAccount;
|
||||
if (!this.createRateLimitBucket.tryConsume(1)) {
|
||||
throw new TooManyRequests(TooManyRequests.Code.REGISTRATION);
|
||||
}
|
||||
|
||||
return userAccount;
|
||||
})
|
||||
.flatMap(this.userDAO::createNew)
|
||||
.flatMap(account -> this.userDAO.setActive(account, true))
|
||||
|
|
|
@ -44,6 +44,12 @@ sebserver.webservice.api.admin.clientId=guiClient
|
|||
sebserver.webservice.api.admin.endpoint=/admin-api/v1
|
||||
sebserver.webservice.api.admin.accessTokenValiditySeconds=3600
|
||||
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.prohibitedProcesses=config/initialProhibitedProcesses.xml
|
||||
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
|
||||
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.address.alias=
|
||||
sebserver.webservice.lms.address.alias=
|
||||
|
||||
|
|
|
@ -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.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.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
|
||||
################################
|
||||
|
|
Loading…
Reference in a new issue