Merge pull request #9104 from thingsboard/feature/system-notifications

Notification to sysadmin when default JWT signing key is used
This commit is contained in:
Andrew Shvayka 2023-09-04 14:18:05 +03:00 committed by GitHub
commit 6f2334b35b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 184 additions and 29 deletions

View File

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

View File

@ -188,11 +188,6 @@ public class ThingsboardSecurityConfiguration {
return auth.build();
}
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;

View File

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

View File

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

View File

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

View File

@ -265,4 +265,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
return (NotificationApiWsClient) super.getWsClient();
}
@Override
public NotificationApiWsClient getAnotherWsClient() {
return (NotificationApiWsClient) super.getAnotherWsClient();
}
}

View File

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

View File

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

View File

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

View File

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