Commit 98ad3b98 authored by Yassmine Mestiri's avatar Yassmine Mestiri
Browse files

iot

parent aea55d6d
Pipeline #2009 failed with stages
in 0 seconds
/**
* Copyright © 2016-2022 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames;
import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Service
@Slf4j
public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
private static final String DEFAULT_LOGIN_PROCESSING_URI = "/login/oauth2/code/";
private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
private static final char PATH_DELIMITER = '/';
private final AntPathRequestMatcher authorizationRequestMatcher = new AntPathRequestMatcher(
DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Autowired
private OAuth2Service oAuth2Service;
@Autowired
private OAuth2AppTokenFactory oAuth2AppTokenFactory;
@Autowired(required = false)
private OAuth2Configuration oauth2Configuration;
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
String redirectUriAction = getAction(request, "login");
String appPackage = getAppPackage(request);
String appToken = getAppToken(request);
return resolve(request, registrationId, redirectUriAction, appPackage, appToken);
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) {
if (registrationId == null) {
return null;
}
String redirectUriAction = getAction(request, "authorize");
String appPackage = getAppPackage(request);
String appToken = getAppToken(request);
return resolve(request, registrationId, redirectUriAction, appPackage, appToken);
}
private String getAction(HttpServletRequest request, String defaultAction) {
String action = request.getParameter("action");
if (action == null) {
return defaultAction;
}
return action;
}
private String getAppPackage(HttpServletRequest request) {
return request.getParameter("pkg");
}
private String getAppToken(HttpServletRequest request) {
return request.getParameter("appToken");
}
@SuppressWarnings("deprecation")
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) {
if (registrationId == null) {
return null;
}
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
}
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
if (!StringUtils.isEmpty(appPackage)) {
if (StringUtils.isEmpty(appToken)) {
throw new IllegalArgumentException("Invalid application token.");
} else {
String appSecret = this.oAuth2Service.findAppSecret(UUID.fromString(registrationId), appPackage);
if (StringUtils.isEmpty(appSecret)) {
throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package.");
}
String callbackUrlScheme = this.oAuth2AppTokenFactory.validateTokenAndGetCallbackUrlScheme(appPackage, appToken, appSecret);
attributes.put(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME, callbackUrlScheme);
}
}
OAuth2AuthorizationRequest.Builder builder;
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.authorizationCode();
Map<String, Object> additionalParameters = new HashMap<>();
if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) &&
clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
addNonceParameters(attributes, additionalParameters);
}
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
addPkceParameters(attributes, additionalParameters);
}
builder.additionalParameters(additionalParameters);
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.implicit();
} else {
throw new IllegalArgumentException("Invalid Authorization Grant Type (" +
clientRegistration.getAuthorizationGrantType().getValue() +
") for Client Registration with Id: " + clientRegistration.getRegistrationId());
}
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
return builder
.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(redirectUriStr)
.scopes(clientRegistration.getScopes())
.state(this.stateGenerator.generateKey())
.attributes(attributes)
.build();
}
private String resolveRegistrationId(HttpServletRequest request) {
if (this.authorizationRequestMatcher.matches(request)) {
return this.authorizationRequestMatcher
.matcher(request).getVariables().get(REGISTRATION_ID_URI_VARIABLE_NAME);
}
return null;
}
/**
* Expands the {@link ClientRegistration#getRedirectUriTemplate()} with following provided variables:<br/>
* - baseUrl (e.g. https://localhost/app) <br/>
* - baseScheme (e.g. https) <br/>
* - baseHost (e.g. localhost) <br/>
* - basePort (e.g. :8080) <br/>
* - basePath (e.g. /app) <br/>
* - registrationId (e.g. google) <br/>
* - action (e.g. login) <br/>
* <p/>
* Null variables are provided as empty strings.
* <p/>
* Default redirectUriTemplate is: {@link org.springframework.security.config.oauth2.client}.CommonOAuth2Provider#DEFAULT_REDIRECT_URL
*
* @return expanded URI
*/
private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) {
Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("registrationId", clientRegistration.getRegistrationId());
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.build();
String scheme = uriComponents.getScheme();
uriVariables.put("baseScheme", scheme == null ? "" : scheme);
String host = uriComponents.getHost();
uriVariables.put("baseHost", host == null ? "" : host);
// following logic is based on HierarchicalUriComponents#toUriString()
int port = uriComponents.getPort();
uriVariables.put("basePort", port == -1 ? "" : ":" + port);
String path = uriComponents.getPath();
if (StringUtils.hasLength(path)) {
if (path.charAt(0) != PATH_DELIMITER) {
path = PATH_DELIMITER + path;
}
}
uriVariables.put("basePath", path == null ? "" : path);
uriVariables.put("baseUrl", uriComponents.toUriString());
uriVariables.put("action", action == null ? "" : action);
String redirectUri = getRedirectUri(request);
log.trace("Redirect URI - {}.", redirectUri);
return UriComponentsBuilder.fromUriString(redirectUri)
.buildAndExpand(uriVariables)
.toUriString();
}
private String getRedirectUri(HttpServletRequest request) {
String loginProcessingUri = oauth2Configuration != null ? oauth2Configuration.getLoginProcessingUrl() : DEFAULT_LOGIN_PROCESSING_URI;
String scheme = MiscUtils.getScheme(request);
String domainName = MiscUtils.getDomainName(request);
int port = MiscUtils.getPort(request);
String baseUrl = scheme + "://" + domainName;
if (needsPort(scheme, port)){
baseUrl += ":" + port;
}
return baseUrl + loginProcessingUri;
}
private boolean needsPort(String scheme, int port) {
boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80;
boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443;
return !isHttpDefault && !isHttpsDefault;
}
/**
* Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests.
*
* @param attributes where the {@link OidcParameterNames#NONCE} is stored for the authentication request
* @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is added for the authentication request
*
* @since 5.2
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest">3.1.2.1. Authentication Request</a>
*/
private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
try {
String nonce = this.secureKeyGenerator.generateKey();
String nonceHash = createHash(nonce);
attributes.put(OidcParameterNames.NONCE, nonce);
additionalParameters.put(OidcParameterNames.NONCE, nonceHash);
} catch (NoSuchAlgorithmException e) { }
}
/**
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
*
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
*
* @since 5.2
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
*/
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
String codeVerifier = this.secureKeyGenerator.generateKey();
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
try {
String codeChallenge = createHash(codeVerifier);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
} catch (NoSuchAlgorithmException e) {
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
}
}
private static String createHash(String value) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}
/**
* Copyright © 2016-2022 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.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import java.util.HashMap;
import java.util.Map;
/**
* Created by yyh on 2017/5/2.
* CORS configuration
*/
@Configuration
@ConfigurationProperties(prefix = "spring.mvc.cors")
public class MvcCorsProperties {
private Map<String, CorsConfiguration> mappings = new HashMap<>();
public MvcCorsProperties() {
super();
}
public Map<String, CorsConfiguration> getMappings() {
return mappings;
}
public void setMappings(Map<String, CorsConfiguration> mappings) {
this.mappings = mappings;
}
}
/**
* Copyright © 2016-2022 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Slf4j
@Component
public class RateLimitProcessingFilter extends OncePerRequestFilter {
@Autowired
private ThingsboardErrorResponseHandler errorResponseHandler;
@Autowired
@Lazy
private TbTenantProfileCache tenantProfileCache;
private final ConcurrentMap<TenantId, TbRateLimits> perTenantLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<CustomerId, TbRateLimits> perCustomerLimits = new ConcurrentHashMap<>();
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
SecurityUser user = getCurrentUser();
if (user != null && !user.isSystemAdmin()) {
var profile = tenantProfileCache.get(user.getTenantId());
if (profile == null) {
log.debug("[{}] Failed to lookup tenant profile", user.getTenantId());
errorResponseHandler.handle(new BadCredentialsException("Failed to lookup tenant profile"), response);
return;
}
var profileConfiguration = profile.getDefaultProfileConfiguration();
if (!checkRateLimits(user.getTenantId(), profileConfiguration.getTenantServerRestLimitsConfiguration(), perTenantLimits, response)) {
return;
}
if (user.isCustomerUser()) {
if (!checkRateLimits(user.getCustomerId(), profileConfiguration.getCustomerServerRestLimitsConfiguration(), perCustomerLimits, response)) {
return;
}
}
}
chain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
@Override
protected boolean shouldNotFilterErrorDispatch() {
return false;
}
private <I extends EntityId> boolean checkRateLimits(I ownerId, String rateLimitConfig, Map<I, TbRateLimits> rateLimitsMap, ServletResponse response) {
if (StringUtils.isNotEmpty(rateLimitConfig)) {
TbRateLimits rateLimits = rateLimitsMap.get(ownerId);
if (rateLimits == null || !rateLimits.getConfiguration().equals(rateLimitConfig)) {
rateLimits = new TbRateLimits(rateLimitConfig);
rateLimitsMap.put(ownerId, rateLimits);
}
if (!rateLimits.tryConsume()) {
errorResponseHandler.handle(new TbRateLimitsException(ownerId.getEntityType()), (HttpServletResponse) response);
return false;
}
} else {
rateLimitsMap.remove(ownerId);
}
return true;
}
protected SecurityUser getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {
return (SecurityUser) authentication.getPrincipal();
} else {
return null;
}
}
}
/**
* Copyright © 2016-2022 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.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@Configuration
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskScheduler());
}
@Bean(destroyMethod="shutdown")
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler threadPoolScheduler = new ThreadPoolTaskScheduler();
threadPoolScheduler.setThreadNamePrefix("TB-Scheduling-");
threadPoolScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
threadPoolScheduler.setRemoveOnCancelPolicy(true);
return threadPoolScheduler;
}
}
/**
* Copyright © 2016-2022 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 com.fasterxml.classmate.TypeResolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
import org.thingsboard.server.exception.ThingsboardErrorResponse;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.auth.rest.LoginResponse;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ExampleBuilder;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.RepresentationBuilder;
import springfox.documentation.builders.RequestParameterBuilder;
import springfox.documentation.builders.ResponseBuilder;
import springfox.documentation.schema.Example;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiListing;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.HttpLoginPasswordScheme;
import springfox.documentation.service.ParameterType;
import springfox.documentation.service.Response;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.service.Tag;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ApiListingBuilderPlugin;
import springfox.documentation.spi.service.ApiListingScannerPlugin;
import springfox.documentation.spi.service.contexts.ApiListingContext;
import springfox.documentation.spi.service.contexts.DocumentationContext;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator;
import springfox.documentation.swagger.common.SwaggerPluginSupport;
import springfox.documentation.swagger.web.DocExpansion;
import springfox.documentation.swagger.web.ModelRendering;
import springfox.documentation.swagger.web.OperationsSorter;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.function.Predicate.not;
import static springfox.documentation.builders.PathSelectors.any;
import static springfox.documentation.builders.PathSelectors.regex;
@Slf4j
@Configuration
@TbCoreComponent
@Profile("!test")
public class SwaggerConfiguration {
@Value("${swagger.api_path_regex}")
private String apiPathRegex;
@Value("${swagger.security_path_regex}")
private String securityPathRegex;
@Value("${swagger.non_security_path_regex}")
private String nonSecurityPathRegex;
@Value("${swagger.title}")
private String title;
@Value("${swagger.description}")
private String description;
@Value("${swagger.contact.name}")
private String contactName;
@Value("${swagger.contact.url}")
private String contactUrl;
@Value("${swagger.contact.email}")
private String contactEmail;
@Value("${swagger.license.title}")
private String licenseTitle;
@Value("${swagger.license.url}")
private String licenseUrl;
@Value("${swagger.version}")
private String version;
@Value("${app.version:unknown}")
private String appVersion;
@Bean
public Docket thingsboardApi() {
TypeResolver typeResolver = new TypeResolver();
return new Docket(DocumentationType.OAS_30)
.groupName("thingsboard")
.apiInfo(apiInfo())
.additionalModels(
typeResolver.resolve(ThingsboardErrorResponse.class),
typeResolver.resolve(ThingsboardCredentialsExpiredResponse.class),
typeResolver.resolve(LoginRequest.class),
typeResolver.resolve(LoginResponse.class)
)
.select()
.paths(apiPaths())
.paths(any())
.build()
.globalResponses(HttpMethod.GET,
defaultErrorResponses(false)
)
.globalResponses(HttpMethod.POST,
defaultErrorResponses(true)
)
.globalResponses(HttpMethod.DELETE,
defaultErrorResponses(false)
)
.securitySchemes(newArrayList(httpLogin()))
.securityContexts(newArrayList(securityContext()))
.enableUrlTemplating(true);
}
@Bean
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
ApiListingScannerPlugin loginEndpointListingScanner(final CachingOperationNameGenerator operationNames) {
return new ApiListingScannerPlugin() {
@Override
public List<ApiDescription> apply(DocumentationContext context) {
return List.of(loginEndpointApiDescription(operationNames));
}
@Override
public boolean supports(DocumentationType delimiter) {
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
}
};
}
@Bean
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
ApiListingBuilderPlugin loginEndpointListingBuilder() {
return new ApiListingBuilderPlugin() {
@Override
public void apply(ApiListingContext apiListingContext) {
if (apiListingContext.getResourceGroup().getGroupName().equals("default")) {
ApiListing apiListing = apiListingContext.apiListingBuilder().build();
if (apiListing.getResourcePath().equals("/api/auth/login")) {
apiListingContext.apiListingBuilder().tags(Set.of(new Tag("login-endpoint", "Login Endpoint")));
apiListingContext.apiListingBuilder().description("Login Endpoint");
}
}
}
@Override
public boolean supports(DocumentationType delimiter) {
return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter);
}
};
}
@Bean
UiConfiguration uiConfig() {
return UiConfigurationBuilder.builder()
.deepLinking(true)
.displayOperationId(false)
.defaultModelsExpandDepth(1)
.defaultModelExpandDepth(1)
.defaultModelRendering(ModelRendering.EXAMPLE)
.displayRequestDuration(false)
.docExpansion(DocExpansion.NONE)
.filter(false)
.maxDisplayedTags(null)
.operationsSorter(OperationsSorter.ALPHA)
.showExtensions(false)
.showCommonExtensions(false)
.supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
.validatorUrl(null)
.persistAuthorization(true)
.syntaxHighlightActivate(true)
.syntaxHighlightTheme("agate")
.build();
}
private SecurityScheme httpLogin() {
return HttpLoginPasswordScheme
.X_AUTHORIZATION_BUILDER
.loginEndpoint("/api/auth/login")
.name("HTTP login form")
.description("Enter Username / Password")
.build();
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(defaultAuth())
.operationSelector(securityPathOperationSelector())
.build();
}
private Predicate<String> apiPaths() {
return regex(apiPathRegex);
}
private Predicate<OperationContext> securityPathOperationSelector() {
return new SecurityPathOperationSelector(securityPathRegex, nonSecurityPathRegex);
}
List<SecurityReference> defaultAuth() {
AuthorizationScope[] authorizationScopes = new AuthorizationScope[3];
authorizationScopes[0] = new AuthorizationScope(Authority.SYS_ADMIN.name(), "System administrator");
authorizationScopes[1] = new AuthorizationScope(Authority.TENANT_ADMIN.name(), "Tenant administrator");
authorizationScopes[2] = new AuthorizationScope(Authority.CUSTOMER_USER.name(), "Customer");
return newArrayList(
new SecurityReference("HTTP login form", authorizationScopes));
}
private ApiInfo apiInfo() {
String apiVersion = version;
if (StringUtils.isEmpty(apiVersion)) {
apiVersion = appVersion;
}
return new ApiInfoBuilder()
.title(title)
.description(description)
.contact(new Contact(contactName, contactUrl, contactEmail))
.license(licenseTitle)
.licenseUrl(licenseUrl)
.version(apiVersion)
.build();
}
private ApiDescription loginEndpointApiDescription(final CachingOperationNameGenerator operationNames) {
return new ApiDescription(null, "/api/auth/login", "Login method to get user JWT token data", "Login endpoint", Collections.singletonList(
new OperationBuilder(operationNames)
.summary("Login method to get user JWT token data")
.tags(Set.of("login-endpoint"))
.authorizations(new ArrayList<>())
.position(0)
.codegenMethodNameStem("loginPost")
.method(HttpMethod.POST)
.notes("Login method used to authenticate user and get JWT token data.\n\nValue of the response **token** " +
"field can be used as **X-Authorization** header value:\n\n`X-Authorization: Bearer $JWT_TOKEN_VALUE`.")
.requestParameters(
List.of(
new RequestParameterBuilder()
.in(ParameterType.BODY)
.required(true)
.description("Login request")
.content(c ->
c.requestBody(true)
.representation(MediaType.APPLICATION_JSON)
.apply(classRepresentation(LoginRequest.class, false))
)
.build()
)
)
.responses(loginResponses())
.build()
), false);
}
private Collection<Response> loginResponses() {
List<Response> responses = new ArrayList<>();
responses.add(
new ResponseBuilder()
.code("200")
.description("OK")
.representation(MediaType.APPLICATION_JSON)
.apply(classRepresentation(LoginResponse.class, true)).
build()
);
responses.addAll(loginErrorResponses());
return responses;
}
/** Helper methods **/
private List<Response> defaultErrorResponses(boolean isPost) {
return List.of(
errorResponse("400", "Bad Request",
ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST)),
errorResponse("401", "Unauthorized",
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
errorResponse("403", "Forbidden",
ThingsboardErrorResponse.of("You don't have permission to perform this operation!",
ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN)),
errorResponse("404", "Not Found",
ThingsboardErrorResponse.of("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND, HttpStatus.NOT_FOUND)),
errorResponse("429", "Too Many Requests",
ThingsboardErrorResponse.of("Too many requests for current tenant!",
ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS))
);
}
private List<Response> loginErrorResponses() {
return List.of(
errorResponse("401", "Unauthorized",
List.of(
errorExample("bad-credentials", "Bad credentials",
ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
errorExample("token-expired", "JWT token expired",
ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)),
errorExample("account-disabled", "Disabled account",
ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
errorExample("account-locked", "Locked account",
ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)),
errorExample("authentication-failed", "General authentication error",
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))
)
),
errorResponse("401 ", "Unauthorized (**Expired credentials**)",
List.of(
errorExample("credentials-expired", "Expired credentials",
ThingsboardCredentialsExpiredResponse.of("User password expired!", StringUtils.randomAlphanumeric(30)))
), ThingsboardCredentialsExpiredResponse.class
)
);
}
private Response errorResponse(String code, String description, ThingsboardErrorResponse example) {
return errorResponse(code, description, List.of(errorExample("error-code-" + code, description, example)));
}
private Response errorResponse(String code, String description, List<Example> examples) {
return errorResponse(code, description, examples, ThingsboardErrorResponse.class);
}
private Response errorResponse(String code, String description, List<Example> examples,
Class<? extends ThingsboardErrorResponse> errorResponseClass) {
return new ResponseBuilder()
.code(code)
.description(description)
.examples(examples)
.representation(MediaType.APPLICATION_JSON)
.apply(classRepresentation(errorResponseClass, true))
.build();
}
private Example errorExample(String id, String summary, ThingsboardErrorResponse example) {
return new ExampleBuilder()
.mediaType(MediaType.APPLICATION_JSON_VALUE)
.summary(summary)
.id(id)
.value(example).build();
}
private Consumer<RepresentationBuilder> classRepresentation(Class<?> clazz, boolean isResponse) {
return r -> r.model(
m ->
m.referenceModel(ref ->
ref.key(k ->
k.qualifiedModelName(q ->
q.namespace(clazz.getPackageName())
.name(clazz.getSimpleName())).isResponse(isResponse)))
);
}
private static class SecurityPathOperationSelector implements Predicate<OperationContext> {
private final Predicate<String> securityPathSelector;
SecurityPathOperationSelector(String securityPathRegex, String nonSecurityPathRegex) {
this.securityPathSelector = regex(securityPathRegex).and(
not(
regex(nonSecurityPathRegex)
));
}
@Override
public boolean test(OperationContext operationContext) {
return this.securityPathSelector.test(operationContext.requestMappingPattern());
}
}
}
/**
* Copyright © 2016-2022 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.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.ResourceBundleMessageSource;
@Configuration
public class ThingsboardMessageConfiguration {
@Bean
@Primary
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
/**
* Copyright © 2016-2022 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 com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenProcessingFilter;
import org.thingsboard.server.service.security.auth.jwt.SkipPathRequestMatcher;
import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
import org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@Order(SecurityProperties.BASIC_AUTH_ORDER)
@TbCoreComponent
public class ThingsboardSecurityConfiguration {
public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
public static final String JWT_TOKEN_HEADER_PARAM_V2 = "Authorization";
public static final String JWT_TOKEN_QUERY_PARAM = "token";
public static final String WEBJARS_ENTRY_POINT = "/webjars/**";
public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**";
@Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler;
@Autowired(required = false)
@Qualifier("oauth2AuthenticationSuccessHandler")
private AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler;
@Autowired(required = false)
@Qualifier("oauth2AuthenticationFailureHandler")
private AuthenticationFailureHandler oauth2AuthenticationFailureHandler;
@Autowired(required = false)
private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
@Autowired
@Qualifier("defaultAuthenticationSuccessHandler")
private AuthenticationSuccessHandler successHandler;
@Autowired
@Qualifier("defaultAuthenticationFailureHandler")
private AuthenticationFailureHandler failureHandler;
@Autowired private RestAuthenticationProvider restAuthenticationProvider;
@Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
@Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
@Autowired(required = false) OAuth2Configuration oauth2Configuration;
@Autowired
@Qualifier("jwtHeaderTokenExtractor")
private TokenExtractor jwtHeaderTokenExtractor;
@Autowired
@Qualifier("jwtQueryTokenExtractor")
private TokenExtractor jwtQueryTokenExtractor;
@Autowired private AuthenticationManager authenticationManager;
@Autowired private ObjectMapper objectMapper;
@Autowired private RateLimitProcessingFilter rateLimitProcessingFilter;
@Bean
protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean
protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception {
RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean
protected RefreshTokenProcessingFilter buildRefreshTokenProcessingFilter() throws Exception {
RefreshTokenProcessingFilter filter = new RefreshTokenProcessingFilter(TOKEN_REFRESH_ENTRY_POINT, successHandler, failureHandler, objectMapper);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean
protected JwtTokenAuthenticationProcessingFilter buildWsJwtTokenAuthenticationProcessingFilter() throws Exception {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(WS_TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtQueryTokenExtractor, matcher);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean
public AuthenticationManager authenticationManager(ObjectPostProcessor<Object> objectPostProcessor) throws Exception {
DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
.postProcess(new DefaultAuthenticationEventPublisher());
var auth = new AuthenticationManagerBuilder(objectPostProcessor);
auth.authenticationEventPublisher(eventPublisher);
auth.authenticationProvider(restAuthenticationProvider);
auth.authenticationProvider(jwtAuthenticationProvider);
auth.authenticationProvider(refreshTokenAuthenticationProvider);
return auth.build();
}
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
@Bean
@Order(0)
SecurityFilterChain resources(HttpSecurity http) throws Exception {
http
.requestMatchers((matchers) -> matchers.antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"))
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
.requestCache().disable()
.securityContext().disable()
.sessionManagement().disable();
return http.build();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers().cacheControl().and().frameOptions().disable()
.and()
.cors()
.and()
.csrf().disable()
.exceptionHandling()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars
.antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.and()
.authorizeRequests()
.antMatchers(WS_TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected WebSocket API End-points
.antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected API End-points
.and()
.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
.and()
.addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
if (oauth2Configuration != null) {
http.oauth2Login()
.authorizationEndpoint()
.authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository)
.authorizationRequestResolver(oAuth2AuthorizationRequestResolver)
.and()
.loginPage("/oauth2Login")
.loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl())
.successHandler(oauth2AuthenticationSuccessHandler)
.failureHandler(oauth2AuthenticationFailureHandler);
}
return http.build();
}
@Bean
@ConditionalOnMissingBean(CorsFilter.class)
public CorsFilter corsFilter(@Autowired MvcCorsProperties mvcCorsProperties) {
if (mvcCorsProperties.getMappings().size() == 0) {
return new CorsFilter(new UrlBasedCorsConfigurationSource());
} else {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.setCorsConfigurations(mvcCorsProperties.getMappings());
return new CorsFilter(source);
}
}
}
/**
* Copyright © 2016-2022 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.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller
public class WebConfig {
@RequestMapping(value = {"/assets", "/assets/", "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)(?!swagger-ui$)[^\\.]*}/**"})
public String redirect() {
return "forward:/index.html";
}
@RequestMapping("/swagger-ui.html")
public void redirectSwagger(HttpServletRequest request, HttpServletResponse response) throws IOException {
String baseUrl = MiscUtils.constructBaseUrl(request);
response.sendRedirect(baseUrl + "/swagger-ui/");
}
}
/**
* Copyright © 2016-2022 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.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.controller.plugin.TbWebSocketHandler;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
@Configuration
@TbCoreComponent
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/";
private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**";
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(32768);
container.setMaxBinaryMessageBufferSize(32768);
return container;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*")
.addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
SecurityUser user = null;
try {
user = getCurrentUser();
} catch (ThingsboardException ex) {
}
if (user == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
} else {
return true;
}
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
//Do nothing
}
});
}
@Bean
public WebSocketHandler wsHandler() {
return new TbWebSocketHandler();
}
protected SecurityUser getCurrentUser() throws ThingsboardException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {
return (SecurityUser) authentication.getPrincipal();
} else {
throw new ThingsboardException("You aren't authorized to perform this operation!", ThingsboardErrorCode.AUTHENTICATION);
}
}
}
/**
* Copyright © 2016-2022 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 com.google.common.util.concurrent.FutureCallback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rpc.LocalRequestMetaData;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
import javax.annotation.Nullable;
import java.util.Optional;
import java.util.UUID;
/**
* Created by ashvayka on 22.03.18.
*/
@TbCoreComponent
@Slf4j
public abstract class AbstractRpcController extends BaseController {
@Autowired
protected TbCoreDeviceRpcService deviceRpcService;
@Autowired
protected AccessValidator accessValidator;
@Value("${server.rest.server_side_rpc.min_timeout:5000}")
protected long minTimeout;
@Value("${server.rest.server_side_rpc.default_timeout:10000}")
protected long defaultTimeout;
protected DeferredResult<ResponseEntity> handleDeviceRPCRequest(boolean oneWay, DeviceId deviceId, String requestBody, HttpStatus timeoutStatus, HttpStatus noActiveConnectionStatus) throws ThingsboardException {
try {
JsonNode rpcRequestBody = JacksonUtil.toJsonNode(requestBody);
ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(rpcRequestBody.get("method").asText(), JacksonUtil.toString(rpcRequestBody.get("params")));
SecurityUser currentUser = getCurrentUser();
TenantId tenantId = currentUser.getTenantId();
final DeferredResult<ResponseEntity> response = new DeferredResult<>();
long timeout = rpcRequestBody.has(DataConstants.TIMEOUT) ? rpcRequestBody.get(DataConstants.TIMEOUT).asLong() : defaultTimeout;
long expTime = rpcRequestBody.has(DataConstants.EXPIRATION_TIME) ? rpcRequestBody.get(DataConstants.EXPIRATION_TIME).asLong() : System.currentTimeMillis() + Math.max(minTimeout, timeout);
UUID rpcRequestUUID = rpcRequestBody.has("requestUUID") ? UUID.fromString(rpcRequestBody.get("requestUUID").asText()) : UUID.randomUUID();
boolean persisted = rpcRequestBody.has(DataConstants.PERSISTENT) && rpcRequestBody.get(DataConstants.PERSISTENT).asBoolean();
String additionalInfo = JacksonUtil.toString(rpcRequestBody.get(DataConstants.ADDITIONAL_INFO));
Integer retries = rpcRequestBody.has(DataConstants.RETRIES) ? rpcRequestBody.get(DataConstants.RETRIES).asInt() : null;
accessValidator.validate(currentUser, Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(rpcRequestUUID,
tenantId,
deviceId,
oneWay,
expTime,
body,
persisted,
retries,
additionalInfo
);
deviceRpcService.processRestApiRpcRequest(rpcRequest, fromDeviceRpcResponse -> reply(new LocalRequestMetaData(rpcRequest, currentUser, result), fromDeviceRpcResponse, timeoutStatus, noActiveConnectionStatus), currentUser);
}
@Override
public void onFailure(Throwable e) {
ResponseEntity entity;
if (e instanceof ToErrorResponseEntity) {
entity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
} else {
entity = new ResponseEntity(HttpStatus.UNAUTHORIZED);
}
logRpcCall(currentUser, deviceId, body, oneWay, Optional.empty(), e);
response.setResult(entity);
}
}));
return response;
} catch (IllegalArgumentException ioe) {
throw new ThingsboardException("Invalid request body", ioe, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
public void reply(LocalRequestMetaData rpcRequest, FromDeviceRpcResponse response, HttpStatus timeoutStatus, HttpStatus noActiveConnectionStatus) {
Optional<RpcError> rpcError = response.getError();
DeferredResult<ResponseEntity> responseWriter = rpcRequest.getResponseWriter();
if (rpcError.isPresent()) {
logRpcCall(rpcRequest, rpcError, null);
RpcError error = rpcError.get();
switch (error) {
case TIMEOUT:
responseWriter.setResult(new ResponseEntity<>(timeoutStatus));
break;
case NO_ACTIVE_CONNECTION:
responseWriter.setResult(new ResponseEntity<>(noActiveConnectionStatus));
break;
default:
responseWriter.setResult(new ResponseEntity<>(timeoutStatus));
break;
}
} else {
Optional<String> responseData = response.getResponse();
if (responseData.isPresent() && !StringUtils.isEmpty(responseData.get())) {
String data = responseData.get();
try {
logRpcCall(rpcRequest, rpcError, null);
responseWriter.setResult(new ResponseEntity<>(JacksonUtil.toJsonNode(data), HttpStatus.OK));
} catch (IllegalArgumentException e) {
log.debug("Failed to decode device response: {}", data, e);
logRpcCall(rpcRequest, rpcError, e);
responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
}
} else {
logRpcCall(rpcRequest, rpcError, null);
responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
}
}
}
private void logRpcCall(LocalRequestMetaData rpcRequest, Optional<RpcError> rpcError, Throwable e) {
logRpcCall(rpcRequest.getUser(), rpcRequest.getRequest().getDeviceId(), rpcRequest.getRequest().getBody(), rpcRequest.getRequest().isOneway(), rpcError, null);
}
private void logRpcCall(SecurityUser user, EntityId entityId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Throwable e) {
String rpcErrorStr = "";
if (rpcError.isPresent()) {
rpcErrorStr = "RPC Error: " + rpcError.get().name();
}
String method = body.getMethod();
String params = body.getParams();
auditLogService.logEntityAction(
user.getTenantId(),
user.getCustomerId(),
user.getId(),
user.getName(),
(UUIDBased & EntityId) entityId,
null,
ActionType.RPC_CALL,
BaseController.toException(e),
rpcErrorStr,
oneWay,
method,
params);
}
}
/**
* Copyright © 2016-2022 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.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
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 org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
import org.thingsboard.server.service.update.UpdateService;
import static org.thingsboard.server.controller.ControllerConstants.*;
@RestController
@TbCoreComponent
@RequestMapping("/api/admin")
public class AdminController extends BaseController {
@Autowired
private MailService mailService;
@Autowired
private SmsService smsService;
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private SystemSecurityService systemSecurityService;
@Lazy
@Autowired
private JwtSettingsService jwtSettingsService;
@Lazy
@Autowired
private JwtTokenFactory tokenFactory;
@Autowired
private EntitiesVersionControlService versionControlService;
@Autowired
private TbAutoCommitSettingsService autoCommitSettingsService;
@Autowired
private UpdateService updateService;
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/{key}", method = RequestMethod.GET)
@ResponseBody
public AdminSettings getAdminSettings(
@ApiParam(value = "A string value of the key (e.g. 'general' or 'mail').")
@PathVariable("key") String key) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key);
if (adminSettings.getKey().equals("mail")) {
((ObjectNode) adminSettings.getJsonValue()).remove("password");
}
return adminSettings;
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " +
"The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " +
"Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings", method = RequestMethod.POST)
@ResponseBody
public AdminSettings saveAdminSettings(
@ApiParam(value = "A JSON value representing the Administration Settings.")
@RequestBody AdminSettings adminSettings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
adminSettings.setTenantId(getTenantId());
adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
if (adminSettings.getKey().equals("mail")) {
mailService.updateMailConfiguration();
((ObjectNode) adminSettings.getJsonValue()).remove("password");
} else if (adminSettings.getKey().equals("sms")) {
smsService.updateSmsConfiguration();
}
return adminSettings;
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get the Security Settings object",
notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.GET)
@ResponseBody
public SecuritySettings getSecuritySettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Update Security Settings (saveSecuritySettings)",
notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.POST)
@ResponseBody
public SecuritySettings saveSecuritySettings(
@ApiParam(value = "A JSON value representing the Security Settings.")
@RequestBody SecuritySettings securitySettings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(TenantId.SYS_TENANT_ID, securitySettings));
return securitySettings;
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get the JWT Settings object (getJwtSettings)",
notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.GET)
@ResponseBody
public JwtSettings getJwtSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(jwtSettingsService.getJwtSettings());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Update JWT Settings (saveJwtSettings)",
notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.POST)
@ResponseBody
public JwtPair saveJwtSettings(
@ApiParam(value = "A JSON value representing the JWT Settings.")
@RequestBody JwtSettings jwtSettings) throws ThingsboardException {
try {
SecurityUser securityUser = getCurrentUser();
accessControlService.checkPermission(securityUser, Resource.ADMIN_SETTINGS, Operation.WRITE);
checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings));
return tokenFactory.createTokenPair(securityUser);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Send test email (sendTestMail)",
notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " +
"You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testMail", method = RequestMethod.POST)
public void sendTestMail(
@ApiParam(value = "A JSON value representing the Mail Settings.")
@RequestBody AdminSettings adminSettings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
adminSettings = checkNotNull(adminSettings);
if (adminSettings.getKey().equals("mail")) {
if (!adminSettings.getJsonValue().has("password")) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"));
((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
}
String email = getCurrentUser().getEmail();
mailService.sendTestMail(adminSettings.getJsonValue(), email);
}
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Send test sms (sendTestMail)",
notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testSms", method = RequestMethod.POST)
public void sendTestSms(
@ApiParam(value = "A JSON value representing the Test SMS request.")
@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
smsService.sendTestSms(testSmsRequest);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get repository settings (getRepositorySettings)",
notes = "Get the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings")
public RepositorySettings getRepositorySettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
RepositorySettings versionControlSettings = checkNotNull(versionControlService.getVersionControlSettings(getTenantId()));
versionControlSettings.setPassword(null);
versionControlSettings.setPrivateKey(null);
versionControlSettings.setPrivateKeyPassword(null);
return versionControlSettings;
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Check repository settings exists (repositorySettingsExists)",
notes = "Check whether the repository settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings/exists")
public Boolean repositorySettingsExists() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
return versionControlService.getVersionControlSettings(getTenantId()) != null;
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings/info")
public RepositorySettingsInfo getRepositorySettingsInfo() throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
RepositorySettings repositorySettings = versionControlService.getVersionControlSettings(getTenantId());
if (repositorySettings != null) {
return RepositorySettingsInfo.builder()
.configured(true)
.readOnly(repositorySettings.isReadOnly())
.build();
} else {
return RepositorySettingsInfo.builder()
.configured(false)
.build();
}
}
@ApiOperation(value = "Creates or Updates the repository settings (saveRepositorySettings)",
notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/repositorySettings")
public DeferredResult<RepositorySettings> saveRepositorySettings(@RequestBody RepositorySettings settings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
ListenableFuture<RepositorySettings> future = versionControlService.saveVersionControlSettings(getTenantId(), settings);
return wrapFuture(Futures.transform(future, savedSettings -> {
savedSettings.setPassword(null);
savedSettings.setPrivateKey(null);
savedSettings.setPrivateKeyPassword(null);
return savedSettings;
}, MoreExecutors.directExecutor()));
}
@ApiOperation(value = "Delete repository settings (deleteRepositorySettings)",
notes = "Deletes the repository settings."
+ TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public DeferredResult<Void> deleteRepositorySettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId()));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Check repository access (checkRepositoryAccess)",
notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST)
public DeferredResult<Void> checkRepositoryAccess(
@ApiParam(value = "A JSON value representing the Repository Settings.")
@RequestBody RepositorySettings settings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
settings = checkNotNull(settings);
return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get auto commit settings (getAutoCommitSettings)",
notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings")
public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
return checkNotNull(autoCommitSettingsService.get(getTenantId()));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Check auto commit settings exists (autoCommitSettingsExists)",
notes = "Check whether the auto commit settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings/exists")
public Boolean autoCommitSettingsExists() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
return autoCommitSettingsService.get(getTenantId()) != null;
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Creates or Updates the auto commit settings (saveAutoCommitSettings)",
notes = "Creates or Updates the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/autoCommitSettings")
public AutoCommitSettings saveAutoCommitSettings(@RequestBody AutoCommitSettings settings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
return autoCommitSettingsService.save(getTenantId(), settings);
}
@ApiOperation(value = "Delete auto commit settings (deleteAutoCommitSettings)",
notes = "Deletes the auto commit settings."
+ TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteAutoCommitSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
autoCommitSettingsService.delete(getTenantId());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Check for new Platform Releases (checkUpdates)",
notes = "Check notifications about new platform releases. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/updates", method = RequestMethod.GET)
@ResponseBody
public UpdateMessage checkUpdates() throws ThingsboardException {
try {
return updateService.checkUpdates();
} catch (Exception e) {
throw handleException(e);
}
}
}
/**
* Copyright © 2016-2022 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 io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@TbCoreComponent
@RequiredArgsConstructor
@RequestMapping("/api")
public class AlarmController extends BaseController {
private final TbAlarmService tbAlarmService;
public static final String ALARM_ID = "alarmId";
private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. ";
private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value";
private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK";
private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value";
private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK";
private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status";
private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'.";
private static final String ALARM_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'.";
private static final String ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION = "A boolean value to specify if the alarm originator name will be " +
"filled in the AlarmInfo object field: 'originatorName' or will returns as null.";
@ApiOperation(value = "Get Alarm (getAlarmById)",
notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET)
@ResponseBody
public Alarm getAlarmById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId);
try {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
return checkAlarmId(alarmId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Alarm Info (getAlarmInfoById)",
notes = "Fetch the Alarm Info object based on the provided Alarm Id. " +
ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/info/{alarmId}", method = RequestMethod.GET)
@ResponseBody
public AlarmInfo getAlarmInfoById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId);
try {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
return checkAlarmInfoId(alarmId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Create or update Alarm (saveAlarm)",
notes = "Creates or Updates the Alarm. " +
"When creating alarm, platform generates Alarm Id as " + UUID_WIKI_LINK +
"The newly created Alarm id will be present in the response. Specify existing Alarm id to update the alarm. " +
"Referencing non-existing Alarm Id will cause 'Not Found' error. " +
"\n\nPlatform also deduplicate the alarms based on the entity id of originator and alarm 'type'. " +
"For example, if the user or system component create the alarm with the type 'HighTemperature' for device 'Device A' the new active alarm is created. " +
"If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " +
"If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Alarm entity. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm", method = RequestMethod.POST)
@ResponseBody
public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException {
alarm.setTenantId(getTenantId());
checkEntity(alarm.getId(), alarm, Resource.ALARM);
return tbAlarmService.save(alarm, getCurrentUser());
}
@ApiOperation(value = "Delete Alarm (deleteAlarm)",
notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE)
@ResponseBody
public Boolean deleteAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.DELETE);
return tbAlarmService.delete(alarm, getCurrentUser());
}
@ApiOperation(value = "Acknowledge Alarm (ackAlarm)",
notes = "Acknowledge the Alarm. " +
"Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
tbAlarmService.ack(alarm, getCurrentUser()).get();
}
@ApiOperation(value = "Clear Alarm (clearAlarm)",
notes = "Clear the Alarm. " +
"Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
tbAlarmService.clear(alarm, getCurrentUser()).get();
}
@ApiOperation(value = "Get Alarms (getAlarms)",
notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAlarms(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable(ENTITY_TYPE) String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_ID) String strEntityId,
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String searchStatus,
@ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String status,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime,
@ApiParam(value = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
@RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException {
checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
if (alarmSearchStatus != null && alarmStatus != null) {
throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " +
"and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
checkEntityId(entityId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
try {
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get All Alarms (getAllAlarms)",
notes = "Returns a page of alarms that belongs to the current user owner. " +
"If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " +
"If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " +
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarms", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAllAlarms(
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String searchStatus,
@ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String status,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime,
@ApiParam(value = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
@RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException {
AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
if (alarmSearchStatus != null && alarmStatus != null) {
throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " +
"and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
try {
if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get());
} else {
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get());
}
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)",
notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " +
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public AlarmSeverity getHighestAlarmSeverity(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable(ENTITY_TYPE) String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_ID) String strEntityId,
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String searchStatus,
@ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String status
) throws ThingsboardException {
checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
if (alarmSearchStatus != null && alarmStatus != null) {
throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " +
"and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
checkEntityId(entityId, Operation.READ);
try {
return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus);
} catch (Exception e) {
throw handleException(e);
}
}
}
/**
* Copyright © 2016-2022 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.google.common.util.concurrent.ListenableFuture;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.asset.AssetBulkImportService;
import org.thingsboard.server.service.entitiy.asset.TbAssetService;
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 java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_NAME_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class AssetController extends BaseController {
private final AssetBulkImportService assetBulkImportService;
private final TbAssetService tbAssetService;
public static final String ASSET_ID = "assetId";
@ApiOperation(value = "Get Asset (getAssetById)",
notes = "Fetch the Asset object based on the provided Asset Id. " +
"If the user has the authority of 'Tenant Administrator', the server checks that the asset is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the asset is assigned to the same customer." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/asset/{assetId}", method = RequestMethod.GET)
@ResponseBody
public Asset getAssetById(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
checkParameter(ASSET_ID, strAssetId);
try {
AssetId assetId = new AssetId(toUUID(strAssetId));
return checkAssetId(assetId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Asset Info (getAssetInfoById)",
notes = "Fetch the Asset Info object based on the provided Asset Id. " +
"If the user has the authority of 'Tenant Administrator', the server checks that the asset is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the asset is assigned to the same customer. "
+ ASSET_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/asset/info/{assetId}", method = RequestMethod.GET)
@ResponseBody
public AssetInfo getAssetInfoById(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
checkParameter(ASSET_ID, strAssetId);
try {
AssetId assetId = new AssetId(toUUID(strAssetId));
return checkAssetInfoId(assetId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Create Or Update Asset (saveAsset)",
notes = "Creates or Updates the Asset. When creating asset, platform generates Asset Id as " + UUID_WIKI_LINK +
"The newly created Asset id will be present in the response. " +
"Specify existing Asset id to update the asset. " +
"Referencing non-existing Asset Id will cause 'Not Found' error. " +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Asset entity. "
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/asset", method = RequestMethod.POST)
@ResponseBody
public Asset saveAsset(@ApiParam(value = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception {
asset.setTenantId(getTenantId());
checkEntity(asset.getId(), asset, Resource.ASSET);
return tbAssetService.save(asset, getCurrentUser());
}
@ApiOperation(value = "Delete asset (deleteAsset)",
notes = "Deletes the asset and all the relations (from and to the asset). Referencing non-existing asset Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteAsset(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws Exception {
checkParameter(ASSET_ID, strAssetId);
AssetId assetId = new AssetId(toUUID(strAssetId));
Asset asset = checkAssetId(assetId, Operation.DELETE);
tbAssetService.delete(asset, getCurrentUser()).get();
}
@ApiOperation(value = "Assign asset to customer (assignAssetToCustomer)",
notes = "Creates assignment of the asset to customer. Customer will be able to query asset afterwards." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/asset/{assetId}", method = RequestMethod.POST)
@ResponseBody
public Asset assignAssetToCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId,
@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
checkParameter(ASSET_ID, strAssetId);
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
Customer customer = checkCustomerId(customerId, Operation.READ);
AssetId assetId = new AssetId(toUUID(strAssetId));
checkAssetId(assetId, Operation.ASSIGN_TO_CUSTOMER);
return tbAssetService.assignAssetToCustomer(getTenantId(), assetId, customer, getCurrentUser());
}
@ApiOperation(value = "Unassign asset from customer (unassignAssetFromCustomer)",
notes = "Clears assignment of the asset to customer. Customer will not be able to query asset afterwards." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/asset/{assetId}", method = RequestMethod.DELETE)
@ResponseBody
public Asset unassignAssetFromCustomer(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
checkParameter(ASSET_ID, strAssetId);
AssetId assetId = new AssetId(toUUID(strAssetId));
Asset asset = checkAssetId(assetId, Operation.UNASSIGN_FROM_CUSTOMER);
if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Asset isn't assigned to any customer!");
}
Customer customer = checkCustomerId(asset.getCustomerId(), Operation.READ);
return tbAssetService.unassignAssetToCustomer(getTenantId(), assetId, customer, getCurrentUser());
}
@ApiOperation(value = "Make asset publicly available (assignAssetToPublicCustomer)",
notes = "Asset will be available for non-authorized (not logged-in) users. " +
"This is useful to create dashboards that you plan to share/embed on a publicly available website. " +
"However, users that are logged-in and belong to different tenant will not be able to access the asset." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/asset/{assetId}", method = RequestMethod.POST)
@ResponseBody
public Asset assignAssetToPublicCustomer(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
checkParameter(ASSET_ID, strAssetId);
AssetId assetId = new AssetId(toUUID(strAssetId));
checkAssetId(assetId, Operation.ASSIGN_TO_CUSTOMER);
return tbAssetService.assignAssetToPublicCustomer(getTenantId(), assetId, getCurrentUser());
}
@ApiOperation(value = "Get Tenant Assets (getTenantAssets)",
notes = "Returns a page of assets owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<Asset> getTenantAssets(
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink));
} else {
return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink));
}
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Tenant Asset Infos (getTenantAssetInfos)",
notes = "Returns a page of assets info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AssetInfo> getTenantAssetInfos(
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String assetProfileId,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetInfosByTenantIdAndType(tenantId, type, pageLink));
} else if (assetProfileId != null && assetProfileId.length() > 0) {
AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId));
return checkNotNull(assetService.findAssetInfosByTenantIdAndAssetProfileId(tenantId, profileId, pageLink));
} else {
return checkNotNull(assetService.findAssetInfosByTenantId(tenantId, pageLink));
}
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Tenant Asset (getTenantAsset)",
notes = "Requested asset must be owned by tenant that the user belongs to. " +
"Asset name is an unique property of asset. So it can be used to identify the asset." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET)
@ResponseBody
public Asset getTenantAsset(
@ApiParam(value = ASSET_NAME_DESCRIPTION)
@RequestParam String assetName) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Customer Assets (getCustomerAssets)",
notes = "Returns a page of assets objects assigned to customer. " +
PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<Asset> getCustomerAssets(
@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
} else {
return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
}
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Customer Asset Infos (getCustomerAssetInfos)",
notes = "Returns a page of assets info objects assigned to customer. " +
PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AssetInfo> getCustomerAssetInfos(
@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String assetProfileId,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
} else if (assetProfileId != null && assetProfileId.length() > 0) {
AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId));
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(tenantId, customerId, profileId, pageLink));
} else {
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
}
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Assets By Ids (getAssetsByIds)",
notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET)
@ResponseBody
public List<Asset> getAssetsByIds(
@ApiParam(value = "A list of assets ids, separated by comma ','")
@RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException {
checkArrayParameter("assetIds", strAssetIds);
try {
SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId();
CustomerId customerId = user.getCustomerId();
List<AssetId> assetIds = new ArrayList<>();
for (String strAssetId : strAssetIds) {
assetIds.add(new AssetId(toUUID(strAssetId)));
}
ListenableFuture<List<Asset>> assets;
if (customerId == null || customerId.isNullUid()) {
assets = assetService.findAssetsByTenantIdAndIdsAsync(tenantId, assetIds);
} else {
assets = assetService.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, assetIds);
}
return checkNotNull(assets.get());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Find related assets (findByQuery)",
notes = "Returns all assets that are related to the specific entity. " +
"The entity id, relation type, asset types, depth of the search, and other query parameters defined using complex 'AssetSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info.", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assets", method = RequestMethod.POST)
@ResponseBody
public List<Asset> findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException {
checkNotNull(query);
checkNotNull(query.getParameters());
checkNotNull(query.getAssetTypes());
checkEntityId(query.getParameters().getEntityId(), Operation.READ);
try {
List<Asset> assets = checkNotNull(assetService.findAssetsByQuery(getTenantId(), query).get());
assets = assets.stream().filter(asset -> {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, Operation.READ, asset.getId(), asset);
return true;
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
return assets;
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Asset Types (getAssetTypes)",
notes = "Returns a set of unique asset types based on assets that are either owned by the tenant or assigned to the customer which user is performing the request.", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/asset/types", method = RequestMethod.GET)
@ResponseBody
public List<EntitySubtype> getAssetTypes() throws ThingsboardException {
try {
SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId();
ListenableFuture<List<EntitySubtype>> assetTypes = assetService.findAssetTypesByTenantId(tenantId);
return checkNotNull(assetTypes.get());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Assign asset to edge (assignAssetToEdge)",
notes = "Creates assignment of an existing asset to an instance of The Edge. " +
EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION +
"Second, remote edge service will receive a copy of assignment asset " +
EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION +
"Third, once asset will be delivered to edge service, it's going to be available for usage on remote edge instance.",
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST)
@ResponseBody
public Asset assignAssetToEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId,
@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
checkParameter(ASSET_ID, strAssetId);
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
Edge edge = checkEdgeId(edgeId, Operation.READ);
AssetId assetId = new AssetId(toUUID(strAssetId));
checkAssetId(assetId, Operation.READ);
return tbAssetService.assignAssetToEdge(getTenantId(), assetId, edge, getCurrentUser());
}
@ApiOperation(value = "Unassign asset from edge (unassignAssetFromEdge)",
notes = "Clears assignment of the asset to the edge. " +
EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION +
"Second, remote edge service will receive an 'unassign' command to remove asset " +
EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION +
"Third, once 'unassign' command will be delivered to edge service, it's going to remove asset locally.",
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.DELETE)
@ResponseBody
public Asset unassignAssetFromEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId,
@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
checkParameter(ASSET_ID, strAssetId);
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
Edge edge = checkEdgeId(edgeId, Operation.READ);
AssetId assetId = new AssetId(toUUID(strAssetId));
Asset asset = checkAssetId(assetId, Operation.READ);
return tbAssetService.unassignAssetFromEdge(getTenantId(), asset, edge, getCurrentUser());
}
@ApiOperation(value = "Get assets assigned to edge (getEdgeAssets)",
notes = "Returns a page of assets assigned to edge. " +
PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<Asset> getEdgeAssets(
@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION)
@PathVariable(EDGE_ID) String strEdgeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = "Timestamp. Assets with creation time before it won't be queried")
@RequestParam(required = false) Long startTime,
@ApiParam(value = "Timestamp. Assets with creation time after it won't be queried")
@RequestParam(required = false) Long endTime) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
checkEdgeId(edgeId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
PageData<Asset> nonFilteredResult;
if (type != null && type.trim().length() > 0) {
nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink);
} else {
nonFilteredResult = assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink);
}
List<Asset> filteredAssets = nonFilteredResult.getData().stream().filter(asset -> {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, Operation.READ, asset.getId(), asset);
return true;
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
PageData<Asset> filteredResult = new PageData<>(filteredAssets,
nonFilteredResult.getTotalPages(),
nonFilteredResult.getTotalElements(),
nonFilteredResult.hasNext());
return checkNotNull(filteredResult);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Import the bulk of assets (processAssetsBulkImport)",
notes = "There's an ability to import the bulk of assets using the only .csv file.", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@PostMapping("/asset/bulk_import")
public BulkImportResult<Asset> processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception {
SecurityUser user = getCurrentUser();
return assetBulkImportService.processBulkImport(request, user);
}
}
/**
* Copyright © 2016-2022 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 io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.asset.AssetProfileInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class AssetProfileController extends BaseController {
private final TbAssetProfileService tbAssetProfileService;
@ApiOperation(value = "Get Asset Profile (getAssetProfileById)",
notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " +
"The server checks that the asset profile is owned by the same tenant. " + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfile/{assetProfileId}", method = RequestMethod.GET)
@ResponseBody
public AssetProfile getAssetProfileById(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
try {
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
return checkAssetProfileId(assetProfileId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Asset Profile Info (getAssetProfileInfoById)",
notes = "Fetch the Asset Profile Info object based on the provided Asset Profile Id. "
+ ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assetProfileInfo/{assetProfileId}", method = RequestMethod.GET)
@ResponseBody
public AssetProfileInfo getAssetProfileInfoById(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
try {
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
return new AssetProfileInfo(checkAssetProfileId(assetProfileId, Operation.READ));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Default Asset Profile (getDefaultAssetProfileInfo)",
notes = "Fetch the Default Asset Profile Info object. " +
ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assetProfileInfo/default", method = RequestMethod.GET)
@ResponseBody
public AssetProfileInfo getDefaultAssetProfileInfo() throws ThingsboardException {
try {
return checkNotNull(assetProfileService.findDefaultAssetProfileInfo(getTenantId()));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Create Or Update Asset Profile (saveAssetProfile)",
notes = "Create or update the Asset Profile. When creating asset profile, platform generates asset profile id as " + UUID_WIKI_LINK +
"The newly created asset profile id will be present in the response. " +
"Specify existing asset profile id to update the asset profile. " +
"Referencing non-existing asset profile Id will cause 'Not Found' error. " + NEW_LINE +
"Asset profile name is unique in the scope of tenant. Only one 'default' asset profile may exist in scope of tenant. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Asset Profile entity. " +
TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json",
consumes = "application/json")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfile", method = RequestMethod.POST)
@ResponseBody
public AssetProfile saveAssetProfile(
@ApiParam(value = "A JSON value representing the asset profile.")
@RequestBody AssetProfile assetProfile) throws Exception {
assetProfile.setTenantId(getTenantId());
checkEntity(assetProfile.getId(), assetProfile, Resource.ASSET_PROFILE);
return tbAssetProfileService.save(assetProfile, getCurrentUser());
}
@ApiOperation(value = "Delete asset profile (deleteAssetProfile)",
notes = "Deletes the asset profile. Referencing non-existing asset profile Id will cause an error. " +
"Can't delete the asset profile if it is referenced by existing assets." + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfile/{assetProfileId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteAssetProfile(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
AssetProfile assetProfile = checkAssetProfileId(assetProfileId, Operation.DELETE);
tbAssetProfileService.delete(assetProfile, getCurrentUser());
}
@ApiOperation(value = "Make Asset Profile Default (setDefaultAssetProfile)",
notes = "Marks asset profile as default within a tenant scope." + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfile/{assetProfileId}/default", method = RequestMethod.POST)
@ResponseBody
public AssetProfile setDefaultAssetProfile(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
AssetProfile assetProfile = checkAssetProfileId(assetProfileId, Operation.WRITE);
AssetProfile previousDefaultAssetProfile = assetProfileService.findDefaultAssetProfile(getTenantId());
return tbAssetProfileService.setDefaultAssetProfile(assetProfile, previousDefaultAssetProfile, getCurrentUser());
}
@ApiOperation(value = "Get Asset Profiles (getAssetProfiles)",
notes = "Returns a page of asset profile objects owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AssetProfile> getAssetProfiles(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(assetProfileService.findAssetProfiles(getTenantId(), pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Asset Profile infos (getAssetProfileInfos)",
notes = "Returns a page of asset profile info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assetProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AssetProfileInfo> getAssetProfileInfos(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(assetProfileService.findAssetProfileInfos(getTenantId(), pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
}
/**
* Copyright © 2016-2022 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 io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.AUDIT_LOG_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION;
@RestController
@TbCoreComponent
@RequestMapping("/api")
public class AuditLogController extends BaseController {
private static final String AUDIT_LOG_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the AuditLog class field: 'createdTime'.";
private static final String AUDIT_LOG_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the AuditLog class field: 'createdTime'.";
private static final String AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION = "A String value representing comma-separated list of action types. " +
"This parameter is optional, but it can be used to filter results to fetch only audit logs of specific action types. " +
"For example, 'LOGIN', 'LOGOUT'. See the 'Model' tab of the Response Class for more details.";
private static final String AUDIT_LOG_SORT_PROPERTY_DESCRIPTION = "Property of audit log to sort by. " +
"See the 'Model' tab of the Response Class for more details. " +
"Note: entityType sort property is not defined in the AuditLog class, however, it can be used to sort audit logs by types of entities that were logged.";
@ApiOperation(value = "Get audit logs by customer id (getAuditLogsByCustomerId)",
notes = "Returns a page of audit logs related to the targeted customer entities (devices, assets, etc.), " +
"and users actions (login, logout, etc.) that belong to this customer. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AuditLog> getAuditLogsByCustomerId(
@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@RequestParam int page,
@ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime,
@ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION)
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
try {
checkParameter("CustomerId", strCustomerId);
TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get audit logs by user id (getAuditLogsByUserId)",
notes = "Returns a page of audit logs related to the actions of targeted user. " +
"For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AuditLog> getAuditLogsByUserId(
@ApiParam(value = USER_ID_PARAM_DESCRIPTION)
@PathVariable("userId") String strUserId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@RequestParam int page,
@ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime,
@ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION)
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
try {
checkParameter("UserId", strUserId);
TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get audit logs by entity id (getAuditLogsByEntityId)",
notes = "Returns a page of audit logs related to the actions on the targeted entity. " +
"Basically, this API call is used to get the full lifecycle of some specific entity. " +
"For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AuditLog> getAuditLogsByEntityId(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable("entityType") String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String strEntityId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime,
@ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION)
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
try {
checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType);
TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get all audit logs (getAuditLogs)",
notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AuditLog> getAuditLogs(
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@RequestParam int page,
@ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime,
@ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION)
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
private List<ActionType> parseActionTypesStr(String actionTypesStr) {
List<ActionType> result = null;
if (StringUtils.isNoneBlank(actionTypesStr)) {
String[] tmp = actionTypesStr.split(",");
result = Arrays.stream(tmp).map(at -> ActionType.valueOf(at.toUpperCase())).collect(Collectors.toList());
}
return result;
}
}
/**
* Copyright © 2016-2022 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.node.ObjectNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
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.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
import org.thingsboard.server.service.security.model.ResetPasswordRequest;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@Slf4j
@RequiredArgsConstructor
public class AuthController extends BaseController {
private final BCryptPasswordEncoder passwordEncoder;
private final JwtTokenFactory tokenFactory;
private final MailService mailService;
private final SystemSecurityService systemSecurityService;
private final AuditLogService auditLogService;
private final ApplicationEventPublisher eventPublisher;
@ApiOperation(value = "Get current User (getUser)",
notes = "Get the information about the User which credentials are used to perform this REST API call.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET)
public @ResponseBody
User getUser() throws ThingsboardException {
try {
SecurityUser securityUser = getCurrentUser();
return userService.findUserById(securityUser.getTenantId(), securityUser.getId());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Logout (logout)",
notes = "Special API call to record the 'logout' of the user to the Audit Logs. Since platform uses [JWT](https://jwt.io/), the actual logout is the procedure of clearing the [JWT](https://jwt.io/) token on the client side. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/logout", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void logout(HttpServletRequest request) throws ThingsboardException {
logLogoutAction(request);
}
@ApiOperation(value = "Change password for current User (changePassword)",
notes = "Change the password for the User which credentials are used to perform this REST API call. Be aware that previously generated [JWT](https://jwt.io/) tokens will be still valid until they expire.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public ObjectNode changePassword(
@ApiParam(value = "Change Password Request")
@RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException {
try {
String currentPassword = changePasswordRequest.getCurrentPassword();
String newPassword = changePasswordRequest.getNewPassword();
SecurityUser securityUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, securityUser.getId());
if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
throw new ThingsboardException("Current password doesn't match!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
systemSecurityService.validatePassword(securityUser.getTenantId(), newPassword, userCredentials);
if (passwordEncoder.matches(newPassword, userCredentials.getPassword())) {
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
userCredentials.setPassword(passwordEncoder.encode(newPassword));
userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials);
sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
ObjectNode response = JacksonUtil.newObjectNode();
response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken());
response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken());
return response;
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)",
notes = "API call to get the password policy for the password validation form(s).")
@RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET)
@ResponseBody
public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException {
try {
SecuritySettings securitySettings =
checkNotNull(systemSecurityService.getSecuritySettings(TenantId.SYS_TENANT_ID));
return securitySettings.getPasswordPolicy();
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Check Activate User Token (checkActivateToken)",
notes = "Checks the activation token and forwards user to 'Create Password' page. " +
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'.")
@RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET)
public ResponseEntity<String> checkActivateToken(
@ApiParam(value = "The activate token string.")
@RequestParam(value = "activateToken") String activateToken) {
HttpHeaders headers = new HttpHeaders();
HttpStatus responseStatus;
UserCredentials userCredentials = userService.findUserCredentialsByActivateToken(TenantId.SYS_TENANT_ID, activateToken);
if (userCredentials != null) {
String createURI = "/login/createPassword";
try {
URI location = new URI(createURI + "?activateToken=" + activateToken);
headers.setLocation(location);
responseStatus = HttpStatus.SEE_OTHER;
} catch (URISyntaxException e) {
log.error("Unable to create URI with address [{}]", createURI);
responseStatus = HttpStatus.BAD_REQUEST;
}
} else {
responseStatus = HttpStatus.CONFLICT;
}
return new ResponseEntity<>(headers, responseStatus);
}
@ApiOperation(value = "Request reset password email (requestResetPasswordByEmail)",
notes = "Request to send the reset password email if the user with specified email address is present in the database. " +
"Always return '200 OK' status for security purposes.")
@RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void requestResetPasswordByEmail(
@ApiParam(value = "The JSON object representing the reset password email request.")
@RequestBody ResetPasswordEmailRequest resetPasswordByEmailRequest,
HttpServletRequest request) throws ThingsboardException {
try {
String email = resetPasswordByEmailRequest.getEmail();
UserCredentials userCredentials = userService.requestPasswordReset(TenantId.SYS_TENANT_ID, email);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
userCredentials.getResetToken());
mailService.sendResetPasswordEmailAsync(resetUrl, email);
} catch (Exception e) {
log.warn("Error occurred: {}", e.getMessage());
}
}
@ApiOperation(value = "Check password reset token (checkResetToken)",
notes = "Checks the password reset token and forwards user to 'Reset Password' page. " +
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'.")
@RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET)
public ResponseEntity<String> checkResetToken(
@ApiParam(value = "The reset token string.")
@RequestParam(value = "resetToken") String resetToken) {
HttpHeaders headers = new HttpHeaders();
HttpStatus responseStatus;
String resetURI = "/login/resetPassword";
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
try {
URI location = new URI(resetURI + "?resetToken=" + resetToken);
headers.setLocation(location);
responseStatus = HttpStatus.SEE_OTHER;
} catch (URISyntaxException e) {
log.error("Unable to create URI with address [{}]", resetURI);
responseStatus = HttpStatus.BAD_REQUEST;
}
} else {
responseStatus = HttpStatus.CONFLICT;
}
return new ResponseEntity<>(headers, responseStatus);
}
@ApiOperation(value = "Activate User",
notes = "Checks the activation token and updates corresponding user password in the database. " +
"Now the user may start using his password to login. " +
"The response already contains the [JWT](https://jwt.io) activation and refresh tokens, " +
"to simplify the user activation flow and avoid asking user to input password again after activation. " +
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '404 Bad Request'.")
@RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtPair activateUser(
@ApiParam(value = "Activate user request.")
@RequestBody ActivateUserRequest activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException {
try {
String activateToken = activateRequest.getActivateToken();
String password = activateRequest.getPassword();
systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, null);
String encodedPassword = passwordEncoder.encode(password);
UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
userService.setUserCredentialsEnabled(user.getTenantId(), user.getId(), true);
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
if (sendActivationMail) {
try {
mailService.sendAccountActivatedEmail(loginUrl, email);
} catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
}
}
sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED);
return tokenFactory.createTokenPair(securityUser);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Reset password (resetPassword)",
notes = "Checks the password reset token and updates the password. " +
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '404 Bad Request'.")
@RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtPair resetPassword(
@ApiParam(value = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {
try {
String resetToken = resetPasswordRequest.getResetToken();
String password = resetPasswordRequest.getPassword();
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, userCredentials);
if (passwordEncoder.matches(password, userCredentials.getPassword())) {
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
String encodedPassword = passwordEncoder.encode(password);
userCredentials.setPassword(encodedPassword);
userCredentials.setResetToken(null);
userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials);
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email);
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
return tokenFactory.createTokenPair(securityUser);
} else {
throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
} catch (Exception e) {
throw handleException(e);
}
}
private void logLogoutAction(HttpServletRequest request) throws ThingsboardException {
try {
var user = getCurrentUser();
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGOUT, null);
eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId()));
} catch (Exception e) {
throw handleException(e);
}
}
}
/**
* Copyright © 2016-2022 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.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.UUID;
public class AutoCommitController extends BaseController {
@Autowired
private EntitiesVersionControlService vcService;
protected ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception {
if (vcService != null) {
return vcService.autoCommit(user, entityId);
} else {
// We do not support auto-commit for rule engine
return Futures.immediateFailedFuture(new RuntimeException("Operation not supported!"));
}
}
}
/**
* Copyright © 2016-2022 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.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.ClaimDevicesService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID;
import static org.thingsboard.server.controller.UserController.YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Slf4j
@TbCoreComponent
public abstract class BaseController {
/*Swagger UI description*/
private static final ObjectMapper json = new ObjectMapper();
@Autowired
private ThingsboardErrorResponseHandler errorResponseHandler;
@Autowired
protected AccessControlService accessControlService;
@Autowired
protected TenantService tenantService;
@Autowired
protected TenantProfileService tenantProfileService;
@Autowired
protected CustomerService customerService;
@Autowired
protected UserService userService;
@Autowired
protected DeviceService deviceService;
@Autowired
protected DeviceProfileService deviceProfileService;
@Autowired
protected AssetService assetService;
@Autowired
protected AssetProfileService assetProfileService;
@Autowired
protected AlarmSubscriptionService alarmService;
@Autowired
protected DeviceCredentialsService deviceCredentialsService;
@Autowired
protected WidgetsBundleService widgetsBundleService;
@Autowired
protected WidgetTypeService widgetTypeService;
@Autowired
protected DashboardService dashboardService;
@Autowired
protected OAuth2Service oAuth2Service;
@Autowired
protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService;
@Autowired
protected ComponentDiscoveryService componentDescriptorService;
@Autowired
protected RuleChainService ruleChainService;
@Autowired
protected TbClusterService tbClusterService;
@Autowired
protected RelationService relationService;
@Autowired
protected AuditLogService auditLogService;
@Autowired
protected DeviceStateService deviceStateService;
@Autowired
protected EntityViewService entityViewService;
@Autowired
protected TelemetrySubscriptionService tsSubService;
@Autowired
protected AttributesService attributesService;
@Autowired
protected ClaimDevicesService claimDevicesService;
@Autowired
protected PartitionService partitionService;
@Autowired
protected TbResourceService resourceService;
@Autowired
protected OtaPackageService otaPackageService;
@Autowired
protected OtaPackageStateService otaPackageStateService;
@Autowired
protected RpcService rpcService;
@Autowired
protected TbQueueProducerProvider producerProvider;
@Autowired
protected TbTenantProfileCache tenantProfileCache;
@Autowired
protected TbDeviceProfileCache deviceProfileCache;
@Autowired
protected TbAssetProfileCache assetProfileCache;
@Autowired(required = false)
protected EdgeService edgeService;
@Autowired(required = false)
protected EdgeRpcService edgeRpcService;
@Autowired
protected TbNotificationEntityService notificationEntityService;
@Autowired
protected QueueService queueService;
@Autowired
protected EntitiesVersionControlService vcService;
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@Value("${edges.enabled}")
@Getter
protected boolean edgesEnabled;
@ExceptionHandler(Exception.class)
public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e);
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception
&& StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) {
e = (Exception) thingsboardException.getCause();
} else {
e = thingsboardException;
}
errorResponseHandler.handle(e, response);
}
@ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response);
}
/**
* @deprecated Exceptions that are not of {@link ThingsboardException} type
* are now caught and mapped to {@link ThingsboardException} by
* {@link ExceptionHandler} {@link BaseController#handleControllerException(Exception, HttpServletResponse)}
* which basically acts like the following boilerplate:
* {@code
* try {
* someExceptionThrowingMethod();
* } catch (Exception e) {
* throw handleException(e);
* }
* }
* */
@Deprecated
ThingsboardException handleException(Exception exception) {
return handleException(exception, true);
}
private ThingsboardException handleException(Exception exception, boolean logException) {
if (logException && logControllerErrorStackTrace) {
log.error("Error [{}]", exception.getMessage(), exception);
}
String cause = "";
if (exception.getCause() != null) {
cause = exception.getCause().getClass().getCanonicalName();
}
if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause.contains("IncorrectParameterException")) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
} else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
}
}
/**
* Handles validation error for controller method arguments annotated with @{@link javax.validation.Valid}
* */
@ExceptionHandler(MethodArgumentNotValidException.class)
public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) {
String errorMessage = "Validation error: " + e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
handleThingsboardException(thingsboardException, response);
}
<T> T checkNotNull(T reference) throws ThingsboardException {
return checkNotNull(reference, "Requested item wasn't found!");
}
<T> T checkNotNull(T reference, String notFoundMessage) throws ThingsboardException {
if (reference == null) {
throw new ThingsboardException(notFoundMessage, ThingsboardErrorCode.ITEM_NOT_FOUND);
}
return reference;
}
<T> T checkNotNull(Optional<T> reference) throws ThingsboardException {
return checkNotNull(reference, "Requested item wasn't found!");
}
<T> T checkNotNull(Optional<T> reference, String notFoundMessage) throws ThingsboardException {
if (reference.isPresent()) {
return reference.get();
} else {
throw new ThingsboardException(notFoundMessage, ThingsboardErrorCode.ITEM_NOT_FOUND);
}
}
void checkParameter(String name, String param) throws ThingsboardException {
if (StringUtils.isEmpty(param)) {
throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
void checkArrayParameter(String name, String[] params) throws ThingsboardException {
if (params == null || params.length == 0) {
throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
for (String param : params) {
checkParameter(name, param);
}
}
}
UUID toUUID(String id) throws ThingsboardException {
try {
return UUID.fromString(id);
} catch (IllegalArgumentException e) {
throw handleException(e, false);
}
}
PageLink createPageLink(int pageSize, int page, String textSearch, String sortProperty, String sortOrder) throws ThingsboardException {
if (StringUtils.isNotEmpty(sortProperty)) {
if (!Validator.isValidProperty(sortProperty)) {
throw new IllegalArgumentException("Invalid sort property");
}
SortOrder.Direction direction = SortOrder.Direction.ASC;
if (StringUtils.isNotEmpty(sortOrder)) {
try {
direction = SortOrder.Direction.valueOf(sortOrder.toUpperCase());
} catch (IllegalArgumentException e) {
throw new ThingsboardException("Unsupported sort order '" + sortOrder + "'! Only 'ASC' or 'DESC' types are allowed.", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
SortOrder sort = new SortOrder(sortProperty, direction);
return new PageLink(pageSize, page, textSearch, sort);
} else {
return new PageLink(pageSize, page, textSearch);
}
}
TimePageLink createTimePageLink(int pageSize, int page, String textSearch,
String sortProperty, String sortOrder, Long startTime, Long endTime) throws ThingsboardException {
PageLink pageLink = this.createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return new TimePageLink(pageLink, startTime, endTime);
}
protected SecurityUser getCurrentUser() throws ThingsboardException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {
return (SecurityUser) authentication.getPrincipal();
} else {
throw new ThingsboardException("You aren't authorized to perform this operation!", ThingsboardErrorCode.AUTHENTICATION);
}
}
Tenant checkTenantId(TenantId tenantId, Operation operation) throws ThingsboardException {
try {
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Tenant tenant = tenantService.findTenantById(tenantId);
checkNotNull(tenant, "Tenant with id [" + tenantId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant);
return tenant;
} catch (Exception e) {
throw handleException(e, false);
}
}
TenantInfo checkTenantInfoId(TenantId tenantId, Operation operation) throws ThingsboardException {
try {
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
TenantInfo tenant = tenantService.findTenantInfoById(tenantId);
checkNotNull(tenant, "Tenant with id [" + tenantId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant);
return tenant;
} catch (Exception e) {
throw handleException(e, false);
}
}
TenantProfile checkTenantProfileId(TenantProfileId tenantProfileId, Operation operation) throws ThingsboardException {
try {
validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId);
TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(getTenantId(), tenantProfileId);
checkNotNull(tenantProfile, "Tenant profile with id [" + tenantProfileId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, operation);
return tenantProfile;
} catch (Exception e) {
throw handleException(e, false);
}
}
protected TenantId getTenantId() throws ThingsboardException {
return getCurrentUser().getTenantId();
}
Customer checkCustomerId(CustomerId customerId, Operation operation) throws ThingsboardException {
try {
validateId(customerId, "Incorrect customerId " + customerId);
Customer customer = customerService.findCustomerById(getTenantId(), customerId);
checkNotNull(customer, "Customer with id [" + customerId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.CUSTOMER, operation, customerId, customer);
return customer;
} catch (Exception e) {
throw handleException(e, false);
}
}
User checkUserId(UserId userId, Operation operation) throws ThingsboardException {
try {
validateId(userId, "Incorrect userId " + userId);
User user = userService.findUserById(getCurrentUser().getTenantId(), userId);
checkNotNull(user, "User with id [" + userId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.USER, operation, userId, user);
return user;
} catch (Exception e) {
throw handleException(e, false);
}
}
protected <I extends EntityId, T extends HasTenantId> void checkEntity(I entityId, T entity, Resource resource) throws ThingsboardException {
if (entityId == null) {
accessControlService
.checkPermission(getCurrentUser(), resource, Operation.CREATE, null, entity);
} else {
checkEntityId(entityId, Operation.WRITE);
}
}
protected void checkEntityId(EntityId entityId, Operation operation) throws ThingsboardException {
try {
if (entityId == null) {
throw new ThingsboardException("Parameter entityId can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
validateId(entityId.getId(), "Incorrect entityId " + entityId);
switch (entityId.getEntityType()) {
case ALARM:
checkAlarmId(new AlarmId(entityId.getId()), operation);
return;
case DEVICE:
checkDeviceId(new DeviceId(entityId.getId()), operation);
return;
case DEVICE_PROFILE:
checkDeviceProfileId(new DeviceProfileId(entityId.getId()), operation);
return;
case CUSTOMER:
checkCustomerId(new CustomerId(entityId.getId()), operation);
return;
case TENANT:
checkTenantId(TenantId.fromUUID(entityId.getId()), operation);
return;
case TENANT_PROFILE:
checkTenantProfileId(new TenantProfileId(entityId.getId()), operation);
return;
case RULE_CHAIN:
checkRuleChain(new RuleChainId(entityId.getId()), operation);
return;
case RULE_NODE:
checkRuleNode(new RuleNodeId(entityId.getId()), operation);
return;
case ASSET:
checkAssetId(new AssetId(entityId.getId()), operation);
return;
case ASSET_PROFILE:
checkAssetProfileId(new AssetProfileId(entityId.getId()), operation);
return;
case DASHBOARD:
checkDashboardId(new DashboardId(entityId.getId()), operation);
return;
case USER:
checkUserId(new UserId(entityId.getId()), operation);
return;
case ENTITY_VIEW:
checkEntityViewId(new EntityViewId(entityId.getId()), operation);
return;
case EDGE:
checkEdgeId(new EdgeId(entityId.getId()), operation);
return;
case WIDGETS_BUNDLE:
checkWidgetsBundleId(new WidgetsBundleId(entityId.getId()), operation);
return;
case WIDGET_TYPE:
checkWidgetTypeId(new WidgetTypeId(entityId.getId()), operation);
return;
case TB_RESOURCE:
checkResourceId(new TbResourceId(entityId.getId()), operation);
return;
case OTA_PACKAGE:
checkOtaPackageId(new OtaPackageId(entityId.getId()), operation);
return;
case QUEUE:
checkQueueId(new QueueId(entityId.getId()), operation);
return;
default:
throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType());
}
} catch (Exception e) {
throw handleException(e, false);
}
}
Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException {
try {
validateId(deviceId, "Incorrect deviceId " + deviceId);
Device device = deviceService.findDeviceById(getCurrentUser().getTenantId(), deviceId);
checkNotNull(device, "Device with id [" + deviceId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device);
return device;
} catch (Exception e) {
throw handleException(e, false);
}
}
DeviceInfo checkDeviceInfoId(DeviceId deviceId, Operation operation) throws ThingsboardException {
try {
validateId(deviceId, "Incorrect deviceId " + deviceId);
DeviceInfo device = deviceService.findDeviceInfoById(getCurrentUser().getTenantId(), deviceId);
checkNotNull(device, "Device with id [" + deviceId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device);
return device;
} catch (Exception e) {
throw handleException(e, false);
}
}
DeviceProfile checkDeviceProfileId(DeviceProfileId deviceProfileId, Operation operation) throws ThingsboardException {
try {
validateId(deviceProfileId, "Incorrect deviceProfileId " + deviceProfileId);
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(getCurrentUser().getTenantId(), deviceProfileId);
checkNotNull(deviceProfile, "Device profile with id [" + deviceProfileId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE_PROFILE, operation, deviceProfileId, deviceProfile);
return deviceProfile;
} catch (Exception e) {
throw handleException(e, false);
}
}
protected EntityView checkEntityViewId(EntityViewId entityViewId, Operation operation) throws ThingsboardException {
try {
validateId(entityViewId, "Incorrect entityViewId " + entityViewId);
EntityView entityView = entityViewService.findEntityViewById(getCurrentUser().getTenantId(), entityViewId);
checkNotNull(entityView, "Entity view with id [" + entityViewId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView);
return entityView;
} catch (Exception e) {
throw handleException(e, false);
}
}
EntityViewInfo checkEntityViewInfoId(EntityViewId entityViewId, Operation operation) throws ThingsboardException {
try {
validateId(entityViewId, "Incorrect entityViewId " + entityViewId);
EntityViewInfo entityView = entityViewService.findEntityViewInfoById(getCurrentUser().getTenantId(), entityViewId);
checkNotNull(entityView, "Entity view with id [" + entityViewId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView);
return entityView;
} catch (Exception e) {
throw handleException(e, false);
}
}
Asset checkAssetId(AssetId assetId, Operation operation) throws ThingsboardException {
try {
validateId(assetId, "Incorrect assetId " + assetId);
Asset asset = assetService.findAssetById(getCurrentUser().getTenantId(), assetId);
checkNotNull(asset, "Asset with id [" + assetId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset);
return asset;
} catch (Exception e) {
throw handleException(e, false);
}
}
AssetInfo checkAssetInfoId(AssetId assetId, Operation operation) throws ThingsboardException {
try {
validateId(assetId, "Incorrect assetId " + assetId);
AssetInfo asset = assetService.findAssetInfoById(getCurrentUser().getTenantId(), assetId);
checkNotNull(asset, "Asset with id [" + assetId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset);
return asset;
} catch (Exception e) {
throw handleException(e, false);
}
}
AssetProfile checkAssetProfileId(AssetProfileId assetProfileId, Operation operation) throws ThingsboardException {
try {
validateId(assetProfileId, "Incorrect assetProfileId " + assetProfileId);
AssetProfile assetProfile = assetProfileService.findAssetProfileById(getCurrentUser().getTenantId(), assetProfileId);
checkNotNull(assetProfile, "Asset profile with id [" + assetProfileId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET_PROFILE, operation, assetProfileId, assetProfile);
return assetProfile;
} catch (Exception e) {
throw handleException(e, false);
}
}
Alarm checkAlarmId(AlarmId alarmId, Operation operation) throws ThingsboardException {
try {
validateId(alarmId, "Incorrect alarmId " + alarmId);
Alarm alarm = alarmService.findAlarmByIdAsync(getCurrentUser().getTenantId(), alarmId).get();
checkNotNull(alarm, "Alarm with id [" + alarmId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarm);
return alarm;
} catch (Exception e) {
throw handleException(e, false);
}
}
AlarmInfo checkAlarmInfoId(AlarmId alarmId, Operation operation) throws ThingsboardException {
try {
validateId(alarmId, "Incorrect alarmId " + alarmId);
AlarmInfo alarmInfo = alarmService.findAlarmInfoByIdAsync(getCurrentUser().getTenantId(), alarmId).get();
checkNotNull(alarmInfo, "Alarm with id [" + alarmId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarmInfo);
return alarmInfo;
} catch (Exception e) {
throw handleException(e, false);
}
}
WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, Operation operation) throws ThingsboardException {
try {
validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(getCurrentUser().getTenantId(), widgetsBundleId);
checkNotNull(widgetsBundle, "Widgets bundle with id [" + widgetsBundleId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.WIDGETS_BUNDLE, operation, widgetsBundleId, widgetsBundle);
return widgetsBundle;
} catch (Exception e) {
throw handleException(e, false);
}
}
WidgetTypeDetails checkWidgetTypeId(WidgetTypeId widgetTypeId, Operation operation) throws ThingsboardException {
try {
validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(getCurrentUser().getTenantId(), widgetTypeId);
checkNotNull(widgetTypeDetails, "Widget type with id [" + widgetTypeId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, operation, widgetTypeId, widgetTypeDetails);
return widgetTypeDetails;
} catch (Exception e) {
throw handleException(e, false);
}
}
Dashboard checkDashboardId(DashboardId dashboardId, Operation operation) throws ThingsboardException {
try {
validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
Dashboard dashboard = dashboardService.findDashboardById(getCurrentUser().getTenantId(), dashboardId);
checkNotNull(dashboard, "Dashboard with id [" + dashboardId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, operation, dashboardId, dashboard);
return dashboard;
} catch (Exception e) {
throw handleException(e, false);
}
}
Edge checkEdgeId(EdgeId edgeId, Operation operation) throws ThingsboardException {
try {
validateId(edgeId, "Incorrect edgeId " + edgeId);
Edge edge = edgeService.findEdgeById(getTenantId(), edgeId);
checkNotNull(edge, "Edge with id [" + edgeId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge);
return edge;
} catch (Exception e) {
throw handleException(e, false);
}
}
EdgeInfo checkEdgeInfoId(EdgeId edgeId, Operation operation) throws ThingsboardException {
try {
validateId(edgeId, "Incorrect edgeId " + edgeId);
EdgeInfo edge = edgeService.findEdgeInfoById(getCurrentUser().getTenantId(), edgeId);
checkNotNull(edge, "Edge with id [" + edgeId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge);
return edge;
} catch (Exception e) {
throw handleException(e, false);
}
}
DashboardInfo checkDashboardInfoId(DashboardId dashboardId, Operation operation) throws ThingsboardException {
try {
validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(getCurrentUser().getTenantId(), dashboardId);
checkNotNull(dashboardInfo, "Dashboard with id [" + dashboardId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, operation, dashboardId, dashboardInfo);
return dashboardInfo;
} catch (Exception e) {
throw handleException(e, false);
}
}
ComponentDescriptor checkComponentDescriptorByClazz(String clazz) throws ThingsboardException {
try {
log.debug("[{}] Lookup component descriptor", clazz);
return checkNotNull(componentDescriptorService.getComponent(clazz));
} catch (Exception e) {
throw handleException(e, false);
}
}
List<ComponentDescriptor> checkComponentDescriptorsByType(ComponentType type, RuleChainType ruleChainType) throws ThingsboardException {
try {
log.debug("[{}] Lookup component descriptors", type);
return componentDescriptorService.getComponents(type, ruleChainType);
} catch (Exception e) {
throw handleException(e, false);
}
}
List<ComponentDescriptor> checkComponentDescriptorsByTypes(Set<ComponentType> types, RuleChainType ruleChainType) throws ThingsboardException {
try {
log.debug("[{}] Lookup component descriptors", types);
return componentDescriptorService.getComponents(types, ruleChainType);
} catch (Exception e) {
throw handleException(e, false);
}
}
protected RuleChain checkRuleChain(RuleChainId ruleChainId, Operation operation) throws ThingsboardException {
validateId(ruleChainId, "Incorrect ruleChainId " + ruleChainId);
RuleChain ruleChain = ruleChainService.findRuleChainById(getCurrentUser().getTenantId(), ruleChainId);
checkNotNull(ruleChain, "Rule chain with id [" + ruleChainId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.RULE_CHAIN, operation, ruleChainId, ruleChain);
return ruleChain;
}
protected RuleNode checkRuleNode(RuleNodeId ruleNodeId, Operation operation) throws ThingsboardException {
validateId(ruleNodeId, "Incorrect ruleNodeId " + ruleNodeId);
RuleNode ruleNode = ruleChainService.findRuleNodeById(getTenantId(), ruleNodeId);
checkNotNull(ruleNode, "Rule node with id [" + ruleNodeId + "] is not found");
checkRuleChain(ruleNode.getRuleChainId(), operation);
return ruleNode;
}
TbResource checkResourceId(TbResourceId resourceId, Operation operation) throws ThingsboardException {
try {
validateId(resourceId, "Incorrect resourceId " + resourceId);
TbResource resource = resourceService.findResourceById(getCurrentUser().getTenantId(), resourceId);
checkNotNull(resource, "Resource with id [" + resourceId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resource);
return resource;
} catch (Exception e) {
throw handleException(e, false);
}
}
TbResourceInfo checkResourceInfoId(TbResourceId resourceId, Operation operation) throws ThingsboardException {
try {
validateId(resourceId, "Incorrect resourceId " + resourceId);
TbResourceInfo resourceInfo = resourceService.findResourceInfoById(getCurrentUser().getTenantId(), resourceId);
checkNotNull(resourceInfo, "Resource with id [" + resourceId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resourceInfo);
return resourceInfo;
} catch (Exception e) {
throw handleException(e, false);
}
}
OtaPackage checkOtaPackageId(OtaPackageId otaPackageId, Operation operation) throws ThingsboardException {
try {
validateId(otaPackageId, "Incorrect otaPackageId " + otaPackageId);
OtaPackage otaPackage = otaPackageService.findOtaPackageById(getCurrentUser().getTenantId(), otaPackageId);
checkNotNull(otaPackage, "OTA package with id [" + otaPackageId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.OTA_PACKAGE, operation, otaPackageId, otaPackage);
return otaPackage;
} catch (Exception e) {
throw handleException(e, false);
}
}
OtaPackageInfo checkOtaPackageInfoId(OtaPackageId otaPackageId, Operation operation) throws ThingsboardException {
try {
validateId(otaPackageId, "Incorrect otaPackageId " + otaPackageId);
OtaPackageInfo otaPackageIn = otaPackageService.findOtaPackageInfoById(getCurrentUser().getTenantId(), otaPackageId);
checkNotNull(otaPackageIn, "OTA package with id [" + otaPackageId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.OTA_PACKAGE, operation, otaPackageId, otaPackageIn);
return otaPackageIn;
} catch (Exception e) {
throw handleException(e, false);
}
}
Rpc checkRpcId(RpcId rpcId, Operation operation) throws ThingsboardException {
try {
validateId(rpcId, "Incorrect rpcId " + rpcId);
Rpc rpc = rpcService.findById(getCurrentUser().getTenantId(), rpcId);
checkNotNull(rpc, "RPC with id [" + rpcId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.RPC, operation, rpcId, rpc);
return rpc;
} catch (Exception e) {
throw handleException(e, false);
}
}
protected Queue checkQueueId(QueueId queueId, Operation operation) throws ThingsboardException {
validateId(queueId, "Incorrect queueId " + queueId);
Queue queue = queueService.findQueueById(getCurrentUser().getTenantId(), queueId);
checkNotNull(queue);
accessControlService.checkPermission(getCurrentUser(), Resource.QUEUE, operation, queueId, queue);
TenantId tenantId = getTenantId();
if (queue.getTenantId().isNullUid() && !tenantId.isNullUid()) {
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
if (tenantProfile.isIsolatedTbRuleEngine()) {
throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
ThingsboardErrorCode.PERMISSION_DENIED);
}
}
return queue;
}
protected <I extends EntityId> I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}
public static Exception toException(Throwable error) {
return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null;
}
protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) {
sendNotificationMsgToEdge(tenantId, null, entityId, null, null, action);
}
protected void sendEntityAssignToEdgeNotificationMsg(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType action) {
sendNotificationMsgToEdge(tenantId, edgeId, entityId, null, null, action);
}
private void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action) {
tbClusterService.sendNotificationMsgToEdge(tenantId, edgeId, entityId, body, type, action);
}
protected void processDashboardIdFromAdditionalInfo(ObjectNode additionalInfo, String requiredFields) throws ThingsboardException {
String dashboardId = additionalInfo.has(requiredFields) ? additionalInfo.get(requiredFields).asText() : null;
if (dashboardId != null && !dashboardId.equals("null")) {
if (dashboardService.findDashboardById(getTenantId(), new DashboardId(UUID.fromString(dashboardId))) == null) {
additionalInfo.remove(requiredFields);
}
}
}
protected MediaType parseMediaType(String contentType) {
try {
return MediaType.parseMediaType(contentType);
} catch (Exception e) {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
protected <T> DeferredResult<T> wrapFuture(ListenableFuture<T> future) {
final DeferredResult<T> deferredResult = new DeferredResult<>();
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(T result) {
deferredResult.setResult(result);
}
@Override
public void onFailure(Throwable t) {
deferredResult.setErrorResult(t);
}
}, MoreExecutors.directExecutor());
return deferredResult;
}
}
/**
* Copyright © 2016-2022 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 io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequestMapping("/api")
public class ComponentDescriptorController extends BaseController {
private static final String COMPONENT_DESCRIPTOR_DEFINITION = "Each Component Descriptor represents configuration of specific rule node (e.g. 'Save Timeseries' or 'Send Email'.). " +
"The Component Descriptors are used by the rule chain Web UI to build the configuration forms for the rule nodes. " +
"The Component Descriptors are discovered at runtime by scanning the class path and searching for @RuleNode annotation. " +
"Once discovered, the up to date list of descriptors is persisted to the database.";
@ApiOperation(value = "Get Component Descriptor (getComponentDescriptorByClazz)",
notes = "Gets the Component Descriptor object using class name from the path parameters. " +
COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@RequestMapping(value = "/component/{componentDescriptorClazz:.+}", method = RequestMethod.GET)
@ResponseBody
public ComponentDescriptor getComponentDescriptorByClazz(
@ApiParam(value = "Component Descriptor class name", required = true)
@PathVariable("componentDescriptorClazz") String strComponentDescriptorClazz) throws ThingsboardException {
checkParameter("strComponentDescriptorClazz", strComponentDescriptorClazz);
try {
return checkComponentDescriptorByClazz(strComponentDescriptorClazz);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Component Descriptors (getComponentDescriptorsByType)",
notes = "Gets the Component Descriptors using rule node type and optional rule chain type request parameters. " +
COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET)
@ResponseBody
public List<ComponentDescriptor> getComponentDescriptorsByType(
@ApiParam(value = "Type of the Rule Node", allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", required = true)
@PathVariable("componentType") String strComponentType,
@ApiParam(value = "Type of the Rule Chain", allowableValues = "CORE,EDGE")
@RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
checkParameter("componentType", strComponentType);
try {
return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Component Descriptors (getComponentDescriptorsByTypes)",
notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " +
COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET)
@ResponseBody
public List<ComponentDescriptor> getComponentDescriptorsByTypes(
@ApiParam(value = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", required = true)
@RequestParam("componentTypes") String[] strComponentTypes,
@ApiParam(value = "Type of the Rule Chain", allowableValues = "CORE,EDGE")
@RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
checkArrayParameter("componentTypes", strComponentTypes);
try {
Set<ComponentType> componentTypes = new HashSet<>();
for (String strComponentType : strComponentTypes) {
componentTypes.add(ComponentType.valueOf(strComponentType));
}
return checkComponentDescriptorsByTypes(componentTypes, getRuleChainType(strRuleChainType));
} catch (Exception e) {
throw handleException(e);
}
}
private RuleChainType getRuleChainType(String strRuleChainType) {
RuleChainType ruleChainType;
if (StringUtils.isEmpty(strRuleChainType)) {
ruleChainType = RuleChainType.CORE;
} else {
ruleChainType = RuleChainType.valueOf(strRuleChainType);
}
return ruleChainType;
}
}
/**
* Copyright © 2016-2022 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;
public class ControllerConstants {
protected static final String NEW_LINE = "\n\n";
protected static final String UUID_WIKI_LINK = "[time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address)). ";
protected static final int DEFAULT_PAGE_SIZE = 1000;
protected static final String ENTITY_TYPE = "entityType";
protected static final String CUSTOMER_ID = "customerId";
protected static final String TENANT_ID = "tenantId";
protected static final String DEVICE_ID = "deviceId";
protected static final String EDGE_ID = "edgeId";
protected static final String RPC_ID = "rpcId";
protected static final String ENTITY_ID = "entityId";
protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " +
"The result is wrapped with PageData object that allows you to iterate over result set using pagination. " +
"See the 'Model' tab of the Response Class for more details. ";
protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ENTITY_VIEW_ID_PARAM_DESCRIPTION = "A string value representing the entity view id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ASSET_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the asset profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String TENANT_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the tenant profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String TENANT_ID_PARAM_DESCRIPTION = "A string value representing the tenant id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String EDGE_ID_PARAM_DESCRIPTION = "A string value representing the edge id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String CUSTOMER_ID_PARAM_DESCRIPTION = "A string value representing the customer id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String OTA_PACKAGE_ID_PARAM_DESCRIPTION = "A string value representing the ota package id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ENTITY_TYPE_PARAM_DESCRIPTION = "A string value representing the entity type. For example, 'DEVICE'";
protected static final String RULE_CHAIN_ID_PARAM_DESCRIPTION = "A string value representing the rule chain id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RULE_NODE_ID_PARAM_DESCRIPTION = "A string value representing the rule node id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String WIDGET_BUNDLE_ID_PARAM_DESCRIPTION = "A string value representing the widget bundle id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String WIDGET_TYPE_ID_PARAM_DESCRIPTION = "A string value representing the widget type id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String VC_REQUEST_ID_PARAM_DESCRIPTION = "A string value representing the version control request id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RESOURCE_ID_PARAM_DESCRIPTION = "A string value representing the resource id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String SYSTEM_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' authority.";
protected static final String SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' or 'TENANT_ADMIN' authority.";
protected static final String TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' authority.";
protected static final String TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' or 'CUSTOMER_USER' authority.";
protected static final String CUSTOMER_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'CUSTOMER_USER' authority.";
protected static final String AVAILABLE_FOR_ANY_AUTHORIZED_USER = "\n\nAvailable for any authorized user. ";
protected static final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page";
protected static final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0";
protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile";
protected static final String ENTITY_VIEW_TYPE_DESCRIPTION = "Entity View type";
protected static final String ASSET_TYPE_DESCRIPTION = "Asset type";
protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'";
protected static final String RULE_CHAIN_TYPE_DESCRIPTION = "Rule chain type (CORE or EDGE)";
protected static final String ASSET_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset name.";
protected static final String DASHBOARD_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the dashboard title.";
protected static final String WIDGET_BUNDLE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the widget bundle title.";
protected static final String RPC_TEXT_SEARCH_DESCRIPTION = "Not implemented. Leave empty.";
protected static final String DEVICE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device name.";
protected static final String ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the entity view name.";
protected static final String USER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the user email.";
protected static final String TENANT_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant name.";
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name.";
protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name.";
protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name.";
protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name.";
protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title.";
protected static final String EDGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the edge name.";
protected static final String EVENT_TEXT_SEARCH_DESCRIPTION = "The value is not used in searching.";
protected static final String AUDIT_LOG_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on one of the next properties: entityType, entityName, userName, actionType, actionStatus.";
protected static final String SORT_PROPERTY_DESCRIPTION = "Property of entity to sort by";
protected static final String DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title";
protected static final String CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, city";
protected static final String RPC_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, expirationTime, request, response";
protected static final String DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, deviceProfileName, label, customerTitle";
protected static final String ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type";
protected static final String ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, customerTitle";
protected static final String USER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, firstName, lastName, email";
protected static final String TENANT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, state, city, address, address2, zip, phone, email";
protected static final String TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault";
protected static final String TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name";
protected static final String TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, tenantProfileName, title, email, country, state, city, address, address2, zip, phone, email";
protected static final String DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, transportType, description, isDefault";
protected static final String ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault";
protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle";
protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status";
protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "ts, id";
protected static final String EDGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle";
protected static final String RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, root";
protected static final String WIDGET_BUNDLE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, tenantId";
protected static final String AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, entityType, entityName, userName, actionType, actionStatus";
protected static final String SORT_ORDER_DESCRIPTION = "Sort order. ASC (ASCENDING) or DESC (DESCENDING)";
protected static final String SORT_ORDER_ALLOWABLE_VALUES = "ASC, DESC";
protected static final String RPC_STATUS_ALLOWABLE_VALUES = "QUEUED, SENT, DELIVERED, SUCCESSFUL, TIMEOUT, EXPIRED, FAILED";
protected static final String RULE_CHAIN_TYPES_ALLOWABLE_VALUES = "CORE, EDGE";
protected static final String TRANSPORT_TYPE_ALLOWABLE_VALUES = "DEFAULT, MQTT, COAP, LWM2M, SNMP";
protected static final String DEVICE_INFO_DESCRIPTION = "Device Info is an extension of the default Device object that contains information about the assigned customer name and device profile name. ";
protected static final String ASSET_INFO_DESCRIPTION = "Asset Info is an extension of the default Asset object that contains information about the assigned customer name. ";
protected static final String ALARM_INFO_DESCRIPTION = "Alarm Info is an extension of the default Alarm object that also contains name of the alarm originator.";
protected static final String RELATION_INFO_DESCRIPTION = "Relation Info is an extension of the default Relation object that contains information about the 'from' and 'to' entity names. ";
protected static final String EDGE_INFO_DESCRIPTION = "Edge Info is an extension of the default Edge object that contains information about the assigned customer name. ";
protected static final String DEVICE_PROFILE_INFO_DESCRIPTION = "Device Profile Info is a lightweight object that includes main information about Device Profile excluding the heavyweight configuration object. ";
protected static final String ASSET_PROFILE_INFO_DESCRIPTION = "Asset Profile Info is a lightweight object that includes main information about Asset Profile. ";
protected static final String QUEUE_SERVICE_TYPE_DESCRIPTION = "Service type (implemented only for the TB-RULE-ENGINE)";
protected static final String QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES = "TB-RULE-ENGINE, TB-CORE, TB-TRANSPORT, JS-EXECUTOR";
protected static final String QUEUE_QUEUE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the queue name.";
protected static final String QUEUE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, topic";
protected static final String QUEUE_ID_PARAM_DESCRIPTION = "A string value representing the queue id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String QUEUE_NAME_PARAM_DESCRIPTION = "A string value representing the queue id. For example, 'Main'";
protected static final String OTA_PACKAGE_INFO_DESCRIPTION = "OTA Package Info is a lightweight object that includes main information about the OTA Package excluding the heavyweight data. ";
protected static final String OTA_PACKAGE_DESCRIPTION = "OTA Package is a heavyweight object that includes main information about the OTA Package and also data. ";
protected static final String OTA_PACKAGE_CHECKSUM_ALGORITHM_ALLOWABLE_VALUES = "MD5, SHA256, SHA384, SHA512, CRC32, MURMUR3_32, MURMUR3_128";
protected static final String OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the ota package title.";
protected static final String OTA_PACKAGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, type, title, version, tag, url, fileName, dataSize, checksum";
protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. ";
protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. ";
protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title.";
protected static final String RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, resourceType, tenantId";
protected static final String LWM2M_OBJECT_DESCRIPTION = "LwM2M Object is a object that includes information about the LwM2M model which can be used in transport configuration for the LwM2M device profile. ";
protected static final String LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name";
protected static final String DEVICE_NAME_DESCRIPTION = "A string value representing the Device name.";
protected static final String ASSET_NAME_DESCRIPTION = "A string value representing the Asset name.";
protected static final String EVENT_START_TIME_DESCRIPTION = "Timestamp. Events with creation time before it won't be queried.";
protected static final String EVENT_END_TIME_DESCRIPTION = "Timestamp. Events with creation time after it won't be queried.";
protected static final String EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION = "Unassignment works in async way - first, 'unassign' notification event pushed to edge queue on platform. ";
protected static final String EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION = "(Edge will receive this instantly, if it's currently connected, or once it's going to be connected to platform). ";
protected static final String EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION = "Assignment works in async way - first, notification event pushed to edge service queue on platform. ";
protected static final String EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION = "(Edge will receive this instantly, if it's currently connected, or once it's going to be connected to platform). ";
protected static final String ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the entity version name.";
protected static final String VERSION_ID_PARAM_DESCRIPTION = "Version id, for example fd82625bdd7d6131cf8027b44ee967012ecaf990. Represents commit hash.";
protected static final String BRANCH_PARAM_DESCRIPTION = "The name of the working branch, for example 'master'";
protected static final String MARKDOWN_CODE_BLOCK_START = "```json\n";
protected static final String MARKDOWN_CODE_BLOCK_END = "\n```";
protected static final String EVENT_ERROR_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"eventType\":\"ERROR\",\n" +
" \"server\":\"ip-172-31-24-152\",\n" +
" \"method\":\"onClusterEventMsg\",\n" +
" \"errorStr\":\"Error Message\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String EVENT_LC_EVENT_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"eventType\":\"LC_EVENT\",\n" +
" \"server\":\"ip-172-31-24-152\",\n" +
" \"event\":\"STARTED\",\n" +
" \"status\":\"Success\",\n" +
" \"errorStr\":\"Error Message\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String EVENT_STATS_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"eventType\":\"STATS\",\n" +
" \"server\":\"ip-172-31-24-152\",\n" +
" \"messagesProcessed\":10,\n" +
" \"errorsOccurred\":5\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEBUG_FILTER_OBJ =
" \"msgDirectionType\":\"IN\",\n" +
" \"server\":\"ip-172-31-24-152\",\n" +
" \"dataSearch\":\"humidity\",\n" +
" \"metadataSearch\":\"deviceName\",\n" +
" \"entityName\":\"DEVICE\",\n" +
" \"relationType\":\"Success\",\n" +
" \"entityId\":\"de9d54a0-2b7a-11ec-a3cc-23386423d98f\",\n" +
" \"msgType\":\"POST_TELEMETRY_REQUEST\",\n" +
" \"isError\":\"false\",\n" +
" \"errorStr\":\"Error Message\"\n" +
"}";
protected static final String EVENT_DEBUG_RULE_NODE_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"eventType\":\"DEBUG_RULE_NODE\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END;
protected static final String EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"eventType\":\"DEBUG_RULE_CHAIN\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END;
protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. ";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\": \"LwRpk00000000\",\n" +
" \"type\": \"lwm2mProfileRpk\"\n" +
" },\n" +
" \"credentials\": {\n" +
" \"id\": \"null\",\n" +
" \"createdTime\": 0,\n" +
" \"deviceId\": \"null\",\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\": {\n" +
" \"client\": {\n" +
" \"endpoint\": \"LwRpk00000000\",\n" +
" \"securityConfigClientMode\": \"RPK\",\n" +
" \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\"\n" +
" },\n" +
" \"bootstrap\": {\n" +
" \"bootstrapServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" },\n" +
" \"lwm2mServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
"Provides a hint about the data type of the entity field that is defined in the filter key. " +
"The value type impacts the list of possible operations that you may use in the corresponding predicate. For example, you may use 'STARTS_WITH' or 'END_WITH', but you can't use 'GREATER_OR_EQUAL' for string values." +
"The following filter value types and corresponding predicate operations are supported: " + NEW_LINE +
" * 'STRING' - used to filter any 'String' or 'JSON' values. Operations: EQUAL, NOT_EQUAL, STARTS_WITH, ENDS_WITH, CONTAINS, NOT_CONTAINS; \n" +
" * 'NUMERIC' - used for 'Long' and 'Double' values. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n" +
" * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" +
" * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n";
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
" \"endsOn\":64800000,\n" +
" \"startsOn\":43200000,\n" +
" \"timezone\":\"Europe/Kiev\",\n" +
" \"daysOfWeek\":[\n" +
" 1,\n" +
" 3,\n" +
" 5\n" +
" ]\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
" \"items\":[\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":1\n" +
" },\n" +
" {\n" +
" \"endsOn\":64800000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":43200000,\n" +
" \"dayOfWeek\":2\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":3\n" +
" },\n" +
" {\n" +
" \"endsOn\":57600000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":36000000,\n" +
" \"dayOfWeek\":4\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":5\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":6\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":7\n" +
" }\n" +
" ],\n" +
" \"timezone\":\"Europe/Kiev\"\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":5,\n" +
" \"dynamicValue\":{\n" +
" \"inherit\":true,\n" +
" \"sourceType\":\"CURRENT_DEVICE\",\n" +
" \"sourceAttribute\":\"tempAttr\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"DURATION\",\n" +
" \"unit\":\"MINUTES\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String RELATION_TYPE_PARAM_DESCRIPTION = "A string value representing relation type between entities. For example, 'Contains', 'Manages'. It can be any string value.";
protected static final String RELATION_TYPE_GROUP_PARAM_DESCRIPTION = "A string value representing relation type group. For example, 'COMMON'";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
protected static final String DEFAULT_DASHBOARD = "defaultDashboardId";
protected static final String HOME_DASHBOARD = "homeDashboardId";
protected static final String SINGLE_ENTITY = "\n\n## Single Entity\n\n" +
"Allows to filter only one entity based on the id. For example, this entity filter selects certain device:\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"singleEntity\",\n" +
" \"singleEntity\": {\n" +
" \"id\": \"d521edb0-2a7a-11ec-94eb-213c95f54092\",\n" +
" \"entityType\": \"DEVICE\"\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String ENTITY_LIST = "\n\n## Entity List Filter\n\n" +
"Allows to filter entities of the same type using their ids. For example, this entity filter selects two devices:\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"entityList\",\n" +
" \"entityType\": \"DEVICE\",\n" +
" \"entityList\": [\n" +
" \"e6501f30-2a7a-11ec-94eb-213c95f54092\",\n" +
" \"e6657bf0-2a7a-11ec-94eb-213c95f54092\"\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String ENTITY_NAME = "\n\n## Entity Name Filter\n\n" +
"Allows to filter entities of the same type using the **'starts with'** expression over entity name. " +
"For example, this entity filter selects all devices which name starts with 'Air Quality':\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"entityName\",\n" +
" \"entityType\": \"DEVICE\",\n" +
" \"entityNameFilter\": \"Air Quality\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String ENTITY_TYPE_FILTER = "\n\n## Entity Type Filter\n\n" +
"Allows to filter entities based on their type (CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, etc)" +
"For example, this entity filter selects all tenant customers:\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"entityType\",\n" +
" \"entityType\": \"CUSTOMER\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String ASSET_TYPE = "\n\n## Asset Type Filter\n\n" +
"Allows to filter assets based on their type and the **'starts with'** expression over their name. " +
"For example, this entity filter selects all 'charging station' assets which name starts with 'Tesla':\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"assetType\",\n" +
" \"assetType\": \"charging station\",\n" +
" \"assetNameFilter\": \"Tesla\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String DEVICE_TYPE = "\n\n## Device Type Filter\n\n" +
"Allows to filter devices based on their type and the **'starts with'** expression over their name. " +
"For example, this entity filter selects all 'Temperature Sensor' devices which name starts with 'ABC':\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"deviceType\",\n" +
" \"deviceType\": \"Temperature Sensor\",\n" +
" \"deviceNameFilter\": \"ABC\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String EDGE_TYPE = "\n\n## Edge Type Filter\n\n" +
"Allows to filter edge instances based on their type and the **'starts with'** expression over their name. " +
"For example, this entity filter selects all 'Factory' edge instances which name starts with 'Nevada':\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"edgeType\",\n" +
" \"edgeType\": \"Factory\",\n" +
" \"edgeNameFilter\": \"Nevada\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String ENTITY_VIEW_TYPE = "\n\n## Entity View Filter\n\n" +
"Allows to filter entity views based on their type and the **'starts with'** expression over their name. " +
"For example, this entity filter selects all 'Concrete Mixer' entity views which name starts with 'CAT':\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"entityViewType\",\n" +
" \"entityViewType\": \"Concrete Mixer\",\n" +
" \"entityViewNameFilter\": \"CAT\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String API_USAGE = "\n\n## Api Usage Filter\n\n" +
"Allows to query for Api Usage based on optional customer id. If the customer id is not set, returns current tenant API usage." +
"For example, this entity filter selects the 'Api Usage' entity for customer with id 'e6501f30-2a7a-11ec-94eb-213c95f54092':\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"apiUsageState\",\n" +
" \"customerId\": {\n" +
" \"id\": \"d521edb0-2a7a-11ec-94eb-213c95f54092\",\n" +
" \"entityType\": \"CUSTOMER\"\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String MAX_LEVEL_DESCRIPTION = "Possible direction values are 'TO' and 'FROM'. The 'maxLevel' defines how many relation levels should the query search 'recursively'. ";
protected static final String FETCH_LAST_LEVEL_ONLY_DESCRIPTION = "Assuming the 'maxLevel' is > 1, the 'fetchLastLevelOnly' defines either to return all related entities or only entities that are on the last level of relations. ";
protected static final String RELATIONS_QUERY_FILTER = "\n\n## Relations Query Filter\n\n" +
"Allows to filter entities that are related to the provided root entity. " +
MAX_LEVEL_DESCRIPTION +
FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
"The 'filter' object allows you to define the relation type and set of acceptable entity types to search for. " +
"The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only those who match the 'filters'.\n\n" +
"For example, this entity filter selects all devices and assets which are related to the asset with id 'e51de0c0-2a7a-11ec-94eb-213c95f54092':\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"relationsQuery\",\n" +
" \"rootEntity\": {\n" +
" \"entityType\": \"ASSET\",\n" +
" \"id\": \"e51de0c0-2a7a-11ec-94eb-213c95f54092\"\n" +
" },\n" +
" \"direction\": \"FROM\",\n" +
" \"maxLevel\": 1,\n" +
" \"fetchLastLevelOnly\": false,\n" +
" \"filters\": [\n" +
" {\n" +
" \"relationType\": \"Contains\",\n" +
" \"entityTypes\": [\n" +
" \"DEVICE\",\n" +
" \"ASSET\"\n" +
" ]\n" +
" }\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String ASSET_QUERY_FILTER = "\n\n## Asset Search Query\n\n" +
"Allows to filter assets that are related to the provided root entity. Filters related assets based on the relation type and set of asset types. " +
MAX_LEVEL_DESCRIPTION +
FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
"The 'relationType' defines the type of the relation to search for. " +
"The 'assetTypes' defines the type of the asset to search for. " +
"The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only assets that match 'relationType' and 'assetTypes' conditions.\n\n" +
"For example, this entity filter selects 'charging station' assets which are related to the asset with id 'e51de0c0-2a7a-11ec-94eb-213c95f54092' using 'Contains' relation:\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"assetSearchQuery\",\n" +
" \"rootEntity\": {\n" +
" \"entityType\": \"ASSET\",\n" +
" \"id\": \"e51de0c0-2a7a-11ec-94eb-213c95f54092\"\n" +
" },\n" +
" \"direction\": \"FROM\",\n" +
" \"maxLevel\": 1,\n" +
" \"fetchLastLevelOnly\": false,\n" +
" \"relationType\": \"Contains\",\n" +
" \"assetTypes\": [\n" +
" \"charging station\"\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String DEVICE_QUERY_FILTER = "\n\n## Device Search Query\n\n" +
"Allows to filter devices that are related to the provided root entity. Filters related devices based on the relation type and set of device types. " +
MAX_LEVEL_DESCRIPTION +
FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
"The 'relationType' defines the type of the relation to search for. " +
"The 'deviceTypes' defines the type of the device to search for. " +
"The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only devices that match 'relationType' and 'deviceTypes' conditions.\n\n" +
"For example, this entity filter selects 'Charging port' and 'Air Quality Sensor' devices which are related to the asset with id 'e52b0020-2a7a-11ec-94eb-213c95f54092' using 'Contains' relation:\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"deviceSearchQuery\",\n" +
" \"rootEntity\": {\n" +
" \"entityType\": \"ASSET\",\n" +
" \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" +
" },\n" +
" \"direction\": \"FROM\",\n" +
" \"maxLevel\": 2,\n" +
" \"fetchLastLevelOnly\": true,\n" +
" \"relationType\": \"Contains\",\n" +
" \"deviceTypes\": [\n" +
" \"Air Quality Sensor\",\n" +
" \"Charging port\"\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String EV_QUERY_FILTER = "\n\n## Entity View Query\n\n" +
"Allows to filter entity views that are related to the provided root entity. Filters related entity views based on the relation type and set of entity view types. " +
MAX_LEVEL_DESCRIPTION +
FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
"The 'relationType' defines the type of the relation to search for. " +
"The 'entityViewTypes' defines the type of the entity view to search for. " +
"The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only devices that match 'relationType' and 'deviceTypes' conditions.\n\n" +
"For example, this entity filter selects 'Concrete mixer' entity views which are related to the asset with id 'e52b0020-2a7a-11ec-94eb-213c95f54092' using 'Contains' relation:\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"entityViewSearchQuery\",\n" +
" \"rootEntity\": {\n" +
" \"entityType\": \"ASSET\",\n" +
" \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" +
" },\n" +
" \"direction\": \"FROM\",\n" +
" \"maxLevel\": 1,\n" +
" \"fetchLastLevelOnly\": false,\n" +
" \"relationType\": \"Contains\",\n" +
" \"entityViewTypes\": [\n" +
" \"Concrete mixer\"\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String EDGE_QUERY_FILTER = "\n\n## Edge Search Query\n\n" +
"Allows to filter edge instances that are related to the provided root entity. Filters related edge instances based on the relation type and set of edge types. " +
MAX_LEVEL_DESCRIPTION +
FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
"The 'relationType' defines the type of the relation to search for. " +
"The 'deviceTypes' defines the type of the device to search for. " +
"The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only devices that match 'relationType' and 'deviceTypes' conditions.\n\n" +
"For example, this entity filter selects 'Factory' edge instances which are related to the asset with id 'e52b0020-2a7a-11ec-94eb-213c95f54092' using 'Contains' relation:\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"deviceSearchQuery\",\n" +
" \"rootEntity\": {\n" +
" \"entityType\": \"ASSET\",\n" +
" \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" +
" },\n" +
" \"direction\": \"FROM\",\n" +
" \"maxLevel\": 2,\n" +
" \"fetchLastLevelOnly\": true,\n" +
" \"relationType\": \"Contains\",\n" +
" \"edgeTypes\": [\n" +
" \"Factory\"\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String EMPTY = "\n\n## Entity Type Filter\n\n" +
"Allows to filter multiple entities of the same type using the **'starts with'** expression over entity name. " +
"For example, this entity filter selects all devices which name starts with 'Air Quality':\n\n" +
MARKDOWN_CODE_BLOCK_START +
"" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String ENTITY_FILTERS =
"\n\n # Entity Filters" +
"\nEntity Filter body depends on the 'type' parameter. Let's review available entity filter types. In fact, they do correspond to available dashboard aliases." +
SINGLE_ENTITY + ENTITY_LIST + ENTITY_NAME + ENTITY_TYPE_FILTER + ASSET_TYPE + DEVICE_TYPE + EDGE_TYPE + ENTITY_VIEW_TYPE + API_USAGE + RELATIONS_QUERY_FILTER
+ ASSET_QUERY_FILTER + DEVICE_QUERY_FILTER + EV_QUERY_FILTER + EDGE_QUERY_FILTER;
protected static final String FILTER_KEY = "\n\n## Filter Key\n\n" +
"Filter Key defines either entity field, attribute or telemetry. It is a JSON object that consists the key name and type. " +
"The following filter key types are supported: \n\n" +
" * 'CLIENT_ATTRIBUTE' - used for client attributes; \n" +
" * 'SHARED_ATTRIBUTE' - used for shared attributes; \n" +
" * 'SERVER_ATTRIBUTE' - used for server attributes; \n" +
" * 'ATTRIBUTE' - used for any of the above; \n" +
" * 'TIME_SERIES' - used for time-series values; \n" +
" * 'ENTITY_FIELD' - used for accessing entity fields like 'name', 'label', etc. The list of available fields depends on the entity type; \n" +
" * 'ALARM_FIELD' - similar to entity field, but is used in alarm queries only; \n" +
"\n\n Let's review the example:\n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"key\": \"temperature\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String FILTER_PREDICATE = "\n\n## Filter Predicate\n\n" +
"Filter Predicate defines the logical expression to evaluate. The list of available operations depends on the filter value type, see above. " +
"Platform supports 4 predicate types: 'STRING', 'NUMERIC', 'BOOLEAN' and 'COMPLEX'. The last one allows to combine multiple operations over one filter key." +
"\n\nSimple predicate example to check 'value < 100': \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"operation\": \"LESS\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 100,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\nComplex predicate example, to check 'value < 10 or value > 20': \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"COMPLEX\",\n" +
" \"operation\": \"OR\",\n" +
" \"predicates\": [\n" +
" {\n" +
" \"operation\": \"LESS\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 10,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" },\n" +
" {\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 20,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" }\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\nMore complex predicate example, to check 'value < 10 or (value > 50 && value < 60)': \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"COMPLEX\",\n" +
" \"operation\": \"OR\",\n" +
" \"predicates\": [\n" +
" {\n" +
" \"operation\": \"LESS\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 10,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" },\n" +
" {\n" +
" \"type\": \"COMPLEX\",\n" +
" \"operation\": \"AND\",\n" +
" \"predicates\": [\n" +
" {\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 50,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" },\n" +
" {\n" +
" \"operation\": \"LESS\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 60,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n You may also want to replace hardcoded values (for example, temperature > 20) with the more dynamic " +
"expression (for example, temperature > 'value of the tenant attribute with key 'temperatureThreshold'). " +
"It is possible to use 'dynamicValue' to define attribute of the tenant, customer or user that is performing the API call. " +
"See example below: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 0,\n" +
" \"dynamicValue\": {\n" +
" \"sourceType\": \"CURRENT_USER\",\n" +
" \"sourceAttribute\": \"temperatureThreshold\"\n" +
" }\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n Note that you may use 'CURRENT_USER', 'CURRENT_CUSTOMER' and 'CURRENT_TENANT' as a 'sourceType'. The 'defaultValue' is used when the attribute with such a name is not defined for the chosen source.";
protected static final String KEY_FILTERS =
"\n\n # Key Filters" +
"\nKey Filter allows you to define complex logical expressions over entity field, attribute or latest time-series value. The filter is defined using 'key', 'valueType' and 'predicate' objects. " +
"Single Entity Query may have zero, one or multiple predicates. If multiple filters are defined, they are evaluated using logical 'AND'. " +
"The example below checks that temperature of the entity is above 20 degrees:" +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"key\": {\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"key\": \"temperature\"\n" +
" },\n" +
" \"valueType\": \"NUMERIC\",\n" +
" \"predicate\": {\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 20,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n Now let's review 'key', 'valueType' and 'predicate' objects in detail."
+ FILTER_KEY + FILTER_VALUE_TYPE + FILTER_PREDICATE;
protected static final String ENTITY_COUNT_QUERY_DESCRIPTION =
"Allows to run complex queries to search the count of platform entities (devices, assets, customers, etc) " +
"based on the combination of main entity filter and multiple key filters. Returns the number of entities that match the query definition.\n\n" +
"# Query Definition\n\n" +
"\n\nMain **entity filter** is mandatory and defines generic search criteria. " +
"For example, \"find all devices with profile 'Moisture Sensor'\" or \"Find all devices related to asset 'Building A'\"" +
"\n\nOptional **key filters** allow to filter results of the entity filter by complex criteria against " +
"main entity fields (name, label, type, etc), attributes and telemetry. " +
"For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and timeseries field 'batteryLevel' > 40\"." +
"\n\nLet's review the example:" +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"entityFilter\": {\n" +
" \"type\": \"entityType\",\n" +
" \"entityType\": \"DEVICE\"\n" +
" },\n" +
" \"keyFilters\": [\n" +
" {\n" +
" \"key\": {\n" +
" \"type\": \"ATTRIBUTE\",\n" +
" \"key\": \"active\"\n" +
" },\n" +
" \"valueType\": \"BOOLEAN\",\n" +
" \"predicate\": {\n" +
" \"operation\": \"EQUAL\",\n" +
" \"value\": {\n" +
" \"defaultValue\": true,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"BOOLEAN\"\n" +
" }\n" +
" }\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n Example mentioned above search all devices which have attribute 'active' set to 'true'. Now let's review available entity filters and key filters syntax:" +
ENTITY_FILTERS +
KEY_FILTERS +
ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
protected static final String ENTITY_DATA_QUERY_DESCRIPTION =
"Allows to run complex queries over platform entities (devices, assets, customers, etc) " +
"based on the combination of main entity filter and multiple key filters. " +
"Returns the paginated result of the query that contains requested entity fields and latest values of requested attributes and time-series data.\n\n" +
"# Query Definition\n\n" +
"\n\nMain **entity filter** is mandatory and defines generic search criteria. " +
"For example, \"find all devices with profile 'Moisture Sensor'\" or \"Find all devices related to asset 'Building A'\"" +
"\n\nOptional **key filters** allow to filter results of the **entity filter** by complex criteria against " +
"main entity fields (name, label, type, etc), attributes and telemetry. " +
"For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and timeseries field 'batteryLevel' > 40\"." +
"\n\nThe **entity fields** and **latest values** contains list of entity fields and latest attribute/telemetry fields to fetch for each entity." +
"\n\nThe **page link** contains information about the page to fetch and the sort ordering." +
"\n\nLet's review the example:" +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"entityFilter\": {\n" +
" \"type\": \"entityType\",\n" +
" \"resolveMultiple\": true,\n" +
" \"entityType\": \"DEVICE\"\n" +
" },\n" +
" \"keyFilters\": [\n" +
" {\n" +
" \"key\": {\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"key\": \"temperature\"\n" +
" },\n" +
" \"valueType\": \"NUMERIC\",\n" +
" \"predicate\": {\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 0,\n" +
" \"dynamicValue\": {\n" +
" \"sourceType\": \"CURRENT_USER\",\n" +
" \"sourceAttribute\": \"temperatureThreshold\",\n" +
" \"inherit\": false\n" +
" }\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" }\n" +
" }\n" +
" ],\n" +
" \"entityFields\": [\n" +
" {\n" +
" \"type\": \"ENTITY_FIELD\",\n" +
" \"key\": \"name\"\n" +
" },\n" +
" {\n" +
" \"type\": \"ENTITY_FIELD\",\n" +
" \"key\": \"label\"\n" +
" },\n" +
" {\n" +
" \"type\": \"ENTITY_FIELD\",\n" +
" \"key\": \"additionalInfo\"\n" +
" }\n" +
" ],\n" +
" \"latestValues\": [\n" +
" {\n" +
" \"type\": \"ATTRIBUTE\",\n" +
" \"key\": \"model\"\n" +
" },\n" +
" {\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"key\": \"temperature\"\n" +
" }\n" +
" ],\n" +
" \"pageLink\": {\n" +
" \"page\": 0,\n" +
" \"pageSize\": 10,\n" +
" \"sortOrder\": {\n" +
" \"key\": {\n" +
" \"key\": \"name\",\n" +
" \"type\": \"ENTITY_FIELD\"\n" +
" },\n" +
" \"direction\": \"ASC\"\n" +
" }\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n Example mentioned above search all devices which have attribute 'active' set to 'true'. Now let's review available entity filters and key filters syntax:" +
ENTITY_FILTERS +
KEY_FILTERS +
ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
protected static final String ALARM_DATA_QUERY_DESCRIPTION = "This method description defines how Alarm Data Query extends the Entity Data Query. " +
"See method 'Find Entity Data by Query' first to get the info about 'Entity Data Query'." +
"\n\n The platform will first search the entities that match the entity and key filters. Then, the platform will use 'Alarm Page Link' to filter the alarms related to those entities. " +
"Finally, platform fetch the properties of alarm that are defined in the **'alarmFields'** and combine them with the other entity, attribute and latest time-series fields to return the result. " +
"\n\n See example of the alarm query below. The query will search first 100 active alarms with type 'Temperature Alarm' or 'Fire Alarm' for any device with current temperature > 0. " +
"The query will return combination of the entity fields: name of the device, device model and latest temperature reading and alarms fields: createdTime, type, severity and status: " +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"entityFilter\": {\n" +
" \"type\": \"entityType\",\n" +
" \"resolveMultiple\": true,\n" +
" \"entityType\": \"DEVICE\"\n" +
" },\n" +
" \"pageLink\": {\n" +
" \"page\": 0,\n" +
" \"pageSize\": 100,\n" +
" \"textSearch\": null,\n" +
" \"searchPropagatedAlarms\": false,\n" +
" \"statusList\": [\n" +
" \"ACTIVE\"\n" +
" ],\n" +
" \"severityList\": [\n" +
" \"CRITICAL\",\n" +
" \"MAJOR\"\n" +
" ],\n" +
" \"typeList\": [\n" +
" \"Temperature Alarm\",\n" +
" \"Fire Alarm\"\n" +
" ],\n" +
" \"sortOrder\": {\n" +
" \"key\": {\n" +
" \"key\": \"createdTime\",\n" +
" \"type\": \"ALARM_FIELD\"\n" +
" },\n" +
" \"direction\": \"DESC\"\n" +
" },\n" +
" \"timeWindow\": 86400000\n" +
" },\n" +
" \"keyFilters\": [\n" +
" {\n" +
" \"key\": {\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"key\": \"temperature\"\n" +
" },\n" +
" \"valueType\": \"NUMERIC\",\n" +
" \"predicate\": {\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"defaultValue\": 0,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" }\n" +
" }\n" +
" ],\n" +
" \"alarmFields\": [\n" +
" {\n" +
" \"type\": \"ALARM_FIELD\",\n" +
" \"key\": \"createdTime\"\n" +
" },\n" +
" {\n" +
" \"type\": \"ALARM_FIELD\",\n" +
" \"key\": \"type\"\n" +
" },\n" +
" {\n" +
" \"type\": \"ALARM_FIELD\",\n" +
" \"key\": \"severity\"\n" +
" },\n" +
" {\n" +
" \"type\": \"ALARM_FIELD\",\n" +
" \"key\": \"status\"\n" +
" }\n" +
" ],\n" +
" \"entityFields\": [\n" +
" {\n" +
" \"type\": \"ENTITY_FIELD\",\n" +
" \"key\": \"name\"\n" +
" }\n" +
" ],\n" +
" \"latestValues\": [\n" +
" {\n" +
" \"type\": \"ATTRIBUTE\",\n" +
" \"key\": \"model\"\n" +
" },\n" +
" {\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"key\": \"temperature\"\n" +
" }\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"";
protected static final String COAP_TRANSPORT_CONFIGURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\":\"COAP\",\n" +
" \"clientSettings\":{\n" +
" \"edrxCycle\":null,\n" +
" \"powerMode\":\"DRX\",\n" +
" \"psmActivityTimer\":null,\n" +
" \"pagingTransmissionWindow\":null\n" +
" },\n" +
" \"coapDeviceTypeConfiguration\":{\n" +
" \"coapDeviceType\":\"DEFAULT\",\n" +
" \"transportPayloadTypeConfiguration\":{\n" +
" \"transportPayloadType\":\"JSON\"\n" +
" }\n" +
" }\n" +
"}"
+ MARKDOWN_CODE_BLOCK_END;
protected static final String TRANSPORT_CONFIGURATION = "# Transport Configuration" + NEW_LINE +
"5 transport configuration types are available:\n" +
" * 'DEFAULT';\n" +
" * 'MQTT';\n" +
" * 'LWM2M';\n" +
" * 'COAP';\n" +
" * 'SNMP'." + NEW_LINE + "Default type supports basic MQTT, HTTP, CoAP and LwM2M transports. " +
"Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-profiles/#transport-configuration) for more details about other types.\n" +
"\nSee another example of COAP transport configuration below:" + NEW_LINE + COAP_TRANSPORT_CONFIGURATION_EXAMPLE;
protected static final String ALARM_FILTER_KEY = "## Alarm Filter Key" + NEW_LINE +
"Filter Key defines either entity field, attribute, telemetry or constant. It is a JSON object that consists the key name and type. The following filter key types are supported:\n" +
" * 'ATTRIBUTE' - used for attributes values;\n" +
" * 'TIME_SERIES' - used for time-series values;\n" +
" * 'ENTITY_FIELD' - used for accessing entity fields like 'name', 'label', etc. The list of available fields depends on the entity type;\n" +
" * 'CONSTANT' - constant value specified." + NEW_LINE + "Let's review the example:" + NEW_LINE +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"TIME_SERIES\",\n" +
" \"key\": \"temperature\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_FILTER_PREDICATE = NEW_LINE + "## Filter Predicate" + NEW_LINE +
"Filter Predicate defines the logical expression to evaluate. The list of available operations depends on the filter value type, see above. " +
"Platform supports 4 predicate types: 'STRING', 'NUMERIC', 'BOOLEAN' and 'COMPLEX'. The last one allows to combine multiple operations over one filter key." + NEW_LINE +
"Simple predicate example to check 'value < 100': " + NEW_LINE +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"operation\": \"LESS\",\n" +
" \"value\": {\n" +
" \"userValue\": null,\n" +
" \"defaultValue\": 100,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END + NEW_LINE +
"Complex predicate example, to check 'value < 10 or value > 20': " + NEW_LINE +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"COMPLEX\",\n" +
" \"operation\": \"OR\",\n" +
" \"predicates\": [\n" +
" {\n" +
" \"operation\": \"LESS\",\n" +
" \"value\": {\n" +
" \"userValue\": null,\n" +
" \"defaultValue\": 10,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" },\n" +
" {\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"userValue\": null,\n" +
" \"defaultValue\": 20,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" }\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END + NEW_LINE +
"More complex predicate example, to check 'value < 10 or (value > 50 && value < 60)': " + NEW_LINE +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"type\": \"COMPLEX\",\n" +
" \"operation\": \"OR\",\n" +
" \"predicates\": [\n" +
" {\n" +
" \"operation\": \"LESS\",\n" +
" \"value\": {\n" +
" \"userValue\": null,\n" +
" \"defaultValue\": 10,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" },\n" +
" {\n" +
" \"type\": \"COMPLEX\",\n" +
" \"operation\": \"AND\",\n" +
" \"predicates\": [\n" +
" {\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"userValue\": null,\n" +
" \"defaultValue\": 50,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" },\n" +
" {\n" +
" \"operation\": \"LESS\",\n" +
" \"value\": {\n" +
" \"userValue\": null,\n" +
" \"defaultValue\": 60,\n" +
" \"dynamicValue\": null\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END + NEW_LINE +
"You may also want to replace hardcoded values (for example, temperature > 20) with the more dynamic " +
"expression (for example, temperature > value of the tenant attribute with key 'temperatureThreshold'). " +
"It is possible to use 'dynamicValue' to define attribute of the tenant, customer or device. " +
"See example below:" + NEW_LINE +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"operation\": \"GREATER\",\n" +
" \"value\": {\n" +
" \"userValue\": null,\n" +
" \"defaultValue\": 0,\n" +
" \"dynamicValue\": {\n" +
" \"inherit\": false,\n" +
" \"sourceType\": \"CURRENT_TENANT\",\n" +
" \"sourceAttribute\": \"temperatureThreshold\"\n" +
" }\n" +
" },\n" +
" \"type\": \"NUMERIC\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END + NEW_LINE +
"Note that you may use 'CURRENT_DEVICE', 'CURRENT_CUSTOMER' and 'CURRENT_TENANT' as a 'sourceType'. The 'defaultValue' is used when the attribute with such a name is not defined for the chosen source. " +
"The 'sourceAttribute' can be inherited from the owner of the specified 'sourceType' if 'inherit' is set to true.";
protected static final String KEY_FILTERS_DESCRIPTION = "# Key Filters" + NEW_LINE +
"Key filter objects are created under the **'condition'** array. They allow you to define complex logical expressions over entity field, " +
"attribute, latest time-series value or constant. The filter is defined using 'key', 'valueType', " +
"'value' (refers to the value of the 'CONSTANT' alarm filter key type) and 'predicate' objects. Let's review each object:" + NEW_LINE +
ALARM_FILTER_KEY + FILTER_VALUE_TYPE + NEW_LINE + DEVICE_PROFILE_FILTER_PREDICATE + NEW_LINE;
protected static final String DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"alarms\":[\n" +
" ],\n" +
" \"configuration\":{\n" +
" \"type\":\"DEFAULT\"\n" +
" },\n" +
" \"provisionConfiguration\":{\n" +
" \"type\":\"DISABLED\",\n" +
" \"provisionDeviceSecret\":null\n" +
" },\n" +
" \"transportConfiguration\":{\n" +
" \"type\":\"DEFAULT\"\n" +
" }\n" +
"}" + MARKDOWN_CODE_BLOCK_END;
protected static final String CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"alarms\":[\n" +
" {\n" +
" \"id\":\"2492b935-1226-59e9-8615-17d8978a4f93\",\n" +
" \"alarmType\":\"Temperature Alarm\",\n" +
" \"clearRule\":{\n" +
" \"schedule\":null,\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"SIMPLE\"\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"LESS\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"propagate\":false,\n" +
" \"createRules\":{\n" +
" \"MAJOR\":{\n" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
" \"endsOn\":64800000,\n" +
" \"startsOn\":43200000,\n" +
" \"timezone\":\"Europe/Kiev\",\n" +
" \"daysOfWeek\":[\n" +
" 1,\n" +
" 3,\n" +
" 5\n" +
" ]\n" +
" },\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"DURATION\",\n" +
" \"unit\":\"MINUTES\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"COMPLEX\",\n" +
" \"operation\":\"OR\",\n" +
" \"predicates\":[\n" +
" {\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":50.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"LESS_OR_EQUAL\"\n" +
" },\n" +
" {\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"GREATER\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"WARNING\":{\n" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
" \"items\":[\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":1\n" +
" },\n" +
" {\n" +
" \"endsOn\":64800000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":43200000,\n" +
" \"dayOfWeek\":2\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":3\n" +
" },\n" +
" {\n" +
" \"endsOn\":57600000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":36000000,\n" +
" \"dayOfWeek\":4\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":5\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":6\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":7\n" +
" }\n" +
" ],\n" +
" \"timezone\":\"Europe/Kiev\"\n" +
" },\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":5,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"tempConstant\",\n" +
" \"type\":\"CONSTANT\"\n" +
" },\n" +
" \"value\":30,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":0.0,\n" +
" \"dynamicValue\":{\n" +
" \"inherit\":false,\n" +
" \"sourceType\":\"CURRENT_DEVICE\",\n" +
" \"sourceAttribute\":\"tempThreshold\"\n" +
" }\n" +
" },\n" +
" \"operation\":\"EQUAL\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"CRITICAL\":{\n" +
" \"schedule\":null,\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"SIMPLE\"\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":50.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"GREATER\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" }\n" +
" },\n" +
" \"propagateRelationTypes\":null\n" +
" }\n" +
" ],\n" +
" \"configuration\":{\n" +
" \"type\":\"DEFAULT\"\n" +
" },\n" +
" \"provisionConfiguration\":{\n" +
" \"type\":\"ALLOW_CREATE_NEW_DEVICES\",\n" +
" \"provisionDeviceSecret\":\"vaxb9hzqdbz3oqukvomg\"\n" +
" },\n" +
" \"transportConfiguration\":{\n" +
" \"type\":\"MQTT\",\n" +
" \"deviceTelemetryTopic\":\"v1/devices/me/telemetry\",\n" +
" \"deviceAttributesTopic\":\"v1/devices/me/attributes\",\n" +
" \"transportPayloadTypeConfiguration\":{\n" +
" \"transportPayloadType\":\"PROTOBUF\",\n" +
" \"deviceTelemetryProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage telemetry;\\n\\nmessage SensorDataReading {\\n\\n optional double temperature = 1;\\n optional double humidity = 2;\\n InnerObject innerObject = 3;\\n\\n message InnerObject {\\n optional string key1 = 1;\\n optional bool key2 = 2;\\n optional double key3 = 3;\\n optional int32 key4 = 4;\\n optional string key5 = 5;\\n }\\n}\",\n" +
" \"deviceAttributesProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage attributes;\\n\\nmessage SensorConfiguration {\\n optional string firmwareVersion = 1;\\n optional string serialNumber = 2;\\n}\",\n" +
" \"deviceRpcRequestProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage rpc;\\n\\nmessage RpcRequestMsg {\\n optional string method = 1;\\n optional int32 requestId = 2;\\n optional string params = 3;\\n}\",\n" +
" \"deviceRpcResponseProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage rpc;\\n\\nmessage RpcResponseMsg {\\n optional string payload = 1;\\n}\"\n" +
" }\n" +
" }\n" +
"}" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_DATA_DEFINITION = NEW_LINE + "# Device profile data definition" + NEW_LINE +
"Device profile data object contains alarm rules configuration, device provision strategy and transport type configuration for device connectivity. Let's review some examples. " +
"First one is the default device profile data configuration and second one - the custom one. " +
NEW_LINE + DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE +
NEW_LINE + "Let's review some specific objects examples related to the device profile configuration:";
protected static final String ALARM_SCHEDULE = NEW_LINE + "# Alarm Schedule" + NEW_LINE +
"Alarm Schedule JSON object represents the time interval during which the alarm rule is active. Note, " +
NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE + NEW_LINE + "means alarm rule is active all the time. " +
"**'daysOfWeek'** field represents Monday as 1, Tuesday as 2 and so on. **'startsOn'** and **'endsOn'** fields represent hours in millis (e.g. 64800000 = 18:00 or 6pm). " +
"**'enabled'** flag specifies if item in a custom rule is active for specific day of the week:" + NEW_LINE +
"## Specific Time Schedule" + NEW_LINE +
DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE + NEW_LINE +
"## Custom Schedule" +
NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE + NEW_LINE;
protected static final String ALARM_CONDITION_TYPE = "# Alarm condition type (**'spec'**)" + NEW_LINE +
"Alarm condition type can be either simple, duration, or repeating. For example, 5 times in a row or during 5 minutes." + NEW_LINE +
"Note, **'userValue'** field is not used and reserved for future usage, **'dynamicValue'** is used for condition appliance by using the value of the **'sourceAttribute'** " +
"or else **'defaultValue'** is used (if **'sourceAttribute'** is absent).\n" +
"\n**'sourceType'** of the **'sourceAttribute'** can be: \n" +
" * 'CURRENT_DEVICE';\n" +
" * 'CURRENT_CUSTOMER';\n" +
" * 'CURRENT_TENANT'." + NEW_LINE +
"**'sourceAttribute'** can be inherited from the owner if **'inherit'** is set to true (for CURRENT_DEVICE and CURRENT_CUSTOMER)." + NEW_LINE +
"## Repeating alarm condition" + NEW_LINE +
DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE + NEW_LINE +
"## Duration alarm condition" + NEW_LINE +
DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE + NEW_LINE +
"**'unit'** can be: \n" +
" * 'SECONDS';\n" +
" * 'MINUTES';\n" +
" * 'HOURS';\n" +
" * 'DAYS'." + NEW_LINE;
protected static final String PROVISION_CONFIGURATION = "# Provision Configuration" + NEW_LINE +
"There are 3 types of device provision configuration for the device profile: \n" +
" * 'DISABLED';\n" +
" * 'ALLOW_CREATE_NEW_DEVICES';\n" +
" * 'CHECK_PRE_PROVISIONED_DEVICES'." + NEW_LINE +
"Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-provisioning/) for more details." + NEW_LINE;
protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + ALARM_SCHEDULE + ALARM_CONDITION_TYPE +
KEY_FILTERS_DESCRIPTION + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION;
protected static final String DEVICE_PROFILE_ID = "deviceProfileId";
protected static final String ASSET_PROFILE_ID = "assetProfileId";
protected static final String MODEL_DESCRIPTION = "See the 'Model' tab for more details.";
protected static final String ENTITY_VIEW_DESCRIPTION = "Entity Views limit the degree of exposure of the Device or Asset telemetry and attributes to the Customers. " +
"Every Entity View references exactly one entity (device or asset) and defines telemetry and attribute keys that will be visible to the assigned Customer. " +
"As a Tenant Administrator you are able to create multiple EVs per Device or Asset and assign them to different Customers. ";
protected static final String ENTITY_VIEW_INFO_DESCRIPTION = "Entity Views Info extends the Entity View with customer title and 'is public' flag. " + ENTITY_VIEW_DESCRIPTION;
protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'.";
protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'.";
protected static final String ATTRIBUTES_SCOPE_ALLOWED_VALUES = "SERVER_SCOPE, CLIENT_SCOPE, SHARED_SCOPE";
protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details.";
protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys.";
protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest timeseries. For example, 'temperature,humidity'.";
protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility";
protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details.";
protected static final String STRICT_DATA_TYPES_DESCRIPTION = "Enables/disables conversion of telemetry values to strings. Conversion is enabled by default. Set parameter to 'true' in order to disable the conversion.";
protected static final String INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION = "Referencing a non-existing entity Id or invalid entity type will cause an error. ";
protected static final String SAVE_ATTIRIBUTES_STATUS_OK = "Attribute from the request was created or updated. ";
protected static final String INVALID_STRUCTURE_OF_THE_REQUEST = "Invalid structure of the request";
protected static final String SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST = INVALID_STRUCTURE_OF_THE_REQUEST + " or invalid attributes scope provided.";
protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_OK = "Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'.";
protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED = "User is not authorized to save entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant.";
protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace.";
protected static final String SAVE_ENTITY_TIMESERIES_STATUS_OK = "Timeseries from the request was created or updated. " +
"Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED'.";
protected static final String SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED = "User is not authorized to save entity timeseries for selected entity. Most likely, User belongs to different Customer or Tenant.";
protected static final String SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED' that includes an error stacktrace.";
protected static final String ENTITY_ATTRIBUTE_SCOPES = " List of possible attribute scopes depends on the entity type: " +
"\n\n * SERVER_SCOPE - supported for all entity types;" +
"\n * CLIENT_SCOPE - supported for devices;" +
"\n * SHARED_SCOPE - supported for devices. "+ "\n\n";
protected static final String ATTRIBUTE_DATA_EXAMPLE = "[\n" +
" {\"key\": \"stringAttributeKey\", \"value\": \"value\", \"lastUpdateTs\": 1609459200000},\n" +
" {\"key\": \"booleanAttributeKey\", \"value\": false, \"lastUpdateTs\": 1609459200001},\n" +
" {\"key\": \"doubleAttributeKey\", \"value\": 42.2, \"lastUpdateTs\": 1609459200002},\n" +
" {\"key\": \"longKeyExample\", \"value\": 73, \"lastUpdateTs\": 1609459200003},\n" +
" {\"key\": \"jsonKeyExample\",\n" +
" \"value\": {\n" +
" \"someNumber\": 42,\n" +
" \"someArray\": [1,2,3],\n" +
" \"someNestedObject\": {\"key\": \"value\"}\n" +
" },\n" +
" \"lastUpdateTs\": 1609459200004\n" +
" }\n" +
"]";
protected static final String LATEST_TS_STRICT_DATA_EXAMPLE = "{\n" +
" \"stringTsKey\": [{ \"value\": \"value\", \"ts\": 1609459200000}],\n" +
" \"booleanTsKey\": [{ \"value\": false, \"ts\": 1609459200000}],\n" +
" \"doubleTsKey\": [{ \"value\": 42.2, \"ts\": 1609459200000}],\n" +
" \"longTsKey\": [{ \"value\": 73, \"ts\": 1609459200000}],\n" +
" \"jsonTsKey\": [{ \n" +
" \"value\": {\n" +
" \"someNumber\": 42,\n" +
" \"someArray\": [1,2,3],\n" +
" \"someNestedObject\": {\"key\": \"value\"}\n" +
" }, \n" +
" \"ts\": 1609459200000}]\n" +
"}\n";
protected static final String LATEST_TS_NON_STRICT_DATA_EXAMPLE = "{\n" +
" \"stringTsKey\": [{ \"value\": \"value\", \"ts\": 1609459200000}],\n" +
" \"booleanTsKey\": [{ \"value\": \"false\", \"ts\": 1609459200000}],\n" +
" \"doubleTsKey\": [{ \"value\": \"42.2\", \"ts\": 1609459200000}],\n" +
" \"longTsKey\": [{ \"value\": \"73\", \"ts\": 1609459200000}],\n" +
" \"jsonTsKey\": [{ \"value\": \"{\\\"someNumber\\\": 42,\\\"someArray\\\": [1,2,3],\\\"someNestedObject\\\": {\\\"key\\\": \\\"value\\\"}}\", \"ts\": 1609459200000}]\n" +
"}\n";
protected static final String TS_STRICT_DATA_EXAMPLE = "{\n" +
" \"temperature\": [\n" +
" {\n" +
" \"value\": 36.7,\n" +
" \"ts\": 1609459200000\n" +
" },\n" +
" {\n" +
" \"value\": 36.6,\n" +
" \"ts\": 1609459201000\n" +
" }\n" +
" ]\n" +
"}";
protected static final String SAVE_ATTRIBUTES_REQUEST_PAYLOAD = "The request payload is a JSON object with key-value format of attributes to create or update. " +
"For example:\n\n"
+ MARKDOWN_CODE_BLOCK_START
+ "{\n" +
" \"stringKey\":\"value1\", \n" +
" \"booleanKey\":true, \n" +
" \"doubleKey\":42.0, \n" +
" \"longKey\":73, \n" +
" \"jsonKey\": {\n" +
" \"someNumber\": 42,\n" +
" \"someArray\": [1,2,3],\n" +
" \"someNestedObject\": {\"key\": \"value\"}\n" +
" }\n" +
"}"
+ MARKDOWN_CODE_BLOCK_END + "\n";
protected static final String SAVE_TIMESERIES_REQUEST_PAYLOAD = "The request payload is a JSON document with three possible formats:\n\n" +
"Simple format without timestamp. In such a case, current server time will be used: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\"temperature\": 26}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n Single JSON object with timestamp: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n JSON array with timestamps: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"[{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}, {\"ts\":1634712588000,\"values\":{\"temperature\":25, \"humidity\":88}}]" +
MARKDOWN_CODE_BLOCK_END ;
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment