Merge branch 'develop/3.5.2' into feature/widget-bundles
This commit is contained in:
		
						commit
						7f47472092
					
				@ -0,0 +1,30 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.config;
 | 
			
		||||
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 | 
			
		||||
 | 
			
		||||
@Configuration
 | 
			
		||||
public class CryptoConfig {
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    protected BCryptPasswordEncoder passwordEncoder() {
 | 
			
		||||
        return new BCryptPasswordEncoder();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -188,11 +188,6 @@ public class ThingsboardSecurityConfiguration {
 | 
			
		||||
        return auth.build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    protected BCryptPasswordEncoder passwordEncoder() {
 | 
			
		||||
        return new BCryptPasswordEncoder();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -23,18 +23,18 @@ import com.google.api.client.auth.oauth2.TokenResponse;
 | 
			
		||||
import com.google.api.client.http.GenericUrl;
 | 
			
		||||
import com.google.api.client.http.javanet.NetHttpTransport;
 | 
			
		||||
import com.google.api.client.json.gson.GsonFactory;
 | 
			
		||||
import lombok.SneakyThrows;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.lang.Nullable;
 | 
			
		||||
import org.springframework.mail.MailException;
 | 
			
		||||
import org.springframework.mail.MailSendException;
 | 
			
		||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
 | 
			
		||||
import org.thingsboard.server.common.data.AdminSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.StringUtils;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.mail.MailOauth2Provider;
 | 
			
		||||
import org.thingsboard.server.dao.exception.IncorrectParameterException;
 | 
			
		||||
 | 
			
		||||
import javax.mail.MessagingException;
 | 
			
		||||
import javax.mail.internet.MimeMessage;
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
import java.time.Instant;
 | 
			
		||||
@ -50,6 +50,7 @@ public class TbMailSender extends JavaMailSenderImpl {
 | 
			
		||||
    private static final String MAIL_PROP = "mail.";
 | 
			
		||||
    private final TbMailContextComponent ctx;
 | 
			
		||||
    private final Lock lock;
 | 
			
		||||
 | 
			
		||||
    private final Boolean oauth2Enabled;
 | 
			
		||||
    private volatile String accessToken;
 | 
			
		||||
    private volatile long tokenExpires;
 | 
			
		||||
@ -70,14 +71,39 @@ public class TbMailSender extends JavaMailSenderImpl {
 | 
			
		||||
        setJavaMailProperties(createJavaMailProperties(jsonConfig));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SneakyThrows
 | 
			
		||||
    public Boolean getOauth2Enabled() {
 | 
			
		||||
        return oauth2Enabled;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public long getTokenExpires() {
 | 
			
		||||
        return tokenExpires;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) {
 | 
			
		||||
        if (oauth2Enabled && (System.currentTimeMillis() > tokenExpires)){
 | 
			
		||||
    protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException {
 | 
			
		||||
        updateOauth2PasswordIfExpired();
 | 
			
		||||
        doSendSuper(mimeMessages, originalMessages);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void doSendSuper(MimeMessage[] mimeMessages, Object[] originalMessages) {
 | 
			
		||||
        super.doSend(mimeMessages, originalMessages);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void testConnection() throws MessagingException {
 | 
			
		||||
        updateOauth2PasswordIfExpired();
 | 
			
		||||
        testConnectionSuper();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void testConnectionSuper() throws MessagingException {
 | 
			
		||||
        super.testConnection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void updateOauth2PasswordIfExpired()  {
 | 
			
		||||
        if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())){
 | 
			
		||||
            refreshAccessToken();
 | 
			
		||||
            setPassword(accessToken);
 | 
			
		||||
        }
 | 
			
		||||
        super.doSend(mimeMessages, originalMessages);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Properties createJavaMailProperties(JsonNode jsonConfig) {
 | 
			
		||||
@ -125,10 +151,10 @@ public class TbMailSender extends JavaMailSenderImpl {
 | 
			
		||||
        return javaMailProperties;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void refreshAccessToken() throws ThingsboardException {
 | 
			
		||||
    public void refreshAccessToken() {
 | 
			
		||||
        lock.lock();
 | 
			
		||||
        try {
 | 
			
		||||
            if (System.currentTimeMillis() > tokenExpires) {
 | 
			
		||||
            if (System.currentTimeMillis() > getTokenExpires()) {
 | 
			
		||||
                AdminSettings settings = ctx.getAdminSettingsService().findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
 | 
			
		||||
                JsonNode jsonValue = settings.getJsonValue();
 | 
			
		||||
 | 
			
		||||
@ -151,8 +177,8 @@ public class TbMailSender extends JavaMailSenderImpl {
 | 
			
		||||
                tokenExpires = System.currentTimeMillis() + (tokenResponse.getExpiresInSeconds().intValue() * 1000);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.warn("Unable to retrieve access token: {}", e.getMessage());
 | 
			
		||||
            throw new ThingsboardException("Error while retrieving access token: " + e.getMessage(), ThingsboardErrorCode.GENERAL);
 | 
			
		||||
            log.error("Unable to retrieve access token: {}", e.getMessage());
 | 
			
		||||
            throw new RuntimeException("Error while retrieving access token: " + e.getMessage());
 | 
			
		||||
        } finally {
 | 
			
		||||
            lock.unlock();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.rule.engine.api.NotificationCenter;
 | 
			
		||||
import org.thingsboard.server.common.data.EntityType;
 | 
			
		||||
import org.thingsboard.server.common.data.User;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.NotificationId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.NotificationRequestId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.NotificationRuleId;
 | 
			
		||||
@ -39,11 +40,12 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus
 | 
			
		||||
import org.thingsboard.server.common.data.notification.NotificationStatus;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
 | 
			
		||||
@ -165,16 +167,54 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
 | 
			
		||||
                .settings(settings)
 | 
			
		||||
                .build();
 | 
			
		||||
 | 
			
		||||
        notificationExecutor.submit(() -> {
 | 
			
		||||
            for (NotificationTarget target : targets) {
 | 
			
		||||
                processForTarget(target, ctx);
 | 
			
		||||
            }
 | 
			
		||||
        processNotificationRequestAsync(ctx, targets, callback);
 | 
			
		||||
        return request;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template) {
 | 
			
		||||
        NotificationTarget target = new NotificationTarget();
 | 
			
		||||
        target.setTenantId(tenantId);
 | 
			
		||||
        PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
 | 
			
		||||
        targetConfig.setUsersFilter(recipients);
 | 
			
		||||
        target.setConfiguration(targetConfig);
 | 
			
		||||
 | 
			
		||||
        NotificationRequest notificationRequest = NotificationRequest.builder()
 | 
			
		||||
                .tenantId(tenantId)
 | 
			
		||||
                .template(template)
 | 
			
		||||
                .targets(List.of(EntityId.NULL_UUID)) // this is temporary and will be removed when 'create from scratch' functionality is implemented for recipients
 | 
			
		||||
                .status(NotificationRequestStatus.PROCESSING)
 | 
			
		||||
                .build();
 | 
			
		||||
        try {
 | 
			
		||||
            notificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest);
 | 
			
		||||
            NotificationProcessingContext ctx = NotificationProcessingContext.builder()
 | 
			
		||||
                    .tenantId(tenantId)
 | 
			
		||||
                    .request(notificationRequest)
 | 
			
		||||
                    .deliveryMethods(Set.of(NotificationDeliveryMethod.WEB))
 | 
			
		||||
                    .template(template)
 | 
			
		||||
                    .build();
 | 
			
		||||
 | 
			
		||||
            processNotificationRequestAsync(ctx, List.of(target), null);
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.error("Failed to process notification request for recipients {} for template '{}'", recipients, template.getName(), e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void processNotificationRequestAsync(NotificationProcessingContext ctx, List<NotificationTarget> targets, Consumer<NotificationRequestStats> callback) {
 | 
			
		||||
        notificationExecutor.submit(() -> {
 | 
			
		||||
            NotificationRequestId requestId = ctx.getRequest().getId();
 | 
			
		||||
            for (NotificationTarget target : targets) {
 | 
			
		||||
                try {
 | 
			
		||||
                    processForTarget(target, ctx);
 | 
			
		||||
                } catch (Exception e) {
 | 
			
		||||
                    log.error("[{}] Failed to process notification request for target {}", requestId, target.getId(), e);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            log.debug("[{}] Notification request processing is finished", requestId);
 | 
			
		||||
 | 
			
		||||
            NotificationRequestStats stats = ctx.getStats();
 | 
			
		||||
            try {
 | 
			
		||||
                notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats);
 | 
			
		||||
                notificationRequestService.updateNotificationRequest(ctx.getTenantId(), requestId, NotificationRequestStatus.SENT, stats);
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                log.error("[{}] Failed to update stats for notification request", requestId, e);
 | 
			
		||||
            }
 | 
			
		||||
@ -187,8 +227,6 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return request;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void processForTarget(NotificationTarget target, NotificationProcessingContext ctx) {
 | 
			
		||||
 | 
			
		||||
@ -22,11 +22,14 @@ import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.context.annotation.Lazy;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.rule.engine.api.NotificationCenter;
 | 
			
		||||
import org.thingsboard.server.cluster.TbClusterService;
 | 
			
		||||
import org.thingsboard.server.common.data.AdminSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
 | 
			
		||||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.dao.notification.DefaultNotifications;
 | 
			
		||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
@ -43,6 +46,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
 | 
			
		||||
    private final AdminSettingsService adminSettingsService;
 | 
			
		||||
    @Lazy
 | 
			
		||||
    private final Optional<TbClusterService> tbClusterService;
 | 
			
		||||
    private final Optional<NotificationCenter> notificationCenter;
 | 
			
		||||
    private final JwtSettingsValidator jwtSettingsValidator;
 | 
			
		||||
 | 
			
		||||
    @Value("${security.jwt.tokenExpirationTime:9000}")
 | 
			
		||||
@ -124,6 +128,9 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
 | 
			
		||||
                        log.warn("WARNING: The platform is configured to use default JWT Signing Key. " +
 | 
			
		||||
                                "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " +
 | 
			
		||||
                                "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.");
 | 
			
		||||
                        notificationCenter.ifPresent(notificationCenter -> {
 | 
			
		||||
                            notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(), DefaultNotifications.jwtSigningKeyIssue.toTemplate());
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                    this.jwtSettings = result;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,6 @@ import org.thingsboard.server.dao.exception.DataValidationException;
 | 
			
		||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
 | 
			
		||||
import org.thingsboard.server.dao.user.UserService;
 | 
			
		||||
import org.thingsboard.server.dao.user.UserServiceImpl;
 | 
			
		||||
import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
 | 
			
		||||
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
@ -73,7 +72,6 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
@Slf4j
 | 
			
		||||
@TbCoreComponent
 | 
			
		||||
public class DefaultSystemSecurityService implements SystemSecurityService {
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,95 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.service.mail;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.BeforeEach;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.junit.jupiter.params.ParameterizedTest;
 | 
			
		||||
import org.junit.jupiter.params.provider.Arguments;
 | 
			
		||||
import org.junit.jupiter.params.provider.MethodSource;
 | 
			
		||||
import org.mockito.Mockito;
 | 
			
		||||
import javax.mail.MessagingException;
 | 
			
		||||
import javax.mail.Session;
 | 
			
		||||
import javax.mail.internet.MimeMessage;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Properties;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.BDDMockito.willCallRealMethod;
 | 
			
		||||
import static org.mockito.BDDMockito.willReturn;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
import static org.mockito.Mockito.times;
 | 
			
		||||
 | 
			
		||||
public class TbMailSenderTest {
 | 
			
		||||
 | 
			
		||||
    private TbMailSender tbMailSender;
 | 
			
		||||
 | 
			
		||||
    @BeforeEach
 | 
			
		||||
    void setUp() {
 | 
			
		||||
        tbMailSender = mock(TbMailSender.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDoSendSendMail() {
 | 
			
		||||
        MimeMessage mimeMsg = new MimeMessage(Session.getInstance(new Properties()));
 | 
			
		||||
        List<MimeMessage> mimeMessages = new ArrayList<>(1);
 | 
			
		||||
        mimeMessages.add(mimeMsg);
 | 
			
		||||
 | 
			
		||||
        willCallRealMethod().given(tbMailSender).doSend(any(), any());
 | 
			
		||||
        tbMailSender.doSend(mimeMessages.toArray(new MimeMessage[0]), null);
 | 
			
		||||
 | 
			
		||||
        Mockito.verify(tbMailSender, times(1)).updateOauth2PasswordIfExpired();
 | 
			
		||||
        Mockito.verify(tbMailSender, times(1)).doSendSuper(any(), any());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testTestConnection() throws MessagingException {
 | 
			
		||||
        willCallRealMethod().given(tbMailSender).testConnection();
 | 
			
		||||
        tbMailSender.testConnection();
 | 
			
		||||
 | 
			
		||||
        Mockito.verify(tbMailSender, times(1)).updateOauth2PasswordIfExpired();
 | 
			
		||||
        Mockito.verify(tbMailSender, times(1)).testConnectionSuper();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ParameterizedTest
 | 
			
		||||
    @MethodSource("provideSenderConfiguration")
 | 
			
		||||
    public void testUpdateOauth2PasswordIfExpiredIfOauth2Enabled(boolean oauth2, long expiresIn, boolean passwordUpdateNeeded) {
 | 
			
		||||
        willReturn(oauth2).given(tbMailSender).getOauth2Enabled();
 | 
			
		||||
        willReturn(expiresIn).given(tbMailSender).getTokenExpires();
 | 
			
		||||
 | 
			
		||||
        willCallRealMethod().given(tbMailSender).updateOauth2PasswordIfExpired();
 | 
			
		||||
        tbMailSender.updateOauth2PasswordIfExpired();
 | 
			
		||||
 | 
			
		||||
        if (passwordUpdateNeeded) {
 | 
			
		||||
            Mockito.verify(tbMailSender, times(1)).refreshAccessToken();
 | 
			
		||||
            Mockito.verify(tbMailSender, times(1)).setPassword(any());
 | 
			
		||||
        } else {
 | 
			
		||||
            Mockito.verify(tbMailSender, Mockito.never()).refreshAccessToken();
 | 
			
		||||
            Mockito.verify(tbMailSender, Mockito.never()).setPassword(any());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Stream<Arguments> provideSenderConfiguration() {
 | 
			
		||||
        return Stream.of(
 | 
			
		||||
                Arguments.of(true, 0L, true),
 | 
			
		||||
                Arguments.of(true, System.currentTimeMillis() + 5000, false),
 | 
			
		||||
                Arguments.of(false, 0L, false),
 | 
			
		||||
                Arguments.of(false, System.currentTimeMillis() + 5000, false)
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -265,4 +265,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
 | 
			
		||||
        return (NotificationApiWsClient) super.getWsClient();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public NotificationApiWsClient getAnotherWsClient() {
 | 
			
		||||
        return (NotificationApiWsClient) super.getAnotherWsClient();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.audit.ActionType;
 | 
			
		||||
import org.thingsboard.server.common.data.id.DeviceId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.NotificationRuleId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.NotificationTargetId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.Notification;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.NotificationRequest;
 | 
			
		||||
@ -47,6 +48,7 @@ import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNot
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType;
 | 
			
		||||
@ -61,6 +63,7 @@ import org.thingsboard.server.common.data.notification.template.SlackDeliveryMet
 | 
			
		||||
import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
import org.thingsboard.server.dao.notification.DefaultNotifications;
 | 
			
		||||
import org.thingsboard.server.dao.notification.NotificationDao;
 | 
			
		||||
import org.thingsboard.server.dao.service.DaoSqlTest;
 | 
			
		||||
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
 | 
			
		||||
@ -601,6 +604,23 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
 | 
			
		||||
        assertThat(stats.getErrors().get(NotificationDeliveryMethod.SLACK).values()).containsExactly(errorMessage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testInternalGeneralWebNotifications() throws Exception {
 | 
			
		||||
        loginSysAdmin();
 | 
			
		||||
        getAnotherWsClient().subscribeForUnreadNotifications(10).waitForReply(true);
 | 
			
		||||
 | 
			
		||||
        getAnotherWsClient().registerWaitForUpdate();
 | 
			
		||||
 | 
			
		||||
        DefaultNotifications.DefaultNotification expectedNotification = DefaultNotifications.maintenanceWork;
 | 
			
		||||
        notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(),
 | 
			
		||||
                expectedNotification.toTemplate());
 | 
			
		||||
 | 
			
		||||
        getAnotherWsClient().waitForUpdate(true);
 | 
			
		||||
        Notification notification = getAnotherWsClient().getLastDataUpdate().getUpdate();
 | 
			
		||||
        assertThat(notification.getSubject()).isEqualTo(expectedNotification.getSubject());
 | 
			
		||||
        assertThat(notification.getText()).isEqualTo(expectedNotification.getText());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testMicrosoftTeamsNotifications() throws Exception {
 | 
			
		||||
        RestTemplate restTemplate = mock(RestTemplate.class);
 | 
			
		||||
@ -688,7 +708,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
 | 
			
		||||
 | 
			
		||||
    protected void connectOtherWsClient() throws Exception {
 | 
			
		||||
        loginCustomerUser();
 | 
			
		||||
        otherWsClient = (NotificationApiWsClient) super.getAnotherWsClient();
 | 
			
		||||
        otherWsClient = super.getAnotherWsClient();
 | 
			
		||||
        loginTenantAdmin();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,14 +16,22 @@
 | 
			
		||||
package org.thingsboard.server.service.security.auth;
 | 
			
		||||
 | 
			
		||||
import io.jsonwebtoken.Claims;
 | 
			
		||||
import org.junit.BeforeClass;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.rule.engine.api.NotificationCenter;
 | 
			
		||||
import org.thingsboard.server.common.data.AdminSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.id.CustomerId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.UserId;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtToken;
 | 
			
		||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsValidator;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.model.UserPrincipal;
 | 
			
		||||
@ -33,28 +41,40 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
 | 
			
		||||
 | 
			
		||||
import java.util.Calendar;
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.mockito.BDDMockito.willReturn;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.argThat;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.eq;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.isA;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
import static org.mockito.Mockito.times;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
 | 
			
		||||
public class JwtTokenFactoryTest {
 | 
			
		||||
 | 
			
		||||
    private static JwtTokenFactory tokenFactory;
 | 
			
		||||
    private static JwtSettings jwtSettings;
 | 
			
		||||
    private JwtTokenFactory tokenFactory;
 | 
			
		||||
    private AdminSettingsService adminSettingsService;
 | 
			
		||||
    private NotificationCenter notificationCenter;
 | 
			
		||||
    private JwtSettingsService jwtSettingsService;
 | 
			
		||||
 | 
			
		||||
    @BeforeClass
 | 
			
		||||
    public static void beforeAll() {
 | 
			
		||||
    private JwtSettings jwtSettings;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void beforeEach() {
 | 
			
		||||
        jwtSettings = new JwtSettings();
 | 
			
		||||
        jwtSettings.setTokenIssuer("tb");
 | 
			
		||||
        jwtSettings.setTokenSigningKey("abewafaf");
 | 
			
		||||
        jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2));
 | 
			
		||||
        jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7));
 | 
			
		||||
 | 
			
		||||
        JwtSettingsService jwtSettingsService = mock(JwtSettingsService.class);
 | 
			
		||||
        willReturn(jwtSettings).given(jwtSettingsService).getJwtSettings();
 | 
			
		||||
        adminSettingsService = mock(AdminSettingsService.class);
 | 
			
		||||
        notificationCenter = mock(NotificationCenter.class);
 | 
			
		||||
        jwtSettingsService = mockJwtSettingsService();
 | 
			
		||||
        mockJwtSettings(jwtSettings);
 | 
			
		||||
 | 
			
		||||
        tokenFactory = new JwtTokenFactory(jwtSettingsService);
 | 
			
		||||
    }
 | 
			
		||||
@ -150,6 +170,33 @@ public class JwtTokenFactoryTest {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testJwtSigningKeyIssueNotification() {
 | 
			
		||||
        JwtSettings badJwtSettings = jwtSettings;
 | 
			
		||||
        badJwtSettings.setTokenSigningKey(JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT);
 | 
			
		||||
        mockJwtSettings(badJwtSettings);
 | 
			
		||||
        jwtSettingsService = mockJwtSettingsService();
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < 5; i++) { // to check if notification is not sent twice
 | 
			
		||||
            jwtSettingsService.getJwtSettings();
 | 
			
		||||
        }
 | 
			
		||||
        verify(notificationCenter, times(1)).sendGeneralWebNotification(eq(TenantId.SYS_TENANT_ID),
 | 
			
		||||
                isA(SystemAdministratorsFilter.class), argThat(template -> template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.WEB)
 | 
			
		||||
                        .getBody().contains("The platform is configured to use default JWT Signing Key")));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void mockJwtSettings(JwtSettings settings) {
 | 
			
		||||
        AdminSettings adminJwtSettings = new AdminSettings();
 | 
			
		||||
        adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(settings));
 | 
			
		||||
        when(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, JwtSettingsService.ADMIN_SETTINGS_JWT_KEY))
 | 
			
		||||
                .thenReturn(adminJwtSettings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private DefaultJwtSettingsService mockJwtSettingsService() {
 | 
			
		||||
        return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(),
 | 
			
		||||
                Optional.of(notificationCenter), new DefaultJwtSettingsValidator());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) {
 | 
			
		||||
        Claims claims = tokenFactory.parseTokenClaims(jwtToken).getBody();
 | 
			
		||||
        assertThat(claims.getExpiration()).matches(actualExpirationTime -> {
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
 | 
			
		||||
import org.thingsboard.server.queue.discovery.PartitionService;
 | 
			
		||||
import org.thingsboard.server.queue.discovery.QueueKey;
 | 
			
		||||
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
 | 
			
		||||
import org.thingsboard.server.queue.usagestats.DefaultTbApiUsageReportClient;
 | 
			
		||||
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
 | 
			
		||||
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
@ -86,7 +87,7 @@ public class DefaultDeviceStateServiceTest {
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp() {
 | 
			
		||||
        service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, null, mock(NotificationRuleProcessor.class)));
 | 
			
		||||
        service = spy(new DefaultDeviceStateService(deviceService, attributesService, tsService, clusterService, partitionService, entityQueryRepository, null, mock(DefaultTbApiUsageReportClient.class), mock(NotificationRuleProcessor.class)));
 | 
			
		||||
        telemetrySubscriptionService = Mockito.mock(TelemetrySubscriptionService.class);
 | 
			
		||||
        ReflectionTestUtils.setField(service, "tsSubService", telemetrySubscriptionService);
 | 
			
		||||
        ReflectionTestUtils.setField(service, "defaultStateCheckIntervalInSec", 60);
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,10 @@
 | 
			
		||||
    <logger name="org.thingsboard.server.transport.lwm2m.server" level="INFO"/>
 | 
			
		||||
    <logger name="org.eclipse.californium.core" level="INFO"/>
 | 
			
		||||
 | 
			
		||||
    <!--    To reduce logs -->
 | 
			
		||||
    <logger name="org.apache.catalina.loader.WebappClassLoaderBase" level="ERROR" />
 | 
			
		||||
    <logger name="org.thingsboard.server.service.queue.DefaultTbClusterService" level="ERROR" />
 | 
			
		||||
 | 
			
		||||
    <!-- Coap client context debug for the test scope -->
 | 
			
		||||
    <!--    <logger name="org.thingsboard.server.transport.coap.client.DefaultCoapClientContext" level="TRACE" />-->
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,9 @@ import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class DefaultNotifications {
 | 
			
		||||
 | 
			
		||||
    private static final String YELLOW_COLOR = "#F9D916";
 | 
			
		||||
    private static final String RED_COLOR = "#e91a1a";
 | 
			
		||||
 | 
			
		||||
    public static final DefaultNotification maintenanceWork = DefaultNotification.builder()
 | 
			
		||||
            .name("Maintenance work notification")
 | 
			
		||||
            .subject("Infrastructure maintenance")
 | 
			
		||||
@ -77,7 +80,7 @@ public class DefaultNotifications {
 | 
			
		||||
            .type(NotificationType.ENTITIES_LIMIT)
 | 
			
		||||
            .subject("${entityType}s limit will be reached soon for tenant ${tenantName}")
 | 
			
		||||
            .text("${entityType}s usage: ${currentCount}/${limit} (${percents}%)")
 | 
			
		||||
            .icon("warning").color("#F9D916")
 | 
			
		||||
            .icon("warning").color(YELLOW_COLOR)
 | 
			
		||||
            .rule(DefaultRule.builder()
 | 
			
		||||
                    .name("Entities count limit (sysadmin)")
 | 
			
		||||
                    .triggerConfig(EntitiesLimitNotificationRuleTriggerConfig.builder()
 | 
			
		||||
@ -100,7 +103,7 @@ public class DefaultNotifications {
 | 
			
		||||
            .type(NotificationType.API_USAGE_LIMIT)
 | 
			
		||||
            .subject("${feature} feature will be disabled soon for tenant ${tenantName}")
 | 
			
		||||
            .text("Usage: ${currentValue} out of ${limit} ${unitLabel}s")
 | 
			
		||||
            .icon("warning").color("#F9D916")
 | 
			
		||||
            .icon("warning").color(YELLOW_COLOR)
 | 
			
		||||
            .rule(DefaultRule.builder()
 | 
			
		||||
                    .name("API feature warning (sysadmin)")
 | 
			
		||||
                    .triggerConfig(ApiUsageLimitNotificationRuleTriggerConfig.builder()
 | 
			
		||||
@ -123,7 +126,7 @@ public class DefaultNotifications {
 | 
			
		||||
            .type(NotificationType.API_USAGE_LIMIT)
 | 
			
		||||
            .subject("${feature} feature was disabled for tenant ${tenantName}")
 | 
			
		||||
            .text("Used ${currentValue} out of ${limit} ${unitLabel}s")
 | 
			
		||||
            .icon("block").color("#e91a1a")
 | 
			
		||||
            .icon("block").color(RED_COLOR)
 | 
			
		||||
            .rule(DefaultRule.builder()
 | 
			
		||||
                    .name("API feature disabled (sysadmin)")
 | 
			
		||||
                    .triggerConfig(ApiUsageLimitNotificationRuleTriggerConfig.builder()
 | 
			
		||||
@ -147,7 +150,7 @@ public class DefaultNotifications {
 | 
			
		||||
            .type(NotificationType.RATE_LIMITS)
 | 
			
		||||
            .subject("Rate limits exceeded")
 | 
			
		||||
            .text("Rate limits for ${api} exceeded")
 | 
			
		||||
            .icon("block").color("#e91a1a")
 | 
			
		||||
            .icon("block").color(RED_COLOR)
 | 
			
		||||
            .rule(DefaultRule.builder()
 | 
			
		||||
                    .name("Per-tenant rate limits exceeded")
 | 
			
		||||
                    .triggerConfig(RateLimitsNotificationRuleTriggerConfig.builder()
 | 
			
		||||
@ -164,7 +167,7 @@ public class DefaultNotifications {
 | 
			
		||||
            .type(NotificationType.RATE_LIMITS)
 | 
			
		||||
            .subject("Rate limits exceeded")
 | 
			
		||||
            .text("Rate limits for ${api} exceeded for '${limitLevelEntityName}'")
 | 
			
		||||
            .icon("block").color("#e91a1a")
 | 
			
		||||
            .icon("block").color(RED_COLOR)
 | 
			
		||||
            .rule(DefaultRule.builder()
 | 
			
		||||
                    .name("Per-entity rate limits exceeded")
 | 
			
		||||
                    .triggerConfig(RateLimitsNotificationRuleTriggerConfig.builder()
 | 
			
		||||
@ -323,6 +326,15 @@ public class DefaultNotifications {
 | 
			
		||||
                    .build())
 | 
			
		||||
            .build();
 | 
			
		||||
 | 
			
		||||
    public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder()
 | 
			
		||||
            .name("JWT Signing Key issue notification")
 | 
			
		||||
            .type(NotificationType.GENERAL)
 | 
			
		||||
            .subject("WARNING: security issue")
 | 
			
		||||
            .text("The platform is configured to use default JWT Signing Key. Please change it on the security settings page")
 | 
			
		||||
            .icon("warning").color(YELLOW_COLOR)
 | 
			
		||||
            .button("Go to settings").link("/security-settings/general")
 | 
			
		||||
            .build();
 | 
			
		||||
 | 
			
		||||
    private final NotificationTemplateService templateService;
 | 
			
		||||
    private final NotificationRuleService ruleService;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,8 @@ import org.thingsboard.server.common.data.id.UserId;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.NotificationRequest;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
 | 
			
		||||
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
 | 
			
		||||
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
@ -30,6 +32,8 @@ public interface NotificationCenter {
 | 
			
		||||
 | 
			
		||||
    NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest, Consumer<NotificationRequestStats> callback);
 | 
			
		||||
 | 
			
		||||
    void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients, NotificationTemplate template);
 | 
			
		||||
 | 
			
		||||
    void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId);
 | 
			
		||||
 | 
			
		||||
    void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
 | 
			
		||||
 | 
			
		||||
@ -33,8 +33,8 @@ public class TbMathNodeConfiguration implements NodeConfiguration<TbMathNodeConf
 | 
			
		||||
    public TbMathNodeConfiguration defaultConfiguration() {
 | 
			
		||||
        TbMathNodeConfiguration configuration = new TbMathNodeConfiguration();
 | 
			
		||||
        configuration.setOperation(TbRuleNodeMathFunctionType.CUSTOM);
 | 
			
		||||
        configuration.setCustomFunction("(t - 32) / 1.8");
 | 
			
		||||
        configuration.setArguments(List.of(new TbMathArgument("t", TbMathArgumentType.MESSAGE_BODY, "temperature")));
 | 
			
		||||
        configuration.setCustomFunction("(x - 32) / 1.8");
 | 
			
		||||
        configuration.setArguments(List.of(new TbMathArgument("x", TbMathArgumentType.MESSAGE_BODY, "temperature")));
 | 
			
		||||
        configuration.setResult(new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "temperatureCelsius", 2, false, false, null));
 | 
			
		||||
        return configuration;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user