NoXss and length validation for notification entities

This commit is contained in:
ViacheslavKlimov 2023-04-13 12:53:59 +03:00
parent 5a57657479
commit e926854911
18 changed files with 115 additions and 75 deletions

View File

@ -103,7 +103,6 @@ import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.settings.UserDashboardAction;
import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
@ -130,12 +129,12 @@ import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserSettingsService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
@ -164,7 +163,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@ -395,16 +396,19 @@ public abstract class BaseController {
* Handles validation error for controller method arguments annotated with @{@link javax.validation.Valid}
* */
@ExceptionHandler(MethodArgumentNotValidException.class)
public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) {
String errorMessage = "Validation error: " + e.getFieldErrors().stream()
public void handleValidationError(MethodArgumentNotValidException validationError, HttpServletResponse response) {
List<ConstraintViolation<Object>> constraintsViolations = validationError.getFieldErrors().stream()
.map(fieldError -> {
String property = fieldError.getField();
if (property.equals("valid") || StringUtils.endsWith(property, ".valid")) { // when custom @AssertTrue is used
property = "";
try {
return (ConstraintViolation<Object>) fieldError.unwrap(ConstraintViolation.class);
} catch (Exception e) {
log.warn("FieldError source is not of type ConstraintViolation");
return null; // should not happen
}
return (!property.isEmpty() ? (property + " ") : "") + fieldError.getDefaultMessage();
})
.collect(Collectors.joining(", "));
.filter(Objects::nonNull)
.collect(Collectors.toList());
String errorMessage = "Validation error: " + ConstraintValidator.getErrorMessage(constraintsViolations);
ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
handleControllerException(thingsboardException, response);
}

View File

@ -125,8 +125,10 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
NotificationRuleId ruleId = request.getRuleId();
notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> {
if (!template.isEnabled()) return;
if (!channels.get(deliveryMethod).check(tenantId)) {
throw new IllegalArgumentException("Unable to send notification via " + deliveryMethod.getName() + ": not configured or not working");
try {
channels.get(deliveryMethod).check(tenantId);
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage());
}
if (ruleId == null) {
if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) {
@ -341,14 +343,20 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
@Override
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(TenantId tenantId) {
return channels.values().stream()
.filter(channel -> channel.check(tenantId))
.filter(channel -> {
try {
channel.check(tenantId);
return true;
} catch (Exception e) {
return false;
}
})
.map(NotificationChannel::getDeliveryMethod)
.collect(Collectors.toSet());
}
@Override
public boolean check(TenantId tenantId) {
return true;
public void check(TenantId tenantId) throws Exception {
}
@Override

View File

@ -48,12 +48,11 @@ public class EmailNotificationChannel implements NotificationChannel<User, Email
}
@Override
public boolean check(TenantId tenantId) {
public void check(TenantId tenantId) throws Exception {
try {
mailService.testConnection(tenantId);
return true;
} catch (Exception e) {
return false;
throw new RuntimeException("Mail server is not available");
}
}

View File

@ -26,7 +26,7 @@ public interface NotificationChannel<R extends NotificationRecipient, T extends
ListenableFuture<Void> sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx);
boolean check(TenantId tenantId);
void check(TenantId tenantId) throws Exception;
NotificationDeliveryMethod getDeliveryMethod();

View File

@ -22,12 +22,12 @@ import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
@Component
@RequiredArgsConstructor
@ -47,9 +47,11 @@ public class SlackNotificationChannel implements NotificationChannel<SlackConver
}
@Override
public boolean check(TenantId tenantId) {
public void check(TenantId tenantId) throws Exception {
NotificationSettings notificationSettings = notificationSettingsService.findNotificationSettings(tenantId);
return notificationSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.SLACK);
if (!notificationSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.SLACK)) {
throw new RuntimeException("Slack API token is not configured");
}
}
@Override

View File

@ -49,8 +49,10 @@ public class SmsNotificationChannel implements NotificationChannel<User, SmsDeli
}
@Override
public boolean check(TenantId tenantId) {
return smsService.isConfigured(tenantId);
public void check(TenantId tenantId) throws Exception {
if (!smsService.isConfigured(tenantId)) {
throw new RuntimeException("SMS provider is not configured");
}
}
@Override

View File

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
@ -43,6 +44,7 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
private TenantId tenantId;
@NotBlank
@NoXss
@Length(max = 255, message = "cannot be longer than 255 chars")
private String name;
@NotNull
private NotificationTemplateId templateId;

View File

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
@ -35,6 +36,7 @@ public class NotificationTarget extends BaseData<NotificationTargetId> implement
private TenantId tenantId;
@NotBlank
@NoXss
@Length(max = 255, message = "cannot be longer than 255 chars")
private String name;
@NotNull
@Valid

View File

@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@JsonIgnoreProperties(ignoreUnknown = true)
@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
public abstract class NotificationTargetConfig {
@NoXss
@Length(max = 500, message = "cannot be longer than 500 chars")
private String description;
@JsonIgnore

View File

@ -21,6 +21,8 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.constraints.NotEmpty;
@ -30,6 +32,8 @@ import javax.validation.constraints.NotEmpty;
@ToString(callSuper = true)
public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject {
@NoXss(fieldName = "email subject")
@Length(fieldName = "email subject", max = 250, message = "cannot be longer than 250 chars")
@NotEmpty
private String subject;

View File

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid;
@ -36,6 +37,7 @@ public class NotificationTemplate extends BaseData<NotificationTemplateId> imple
private TenantId tenantId;
@NoXss
@NotEmpty
@Length(max = 255, message = "cannot be longer than 255 chars")
private String name;
@NoXss
@NotNull

View File

@ -1,30 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.template;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NotificationText {
private String body;
private String subject;
}

View File

@ -20,6 +20,7 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@NoArgsConstructor
@ -31,6 +32,12 @@ public class SlackDeliveryMethodNotificationTemplate extends DeliveryMethodNotif
super(other);
}
@NoXss(fieldName = "Slack message")
@Override
public String getBody() {
return super.getBody();
}
@Override
public NotificationDeliveryMethod getMethod() {
return NotificationDeliveryMethod.SLACK;

View File

@ -20,6 +20,8 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@Data
@NoArgsConstructor
@ -31,6 +33,13 @@ public class SmsDeliveryMethodNotificationTemplate extends DeliveryMethodNotific
super(other);
}
@NoXss(fieldName = "SMS message")
@Length(fieldName = "SMS message", max = 320, message = "cannot be longer than 320 chars")
@Override
public String getBody() {
return super.getBody();
}
@Override
public NotificationDeliveryMethod getMethod() {
return NotificationDeliveryMethod.SMS;

View File

@ -25,6 +25,8 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.constraints.NotEmpty;
import java.util.Optional;
@ -35,6 +37,8 @@ import java.util.Optional;
@ToString(callSuper = true)
public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject {
@NoXss(fieldName = "web notification subject")
@Length(fieldName = "web notification subject", max = 150, message = "cannot be longer than 150 chars")
@NotEmpty
private String subject;
private JsonNode additionalConfig;
@ -45,6 +49,15 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific
this.additionalConfig = other.additionalConfig != null ? other.additionalConfig.deepCopy() : null;
}
@NoXss(fieldName = "web notification message")
@Length(fieldName = "web notification message", max = 250, message = "cannot be longer than 250 chars")
@Override
public String getBody() {
return super.getBody();
}
@NoXss(fieldName = "web notification button text")
@Length(fieldName = "web notification button text", max = 50, message = "cannot be longer than 50 chars")
@JsonIgnore
public String getButtonText() {
return getButtonConfigProperty("text");
@ -57,6 +70,8 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific
});
}
@NoXss(fieldName = "web notification button link")
@Length(fieldName = "web notification button link", max = 300, message = "cannot be longer than 300 chars")
@JsonIgnore
public String getButtonLink() {
return getButtonConfigProperty("link");

View File

@ -23,12 +23,12 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.METHOD})
@Constraint(validatedBy = {})
public @interface Length {
String message() default "length must be equal or less than {max}";
String fieldName();
String fieldName() default "";
int max() default 255;

View File

@ -23,12 +23,16 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.METHOD})
@Constraint(validatedBy = {})
public @interface NoXss {
String message() default "is malformed";
String fieldName() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service;
import com.google.common.collect.Iterators;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.cfg.ConstraintMapping;
@ -29,11 +30,13 @@ import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import org.thingsboard.server.dao.exception.DataValidationException;
import javax.validation.Path;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.AssertTrue;
import java.util.List;
import javax.validation.metadata.ConstraintDescriptor;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@ -51,26 +54,31 @@ public class ConstraintValidator {
}
public static void validateFields(Object data, String errorPrefix) {
List<String> constraintsViolations = getConstraintsViolations(data);
Set<ConstraintViolation<Object>> constraintsViolations = fieldsValidator.validate(data);
if (!constraintsViolations.isEmpty()) {
throw new DataValidationException(errorPrefix + String.join(", ", constraintsViolations));
throw new DataValidationException(errorPrefix + getErrorMessage(constraintsViolations));
}
}
public static List<String> getConstraintsViolations(Object data) {
return fieldsValidator.validate(data).stream()
.map(constraintViolation -> {
String property;
if (constraintViolation.getConstraintDescriptor().getAttributes().containsKey("fieldName")) {
property = constraintViolation.getConstraintDescriptor().getAttributes().get("fieldName").toString();
} else {
Path propertyPath = constraintViolation.getPropertyPath();
property = Iterators.getLast(propertyPath.iterator()).toString();
}
return property + " " + constraintViolation.getMessage();
})
.distinct()
.collect(Collectors.toList());
public static String getErrorMessage(Collection<ConstraintViolation<Object>> constraintsViolations) {
return constraintsViolations.stream()
.map(ConstraintValidator::getErrorMessage)
.distinct().sorted().collect(Collectors.joining(", "));
}
public static String getErrorMessage(ConstraintViolation<Object> constraintViolation) {
ConstraintDescriptor<?> constraintDescriptor = constraintViolation.getConstraintDescriptor();
String property = (String) constraintDescriptor.getAttributes().get("fieldName");
if (StringUtils.isEmpty(property) && !(constraintDescriptor.getAnnotation() instanceof AssertTrue)) {
property = Iterators.getLast(constraintViolation.getPropertyPath().iterator()).toString();
}
String error = "";
if (StringUtils.isNotEmpty(property)) {
error += property + " ";
}
error += constraintViolation.getMessage();
return error;
}
private static void initializeValidators() {