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:
anhefti 2021-03-11 18:16:04 +01:00
commit bbcbc318a9
11 changed files with 196 additions and 10 deletions

View file

@ -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 -->

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 -> {
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;
}

View file

@ -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);
}
}

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.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 {

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.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,

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.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))

View file

@ -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=

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.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
################################