202 changed files with 12816 additions and 981 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,188 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.controller; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import io.swagger.v3.oas.annotations.Parameter; |
||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.http.HttpStatus; |
||||
|
import org.springframework.http.ResponseEntity; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestHeader; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.mobile.AndroidConfig; |
||||
|
import org.thingsboard.server.common.data.mobile.IosConfig; |
||||
|
import org.thingsboard.server.common.data.mobile.MobileAppSettings; |
||||
|
import org.thingsboard.server.common.data.security.model.JwtPair; |
||||
|
import org.thingsboard.server.config.annotations.ApiOperation; |
||||
|
import org.thingsboard.server.dao.mobile.MobileAppSettingsService; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
import org.thingsboard.server.service.mobile.secret.MobileAppSecretService; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
import org.thingsboard.server.service.security.permission.Operation; |
||||
|
import org.thingsboard.server.service.security.permission.Resource; |
||||
|
import org.thingsboard.server.service.security.system.SystemSecurityService; |
||||
|
|
||||
|
import java.net.URI; |
||||
|
import java.net.URISyntaxException; |
||||
|
|
||||
|
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; |
||||
|
|
||||
|
@RequiredArgsConstructor |
||||
|
@RestController |
||||
|
@TbCoreComponent |
||||
|
public class MobileApplicationController extends BaseController { |
||||
|
|
||||
|
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}") |
||||
|
private int mobileSecretKeyTtl; |
||||
|
|
||||
|
public static final String ASSET_LINKS_PATTERN = "[{\n" + |
||||
|
" \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" + |
||||
|
" \"target\": {\n" + |
||||
|
" \"namespace\": \"android_app\",\n" + |
||||
|
" \"package_name\": \"%s\",\n" + |
||||
|
" \"sha256_cert_fingerprints\":\n" + |
||||
|
" [\"%s\"]\n" + |
||||
|
" }\n" + |
||||
|
"}]"; |
||||
|
|
||||
|
public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" + |
||||
|
" \"applinks\": {\n" + |
||||
|
" \"apps\": [],\n" + |
||||
|
" \"details\": [\n" + |
||||
|
" {\n" + |
||||
|
" \"appID\": \"%s\",\n" + |
||||
|
" \"paths\": [ \"/api/noauth/qr\" ]\n" + |
||||
|
" }\n" + |
||||
|
" ]\n" + |
||||
|
" }\n" + |
||||
|
"}"; |
||||
|
|
||||
|
public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app"; |
||||
|
public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695"; |
||||
|
public static final String SECRET = "secret"; |
||||
|
public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-lived secret key"; |
||||
|
public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io"; |
||||
|
public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s"; |
||||
|
|
||||
|
private final SystemSecurityService systemSecurityService; |
||||
|
private final MobileAppSecretService mobileAppSecretService; |
||||
|
private final MobileAppSettingsService mobileAppSettingsService; |
||||
|
|
||||
|
@ApiOperation(value = "Get associated android applications (getAssetLinks)") |
||||
|
@GetMapping(value = "/.well-known/assetlinks.json") |
||||
|
public ResponseEntity<JsonNode> getAssetLinks() { |
||||
|
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); |
||||
|
AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig(); |
||||
|
if (androidConfig != null && androidConfig.isEnabled() && !androidConfig.getAppPackage().isBlank() && !androidConfig.getSha256CertFingerprints().isBlank()) { |
||||
|
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, androidConfig.getAppPackage(), androidConfig.getSha256CertFingerprints()))); |
||||
|
} else { |
||||
|
return ResponseEntity.notFound().build(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)") |
||||
|
@GetMapping(value = "/.well-known/apple-app-site-association") |
||||
|
public ResponseEntity<JsonNode> getAppleAppSiteAssociation() { |
||||
|
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); |
||||
|
IosConfig iosConfig = mobileAppSettings.getIosConfig(); |
||||
|
if (iosConfig != null && iosConfig.isEnabled() && !iosConfig.getAppId().isBlank()) { |
||||
|
return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, iosConfig.getAppId()))); |
||||
|
} else { |
||||
|
return ResponseEntity.notFound().build(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)", |
||||
|
notes = "The request payload contains configuration for android/iOS applications and platform qr code widget settings." + SYSTEM_AUTHORITY_PARAGRAPH) |
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
||||
|
@PostMapping(value = "/api/mobile/app/settings") |
||||
|
public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") |
||||
|
@RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException { |
||||
|
SecurityUser currentUser = getCurrentUser(); |
||||
|
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE); |
||||
|
mobileAppSettings.setTenantId(getTenantId()); |
||||
|
return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", |
||||
|
notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@GetMapping(value = "/api/mobile/app/settings") |
||||
|
public MobileAppSettings getMobileAppSettings() throws ThingsboardException { |
||||
|
SecurityUser currentUser = getCurrentUser(); |
||||
|
accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); |
||||
|
return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Get the deep link to the associated mobile application (getMobileAppDeepLink)", |
||||
|
notes = "Fetch the url that takes user to linked mobile application " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@GetMapping(value = "/api/mobile/deepLink", produces = "text/plain") |
||||
|
public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { |
||||
|
String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser()); |
||||
|
String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request); |
||||
|
String platformDomain = new URI(baseUrl).getHost(); |
||||
|
MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); |
||||
|
String appDomain; |
||||
|
if (!mobileAppSettings.isUseDefaultApp()) { |
||||
|
appDomain = platformDomain; |
||||
|
} else { |
||||
|
appDomain = DEFAULT_APP_DOMAIN; |
||||
|
} |
||||
|
String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret, mobileSecretKeyTtl); |
||||
|
if (!appDomain.equals(platformDomain)) { |
||||
|
deepLink = deepLink + "&host=" + baseUrl; |
||||
|
} |
||||
|
return "\"" + deepLink + "\""; |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)", |
||||
|
notes = "Returns the token of the User based on the provided secret key.") |
||||
|
@GetMapping(value = "/api/noauth/qr/{secret}") |
||||
|
public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION) |
||||
|
@PathVariable(SECRET) String secret) throws ThingsboardException { |
||||
|
checkParameter(SECRET, secret); |
||||
|
return mobileAppSecretService.getJwtPair(secret); |
||||
|
} |
||||
|
|
||||
|
@GetMapping(value = "/api/noauth/qr") |
||||
|
public ResponseEntity<?> getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) { |
||||
|
if (userAgent.contains("Android")) { |
||||
|
return ResponseEntity.status(HttpStatus.FOUND) |
||||
|
.header("Location", ANDROID_APPLICATION_STORE_LINK) |
||||
|
.build(); |
||||
|
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) { |
||||
|
return ResponseEntity.status(HttpStatus.FOUND) |
||||
|
.header("Location", APPLE_APPLICATION_STORE_LINK) |
||||
|
.build(); |
||||
|
} else { |
||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND) |
||||
|
.build(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile.secret; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.common.data.security.model.JwtPair; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
|
||||
|
public interface MobileAppSecretService { |
||||
|
|
||||
|
String generateMobileAppSecret(SecurityUser securityUser); |
||||
|
|
||||
|
JwtPair getJwtPair(String secret) throws ThingsboardException; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile.secret; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.transaction.event.TransactionalEventListener; |
||||
|
import org.thingsboard.server.cache.TbCacheValueWrapper; |
||||
|
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.security.model.JwtPair; |
||||
|
import org.thingsboard.server.dao.entity.AbstractCachedService; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
||||
|
import org.thingsboard.server.service.security.system.SystemSecurityService; |
||||
|
|
||||
|
import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH; |
||||
|
|
||||
|
@Service |
||||
|
@Slf4j |
||||
|
@RequiredArgsConstructor |
||||
|
public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService { |
||||
|
|
||||
|
private final JwtTokenFactory tokenFactory; |
||||
|
private final SystemSecurityService systemSecurityService; |
||||
|
|
||||
|
@Override |
||||
|
public String generateMobileAppSecret(SecurityUser securityUser) { |
||||
|
log.trace("Executing generateSecret for user [{}]", securityUser.getId()); |
||||
|
Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength(); |
||||
|
String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength); |
||||
|
cache.put(secret, tokenFactory.createTokenPair(securityUser)); |
||||
|
return secret; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public JwtPair getJwtPair(String secret) throws ThingsboardException { |
||||
|
TbCacheValueWrapper<JwtPair> jwtPair = cache.get(secret); |
||||
|
if (jwtPair != null) { |
||||
|
return jwtPair.get(); |
||||
|
} else { |
||||
|
throw new ThingsboardException("Jwt token not found or expired!", ThingsboardErrorCode.JWT_TOKEN_EXPIRED); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@TransactionalEventListener(classes = MobileSecretEvictEvent.class) |
||||
|
@Override |
||||
|
public void handleEvictEvent(MobileSecretEvictEvent event) { |
||||
|
cache.evict(event.getSecret()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile.secret; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.cache.CacheManager; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.cache.CaffeineTbTransactionalCache; |
||||
|
import org.thingsboard.server.common.data.CacheConstants; |
||||
|
import org.thingsboard.server.common.data.security.model.JwtPair; |
||||
|
|
||||
|
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) |
||||
|
@Service("MobileSecretCache") |
||||
|
public class MobileSecretCaffeineCache extends CaffeineTbTransactionalCache<String, JwtPair> { |
||||
|
|
||||
|
public MobileSecretCaffeineCache(CacheManager cacheManager) { |
||||
|
super(cacheManager, CacheConstants.MOBILE_SECRET_KEY_CACHE); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile.secret; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class MobileSecretEvictEvent { |
||||
|
|
||||
|
private final String secret; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile.secret; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.cache.CacheSpecsMap; |
||||
|
import org.thingsboard.server.cache.RedisTbTransactionalCache; |
||||
|
import org.thingsboard.server.cache.TBRedisCacheConfiguration; |
||||
|
import org.thingsboard.server.cache.TbJsonRedisSerializer; |
||||
|
import org.thingsboard.server.common.data.CacheConstants; |
||||
|
import org.thingsboard.server.common.data.security.model.JwtPair; |
||||
|
|
||||
|
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") |
||||
|
@Service("MobileSecretCache") |
||||
|
public class MobileSecretRedisCache extends RedisTbTransactionalCache<String, JwtPair> { |
||||
|
|
||||
|
public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { |
||||
|
super(CacheConstants.MOBILE_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.subscription; |
||||
|
|
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
/** |
||||
|
* The modification result of entity subscription |
||||
|
*/ |
||||
|
@Builder |
||||
|
@Data |
||||
|
public class SubscriptionModificationResult { |
||||
|
|
||||
|
private TenantId tenantId; |
||||
|
private EntityId entityId; |
||||
|
private TbSubscription<?> subscription; |
||||
|
private TbSubscription<?> missedUpdatesCandidate; |
||||
|
private TbEntitySubEvent event; |
||||
|
|
||||
|
public boolean hasEvent() { |
||||
|
return event != null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,253 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.controller; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.thingsboard.server.common.data.mobile.AndroidConfig; |
||||
|
import org.thingsboard.server.common.data.mobile.IosConfig; |
||||
|
import org.thingsboard.server.common.data.mobile.MobileAppSettings; |
||||
|
import org.thingsboard.server.common.data.mobile.QRCodeConfig; |
||||
|
import org.thingsboard.server.common.data.security.model.JwtPair; |
||||
|
import org.thingsboard.server.dao.service.DaoSqlTest; |
||||
|
|
||||
|
import java.util.regex.Matcher; |
||||
|
import java.util.regex.Pattern; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.hamcrest.Matchers.containsString; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
|
||||
|
@Slf4j |
||||
|
@DaoSqlTest |
||||
|
public class MobileApplicationControllerTest extends AbstractControllerTest { |
||||
|
|
||||
|
@Value("${cache.specs.mobileSecretKey.timeToLiveInMinutes:2}") |
||||
|
private int mobileSecretKeyTtl; |
||||
|
private static final String ANDROID_PACKAGE_NAME = "testAppPackage"; |
||||
|
private static final String ANDROID_APP_SHA256 = "DF:28:32:66:8B:A7:D3:EC:7D:73:CF:CC"; |
||||
|
private static final String APPLE_APP_ID = "testId"; |
||||
|
private static final String TEST_LABEL = "Test label"; |
||||
|
|
||||
|
@Before |
||||
|
public void setUp() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
|
||||
|
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
QRCodeConfig qrCodeConfig = new QRCodeConfig(); |
||||
|
qrCodeConfig.setQrCodeLabel(TEST_LABEL); |
||||
|
|
||||
|
mobileAppSettings.setUseDefaultApp(true); |
||||
|
AndroidConfig androidConfig = AndroidConfig.builder() |
||||
|
.appPackage(ANDROID_PACKAGE_NAME) |
||||
|
.sha256CertFingerprints(ANDROID_APP_SHA256) |
||||
|
.enabled(true) |
||||
|
.build(); |
||||
|
|
||||
|
IosConfig iosConfig = IosConfig.builder() |
||||
|
.appId(APPLE_APP_ID) |
||||
|
.enabled(true) |
||||
|
.build(); |
||||
|
mobileAppSettings.setAndroidConfig(androidConfig); |
||||
|
mobileAppSettings.setIosConfig(iosConfig); |
||||
|
mobileAppSettings.setQrCodeConfig(qrCodeConfig); |
||||
|
|
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSaveMobileAppSettings() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
assertThat(mobileAppSettings.getQrCodeConfig().getQrCodeLabel()).isEqualTo(TEST_LABEL); |
||||
|
assertThat(mobileAppSettings.isUseDefaultApp()).isTrue(); |
||||
|
|
||||
|
mobileAppSettings.setUseDefaultApp(false); |
||||
|
|
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isOk()); |
||||
|
|
||||
|
MobileAppSettings updatedMobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
assertThat(updatedMobileAppSettings.isUseDefaultApp()).isFalse(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testShouldNotSaveMobileAppSettingsWithoutRequiredConfig() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
|
||||
|
mobileAppSettings.setUseDefaultApp(false); |
||||
|
mobileAppSettings.setAndroidConfig(null); |
||||
|
mobileAppSettings.setIosConfig(null); |
||||
|
mobileAppSettings.setQrCodeConfig(null); |
||||
|
|
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isBadRequest()) |
||||
|
.andExpect(statusReason(containsString("Android/ios settings are required to use custom application!"))); |
||||
|
|
||||
|
mobileAppSettings.setAndroidConfig(AndroidConfig.builder().enabled(false).build()); |
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isBadRequest()) |
||||
|
.andExpect(statusReason(containsString("Android/ios settings are required to use custom application!"))); |
||||
|
|
||||
|
mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build()); |
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isBadRequest()) |
||||
|
.andExpect(statusReason(containsString("Qr code configuration is required!"))); |
||||
|
|
||||
|
mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build()); |
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
mobileAppSettings.setUseDefaultApp(false); |
||||
|
AndroidConfig androidConfig = AndroidConfig.builder() |
||||
|
.enabled(true) |
||||
|
.appPackage(null) |
||||
|
.sha256CertFingerprints(null) |
||||
|
.build(); |
||||
|
mobileAppSettings.setAndroidConfig(androidConfig); |
||||
|
|
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isBadRequest()) |
||||
|
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!"))); |
||||
|
|
||||
|
androidConfig.setAppPackage("test_app_package"); |
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isBadRequest()) |
||||
|
.andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!"))); |
||||
|
|
||||
|
androidConfig.setSha256CertFingerprints("test_sha_256"); |
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
mobileAppSettings.setUseDefaultApp(false); |
||||
|
IosConfig iosConfig = IosConfig.builder() |
||||
|
.enabled(true) |
||||
|
.appId(null) |
||||
|
.build(); |
||||
|
mobileAppSettings.setIosConfig(iosConfig); |
||||
|
|
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isBadRequest()) |
||||
|
.andExpect(statusReason(containsString("Application id is required for custom ios application!"))); |
||||
|
|
||||
|
iosConfig.setAppId("test_app_id"); |
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testShouldSaveMobileAppSettingsForDefaultApp() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
mobileAppSettings.setUseDefaultApp(true); |
||||
|
mobileAppSettings.setIosConfig(null); |
||||
|
mobileAppSettings.setAndroidConfig(null); |
||||
|
|
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testGetApplicationAssociations() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
mobileAppSettings.setUseDefaultApp(false); |
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings) |
||||
|
.andExpect(status().isOk()); |
||||
|
|
||||
|
JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); |
||||
|
assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME); |
||||
|
assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256); |
||||
|
|
||||
|
JsonNode appleAssociation = doGet("/.well-known/apple-app-site-association", JsonNode.class); |
||||
|
assertThat(appleAssociation.get("applinks").get("details").get(0).get("appID").asText()).isEqualTo(APPLE_APP_ID); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testGetMobileDeepLink() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
String deepLink = doGet("/api/mobile/deepLink", String.class); |
||||
|
|
||||
|
Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\""); |
||||
|
Matcher parsedDeepLink = expectedPattern.matcher(deepLink); |
||||
|
assertThat(parsedDeepLink.matches()).isTrue(); |
||||
|
String appHost = parsedDeepLink.group(1); |
||||
|
String secret = parsedDeepLink.group(2); |
||||
|
String ttl = parsedDeepLink.group(3); |
||||
|
assertThat(appHost).isEqualTo("demo.thingsboard.io"); |
||||
|
assertThat(ttl).isEqualTo(String.valueOf(mobileSecretKeyTtl)); |
||||
|
|
||||
|
JwtPair jwtPair = doGet("/api/noauth/qr/" + secret, JwtPair.class); |
||||
|
assertThat(jwtPair).isNotNull(); |
||||
|
|
||||
|
loginTenantAdmin(); |
||||
|
String tenantDeepLink = doGet("/api/mobile/deepLink", String.class); |
||||
|
Matcher tenantParsedDeepLink = expectedPattern.matcher(tenantDeepLink); |
||||
|
assertThat(tenantParsedDeepLink.matches()).isTrue(); |
||||
|
String tenantSecret = tenantParsedDeepLink.group(2); |
||||
|
|
||||
|
JwtPair tenantJwtPair = doGet("/api/noauth/qr/" + tenantSecret, JwtPair.class); |
||||
|
assertThat(tenantJwtPair).isNotNull(); |
||||
|
|
||||
|
loginCustomerUser(); |
||||
|
String customerDeepLink = doGet("/api/mobile/deepLink", String.class); |
||||
|
Matcher customerParsedDeepLink = expectedPattern.matcher(customerDeepLink); |
||||
|
assertThat(customerParsedDeepLink.matches()).isTrue(); |
||||
|
String customerSecret = customerParsedDeepLink.group(2); |
||||
|
|
||||
|
JwtPair customerJwtPair = doGet("/api/noauth/qr/" + customerSecret, JwtPair.class); |
||||
|
assertThat(customerJwtPair).isNotNull(); |
||||
|
|
||||
|
// update mobile setting to use custom one
|
||||
|
loginSysAdmin(); |
||||
|
MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); |
||||
|
mobileAppSettings.setUseDefaultApp(false); |
||||
|
doPost("/api/mobile/app/settings", mobileAppSettings); |
||||
|
|
||||
|
String customAppDeepLink = doGet("/api/mobile/deepLink", String.class); |
||||
|
Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\""); |
||||
|
Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink); |
||||
|
assertThat(customAppParsedDeepLink.matches()).isTrue(); |
||||
|
assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost"); |
||||
|
|
||||
|
loginTenantAdmin(); |
||||
|
String tenantCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class); |
||||
|
Matcher tenantCustomAppParsedDeepLink = customAppExpectedPattern.matcher(tenantCustomAppDeepLink); |
||||
|
assertThat(tenantCustomAppParsedDeepLink.matches()).isTrue(); |
||||
|
assertThat(tenantCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); |
||||
|
|
||||
|
loginCustomerUser(); |
||||
|
String customerCustomAppDeepLink = doGet("/api/mobile/deepLink", String.class); |
||||
|
Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink); |
||||
|
assertThat(customerCustomAppParsedDeepLink.matches()).isTrue(); |
||||
|
assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.transport.lwm2m.rpc.sql; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import org.eclipse.leshan.core.ResponseCode; |
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; |
||||
|
|
||||
|
import static org.junit.Assert.assertEquals; |
||||
|
import static org.junit.Assert.assertFalse; |
||||
|
import static org.junit.Assert.assertTrue; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; |
||||
|
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; |
||||
|
|
||||
|
public class RpcLwm2mIntegrationWriteCborTest extends AbstractRpcLwM2MIntegrationTest { |
||||
|
|
||||
|
/** |
||||
|
* Client with ContentFormat ={} |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Resource: |
||||
|
* <Type>Opaque</Type> |
||||
|
* Input type String (HexDec) |
||||
|
* WriteReplace {"id": "/19_1.1/0/0","value": {"0":"01234567890CAB"}} |
||||
|
* return Hex.decodeHex(((String)value).toCharArray()) - ok [1, 35, 69, 103, -119, 12, -85, -1] |
||||
|
* {"result":"CHANGED"} -> Actual Value Equals expectedValue0 |
||||
|
*/ |
||||
|
@Test |
||||
|
public void testWriteReplaceValueMultipleResource_InputFormatData_Ok_Result_CHANGED_Value_Equals_expectedValue0() throws Exception { |
||||
|
String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; |
||||
|
int resourceInstanceId0 = 0; |
||||
|
String expectedValue0 = "01234567890CABFF"; |
||||
|
String expectedValue = "{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\"}"; |
||||
|
String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); |
||||
|
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
||||
|
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); |
||||
|
String expectedPath0 = expectedPath + "/" + resourceInstanceId0; |
||||
|
actualResult = sendRPCReadById(expectedPath0); |
||||
|
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
||||
|
String actualValues = rpcActualResult.get("value").asText(); |
||||
|
String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]"; |
||||
|
assertTrue(actualValues.contains(expected)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Resource: |
||||
|
* <Type>Opaque</Type> |
||||
|
* Input type String (not HexDec) |
||||
|
* return Hex.decodeHex(((String)value).toCharArray()) - error |
||||
|
* return Base64.getDecoder().decode(((String) value).getBytes()) - ok; |
||||
|
* WriteReplace {"id": "/19_1.1/0/0","value": {"0":"01234567890"}} |
||||
|
* {"result":"CHANGED"} -> Actual Value Not Equals expectedValue0 |
||||
|
*/ |
||||
|
@Test |
||||
|
public void testWriteReplaceValueMultipleResource_Error_InputFormatData_Result_CHANGED_Value_Not_Equals_expectedValue0() throws Exception { |
||||
|
String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; |
||||
|
int resourceInstanceId0 = 0; |
||||
|
String expectedValue0 = "01234567890"; |
||||
|
String expectedValue = "{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\"}"; |
||||
|
String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); |
||||
|
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
||||
|
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); |
||||
|
String expectedPath0 = expectedPath + "/" + resourceInstanceId0; |
||||
|
actualResult = sendRPCReadById(expectedPath0); |
||||
|
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
||||
|
String actualValues = rpcActualResult.get("value").asText(); |
||||
|
String expected = "LwM2mResourceInstance [id=" + resourceInstanceId0 + ", value=" + expectedValue0.length()/2 + "Bytes, type=OPAQUE]"; |
||||
|
assertFalse(actualValues.contains(expected)); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private String sendRPCWriteObjectById(String method, String path, Object value) throws Exception { |
||||
|
String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + path + "\", \"value\": " + value + " }}"; |
||||
|
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
||||
|
} |
||||
|
|
||||
|
private String sendRPCReadById(String id) throws Exception { |
||||
|
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + id + "\"}}"; |
||||
|
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.transport.mqtt; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import org.junit.Assert; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.mockito.Mockito; |
||||
|
import org.springframework.boot.test.mock.mockito.SpyBean; |
||||
|
import org.springframework.test.context.TestPropertySource; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.TenantProfile; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; |
||||
|
import org.thingsboard.server.common.data.security.DeviceCredentials; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
||||
|
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; |
||||
|
import org.thingsboard.server.controller.AbstractControllerTest; |
||||
|
import org.thingsboard.server.dao.service.DaoSqlTest; |
||||
|
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; |
||||
|
|
||||
|
import java.util.function.Consumer; |
||||
|
|
||||
|
import static org.junit.Assert.assertEquals; |
||||
|
import static org.junit.Assert.assertNotNull; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY; |
||||
|
|
||||
|
@DaoSqlTest |
||||
|
@TestPropertySource(properties = { |
||||
|
"service.integrations.supported=ALL", |
||||
|
"transport.mqtt.enabled=true", |
||||
|
}) |
||||
|
public class MqttGatewayRateLimitsTest extends AbstractControllerTest { |
||||
|
|
||||
|
private static final String TOPIC = "v1/gateway/telemetry"; |
||||
|
private static final String DEVICE_A = "DeviceA"; |
||||
|
private static final String DEVICE_B = "DeviceB"; |
||||
|
|
||||
|
private DeviceId gatewayId; |
||||
|
private String gatewayAccessToken; |
||||
|
|
||||
|
@SpyBean |
||||
|
private NotificationRuleProcessor notificationRuleProcessor; |
||||
|
|
||||
|
@Before |
||||
|
public void beforeTest() throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
|
||||
|
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class); |
||||
|
Assert.assertNotNull(tenantProfile); |
||||
|
|
||||
|
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration(); |
||||
|
|
||||
|
profileConfiguration.setTransportGatewayMsgRateLimit(null); |
||||
|
profileConfiguration.setTransportGatewayTelemetryMsgRateLimit(null); |
||||
|
profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit(null); |
||||
|
|
||||
|
doPost("/api/tenantProfile", tenantProfile); |
||||
|
|
||||
|
loginTenantAdmin(); |
||||
|
createGateway(); |
||||
|
|
||||
|
Mockito.reset(notificationRuleProcessor); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void transportGatewayMsgRateLimitTest() throws Exception { |
||||
|
// Device A 2 msgs success ('create device', 'to device actor'), Device B 'create device success' , 'to device actor' - limited
|
||||
|
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayMsgRateLimit("3:600")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void transportGatewayTelemetryMsgRateLimitTest() throws Exception { |
||||
|
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryMsgRateLimit("1:600")); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void transportGatewayTelemetryDataPointsRateLimitTest() throws Exception { |
||||
|
transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit("1:600")); |
||||
|
} |
||||
|
|
||||
|
private void transportGatewayRateLimitTest(Consumer<DefaultTenantProfileConfiguration> profileConfiguration) throws Exception { |
||||
|
loginSysAdmin(); |
||||
|
|
||||
|
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class); |
||||
|
Assert.assertNotNull(tenantProfile); |
||||
|
|
||||
|
profileConfiguration.accept((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration()); |
||||
|
|
||||
|
doPost("/api/tenantProfile", tenantProfile); |
||||
|
|
||||
|
MqttTestClient client = new MqttTestClient(); |
||||
|
client.connectAndWait(gatewayAccessToken); |
||||
|
client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_A)); |
||||
|
|
||||
|
loginTenantAdmin(); |
||||
|
|
||||
|
Device deviceA = getDeviceByName(DEVICE_A); |
||||
|
|
||||
|
var deviceATrigger = createRateLimitsTrigger(deviceA); |
||||
|
|
||||
|
Mockito.verify(notificationRuleProcessor, Mockito.never()).process(eq(deviceATrigger)); |
||||
|
|
||||
|
try { |
||||
|
client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_B)); |
||||
|
} catch (Exception t) { |
||||
|
} |
||||
|
|
||||
|
Device deviceB = getDeviceByName(DEVICE_B); |
||||
|
|
||||
|
var deviceBTrigger = createRateLimitsTrigger(deviceB); |
||||
|
|
||||
|
Mockito.verify(notificationRuleProcessor, Mockito.times(1)).process(deviceBTrigger); |
||||
|
|
||||
|
if (client.isConnected()) { |
||||
|
client.disconnect(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void createGateway() throws Exception { |
||||
|
Device device = new Device(); |
||||
|
device.setName("gateway"); |
||||
|
ObjectNode additionalInfo = JacksonUtil.newObjectNode(); |
||||
|
additionalInfo.put("gateway", true); |
||||
|
device.setAdditionalInfo(additionalInfo); |
||||
|
device = doPost("/api/device", device, Device.class); |
||||
|
assertNotNull(device); |
||||
|
gatewayId = device.getId(); |
||||
|
assertNotNull(gatewayId); |
||||
|
|
||||
|
DeviceCredentials deviceCredentials = doGet("/api/device/" + gatewayId + "/credentials", DeviceCredentials.class); |
||||
|
assertNotNull(deviceCredentials); |
||||
|
assertEquals(gatewayId, deviceCredentials.getDeviceId()); |
||||
|
gatewayAccessToken = deviceCredentials.getCredentialsId(); |
||||
|
assertNotNull(gatewayAccessToken); |
||||
|
} |
||||
|
|
||||
|
private Device getDeviceByName(String deviceName) throws Exception { |
||||
|
Device device = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); |
||||
|
assertNotNull(device); |
||||
|
return device; |
||||
|
} |
||||
|
|
||||
|
private byte[] getGatewayPayload(String deviceName) { |
||||
|
return String.format("{\"%s\": [{\"values\": {\"temperature\": 42}}]}", deviceName).getBytes(); |
||||
|
} |
||||
|
|
||||
|
private RateLimitsTrigger createRateLimitsTrigger(Device device) { |
||||
|
return RateLimitsTrigger.builder() |
||||
|
.tenantId(tenantId) |
||||
|
.api(TRANSPORT_MESSAGES_PER_GATEWAY) |
||||
|
.limitLevel(device.getId()) |
||||
|
.limitLevelEntityName(device.getName()) |
||||
|
.build(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.id; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
|
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Schema |
||||
|
public class MobileAppSettingsId extends UUIDBased { |
||||
|
|
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
@JsonCreator |
||||
|
public MobileAppSettingsId(@JsonProperty("id") UUID id) { |
||||
|
super(id); |
||||
|
} |
||||
|
|
||||
|
public static MobileAppSettingsId fromString(String mobileAppSettingsId) { |
||||
|
return new MobileAppSettingsId(UUID.fromString(mobileAppSettingsId)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import org.thingsboard.server.common.data.validation.NoXss; |
||||
|
|
||||
|
@Data |
||||
|
@Builder |
||||
|
@NoArgsConstructor |
||||
|
@AllArgsConstructor |
||||
|
@EqualsAndHashCode |
||||
|
public class AndroidConfig { |
||||
|
|
||||
|
private boolean enabled; |
||||
|
@NoXss |
||||
|
private String appPackage; |
||||
|
@NoXss |
||||
|
private String sha256CertFingerprints; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile; |
||||
|
|
||||
|
public enum BadgePosition { |
||||
|
|
||||
|
RIGHT, |
||||
|
LEFT; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile; |
||||
|
|
||||
|
|
||||
|
public enum BadgeStyle { |
||||
|
|
||||
|
ORIGINAL, |
||||
|
WHITE; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import org.thingsboard.server.common.data.validation.NoXss; |
||||
|
|
||||
|
@Data |
||||
|
@Builder |
||||
|
@NoArgsConstructor |
||||
|
@AllArgsConstructor |
||||
|
@EqualsAndHashCode |
||||
|
public class IosConfig { |
||||
|
|
||||
|
private boolean enabled; |
||||
|
@NoXss |
||||
|
private String appId; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile; |
||||
|
|
||||
|
import jakarta.validation.Valid; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.BaseData; |
||||
|
import org.thingsboard.server.common.data.HasTenantId; |
||||
|
import org.thingsboard.server.common.data.id.MobileAppSettingsId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
@Data |
||||
|
public class MobileAppSettings extends BaseData<MobileAppSettingsId> implements HasTenantId { |
||||
|
|
||||
|
private static final long serialVersionUID = 2628323657987010348L; |
||||
|
|
||||
|
private TenantId tenantId; |
||||
|
private boolean useDefaultApp; |
||||
|
@Valid |
||||
|
private AndroidConfig androidConfig; |
||||
|
@Valid |
||||
|
private IosConfig iosConfig; |
||||
|
@Valid |
||||
|
private QRCodeConfig qrCodeConfig; |
||||
|
|
||||
|
public MobileAppSettings() { |
||||
|
} |
||||
|
public MobileAppSettings(MobileAppSettingsId id) { |
||||
|
super(id); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.mobile; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import org.thingsboard.server.common.data.validation.NoXss; |
||||
|
|
||||
|
@Data |
||||
|
@Builder |
||||
|
@NoArgsConstructor |
||||
|
@AllArgsConstructor |
||||
|
@EqualsAndHashCode |
||||
|
public class QRCodeConfig { |
||||
|
|
||||
|
private boolean showOnHomePage; |
||||
|
private boolean badgeEnabled; |
||||
|
private boolean qrCodeLabelEnabled; |
||||
|
private BadgePosition badgePosition; |
||||
|
private BadgeStyle badgeStyle; |
||||
|
@NoXss |
||||
|
private String qrCodeLabel; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.queue.common; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.queue.ServiceType; |
||||
|
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; |
||||
|
import org.thingsboard.server.queue.TbQueueCallback; |
||||
|
import org.thingsboard.server.queue.TbQueueProducer; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionService; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
@Slf4j |
||||
|
public class TbRuleEngineProducerService { |
||||
|
|
||||
|
private final PartitionService partitionService; |
||||
|
|
||||
|
public void sendToRuleEngine(TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> producer, |
||||
|
TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { |
||||
|
List<TopicPartitionInfo> tpis = partitionService.resolveAll(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, tbMsg.getOriginator()); |
||||
|
if (tpis.size() > 1) { |
||||
|
UUID correlationId = UUID.randomUUID(); |
||||
|
for (int i = 0; i < tpis.size(); i++) { |
||||
|
TopicPartitionInfo tpi = tpis.get(i); |
||||
|
Integer partition = tpi.getPartition().orElse(null); |
||||
|
UUID id = i > 0 ? UUID.randomUUID() : tbMsg.getId(); |
||||
|
|
||||
|
tbMsg = tbMsg.toBuilder() |
||||
|
.id(id) |
||||
|
.correlationId(correlationId) |
||||
|
.partition(partition) |
||||
|
.build(); |
||||
|
sendToRuleEngine(producer, tpi, tenantId, tbMsg, i == tpis.size() - 1 ? callback : null); |
||||
|
} |
||||
|
} else { |
||||
|
sendToRuleEngine(producer, tpis.get(0), tenantId, tbMsg, callback); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void sendToRuleEngine(TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> producer, TopicPartitionInfo tpi, |
||||
|
TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { |
||||
|
if (log.isTraceEnabled()) { |
||||
|
log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, tbMsg.getOriginator(), tpi.getFullTopicName(), tbMsg); |
||||
|
} |
||||
|
ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder() |
||||
|
.setTbMsg(TbMsg.toByteString(tbMsg)) |
||||
|
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) |
||||
|
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build(); |
||||
|
producer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue