184 changed files with 5161 additions and 504 deletions
@ -0,0 +1,154 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import jakarta.validation.Valid; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.DeleteMapping; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.PutMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.ApiKeyId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
|||
import org.thingsboard.server.config.annotations.ApiOperation; |
|||
import org.thingsboard.server.dao.pat.ApiKeyService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
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.Optional; |
|||
import java.util.UUID; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.API_KEY_ID_PARAM_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.API_KEY_TEXT_SEARCH_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; |
|||
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_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; |
|||
import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION; |
|||
|
|||
@RestController |
|||
@TbCoreComponent |
|||
@Slf4j |
|||
@RequestMapping("/api") |
|||
@RequiredArgsConstructor |
|||
public class ApiKeyController extends BaseController { |
|||
|
|||
private final ApiKeyService apiKeyService; |
|||
|
|||
@ApiOperation(value = "Save API key for user (saveApiKey)", |
|||
notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey <value>'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@PostMapping(value = "/apiKey") |
|||
public ApiKey saveApiKey( |
|||
@Parameter(description = "A JSON value representing the Api Key token.") |
|||
@RequestBody @Valid ApiKeyInfo apiKeyInfo) throws ThingsboardException { |
|||
User user = checkUserId(apiKeyInfo.getUserId(), Operation.WRITE); |
|||
apiKeyInfo.setTenantId(user.getTenantId()); |
|||
checkEntity(apiKeyInfo.getId(), apiKeyInfo, Resource.API_KEY); |
|||
return checkNotNull(apiKeyService.saveApiKey(apiKeyInfo.getTenantId(), apiKeyInfo)); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get User Api Keys (getUserApiKeys)", |
|||
notes = "Returns a page of api keys owned by user. " + |
|||
PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@GetMapping(value = "/apiKeys/{userId}") |
|||
public PageData<ApiKeyInfo> getUserApiKeys( |
|||
@Parameter(description = USER_ID_PARAM_DESCRIPTION) |
|||
@PathVariable("userId") String userIdStr, |
|||
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) |
|||
@RequestParam int pageSize, |
|||
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) |
|||
@RequestParam int page, |
|||
@Parameter(description = API_KEY_TEXT_SEARCH_DESCRIPTION) |
|||
@RequestParam(required = false) String textSearch, |
|||
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "expirationTime", "description", "enabled"})) |
|||
@RequestParam(required = false) String sortProperty, |
|||
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) |
|||
@RequestParam(required = false) String sortOrder) throws ThingsboardException { |
|||
SecurityUser securityUser = getCurrentUser(); |
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
UserId userId = new UserId(toUUID(userIdStr)); |
|||
accessControlService.checkPermission(securityUser, Resource.API_KEY, Operation.READ); |
|||
User user = checkUserId(userId, Operation.READ); |
|||
return apiKeyService.findApiKeysByUserId(user.getTenantId(), userId, pageLink); |
|||
} |
|||
|
|||
@ApiOperation(value = "Update API key Description", |
|||
notes = "Updates the description of the existing API key by apiKeyId. " + |
|||
"Only the description can be updated. " + |
|||
"Referencing a non-existing ApiKey Id will cause a 'Not Found' error." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@PutMapping("/apiKey/{id}/description") |
|||
public ApiKeyInfo updateApiKeyDescription( |
|||
@Parameter(description = API_KEY_ID_PARAM_DESCRIPTION, required = true) |
|||
@PathVariable UUID id, |
|||
@Parameter(description = "New description for the API key", example = "Description") |
|||
@RequestBody Optional<String> description) throws Exception { |
|||
ApiKeyId apiKeyId = new ApiKeyId(id); |
|||
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.WRITE); |
|||
checkUserId(apiKey.getUserId(), Operation.WRITE); |
|||
apiKey.setDescription(description.orElse(null)); |
|||
return apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey); |
|||
} |
|||
|
|||
@ApiOperation(value = "Enable or disable API key (enableApiKey)", |
|||
notes = "Updates api key with enabled = true/false. " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@PutMapping(value = "/apiKey/{id}/enabled/{enabledValue}") |
|||
public ApiKeyInfo enableApiKey( |
|||
@Parameter(description = "Unique identifier of the API key to enable/disable", required = true) |
|||
@PathVariable UUID id, |
|||
@Parameter(description = "Enabled or disabled api key", required = true) |
|||
@PathVariable(value = "enabledValue") Boolean enabledValue) throws ThingsboardException { |
|||
ApiKeyId apiKeyId = new ApiKeyId(id); |
|||
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.WRITE); |
|||
checkUserId(apiKey.getUserId(), Operation.WRITE); |
|||
apiKey.setEnabled(enabledValue); |
|||
return apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey); |
|||
} |
|||
|
|||
@ApiOperation(value = "Delete API key by ID (deleteApiKey)", |
|||
notes = "Deletes the API key. Referencing non-existing ApiKey Id will cause an error." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
@DeleteMapping(value = "/apiKey/{id}") |
|||
public void deleteApiKey(@PathVariable UUID id) throws ThingsboardException { |
|||
ApiKeyId apiKeyId = new ApiKeyId(id); |
|||
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.DELETE); |
|||
checkUserId(apiKey.getUserId(), Operation.WRITE); |
|||
apiKeyService.deleteApiKey(apiKey.getTenantId(), apiKey, false); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.extractor; |
|||
|
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; |
|||
|
|||
@Component(value = "apiKeyHeaderTokenExtractor") |
|||
public class ApiKeyHeaderTokenExtractor extends AbstractHeaderTokenExtractor { |
|||
|
|||
public ApiKeyHeaderTokenExtractor() { |
|||
super(API_KEY_HEADER_PREFIX); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.extractor; |
|||
|
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.BEARER_HEADER_PREFIX; |
|||
|
|||
@Component(value = "jwtHeaderTokenExtractor") |
|||
public class JwtHeaderTokenExtractor extends AbstractHeaderTokenExtractor { |
|||
|
|||
public JwtHeaderTokenExtractor() { |
|||
super(BEARER_HEADER_PREFIX); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.pat; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.security.authentication.BadCredentialsException; |
|||
import org.springframework.security.authentication.CredentialsExpiredException; |
|||
import org.springframework.security.authentication.DisabledException; |
|||
import org.springframework.security.authentication.InsufficientAuthenticationException; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.UserAuthDetails; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.dao.pat.ApiKeyService; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.model.UserPrincipal; |
|||
import org.thingsboard.server.service.security.model.token.RawApiKey; |
|||
import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class ApiKeyAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider { |
|||
|
|||
private final ApiKeyService apiKeyService; |
|||
private final UserAuthDetailsCache userAuthDetailsCache; |
|||
|
|||
@Override |
|||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
|||
RawApiKey rawApiKey = (RawApiKey) authentication.getCredentials(); |
|||
SecurityUser securityUser = authenticate(rawApiKey.apiKey()); |
|||
return new ApiKeyAuthenticationToken(securityUser); |
|||
} |
|||
|
|||
@Override |
|||
public boolean supports(Class<?> authentication) { |
|||
return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication); |
|||
} |
|||
|
|||
private SecurityUser authenticate(String key) { |
|||
if (StringUtils.isEmpty(key)) { |
|||
throw new BadCredentialsException("Empty API key"); |
|||
} |
|||
ApiKey apiKey = apiKeyService.findApiKeyByValue(key); |
|||
if (apiKey == null) { |
|||
throw new BadCredentialsException("User not found for the provided API key"); |
|||
} |
|||
if (!apiKey.isEnabled()) { |
|||
throw new DisabledException("API key auth is not active"); |
|||
} |
|||
if (apiKey.getExpirationTime() != 0 && apiKey.getExpirationTime() < System.currentTimeMillis()) { |
|||
throw new CredentialsExpiredException("API key is expired"); |
|||
} |
|||
UserAuthDetails userAuthDetails = userAuthDetailsCache.getUserAuthDetails(apiKey.getTenantId(), apiKey.getUserId()); |
|||
if (userAuthDetails == null) { |
|||
throw new UsernameNotFoundException("User with credentials not found"); |
|||
} |
|||
if (!userAuthDetails.credentialsEnabled()) { |
|||
throw new DisabledException("User is not active"); |
|||
} |
|||
|
|||
User user = userAuthDetails.user(); |
|||
if (user.getAuthority() == null) { |
|||
throw new InsufficientAuthenticationException("User has no authority assigned"); |
|||
} |
|||
UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); |
|||
return new SecurityUser(user, true, userPrincipal); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.pat; |
|||
|
|||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.model.token.RawApiKey; |
|||
|
|||
import java.io.Serial; |
|||
|
|||
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 2978710889397403536L; |
|||
|
|||
private RawApiKey rawApiKey; |
|||
private SecurityUser securityUser; |
|||
|
|||
public ApiKeyAuthenticationToken(RawApiKey rawApiKey) { |
|||
super(null); |
|||
this.rawApiKey = rawApiKey; |
|||
setAuthenticated(false); |
|||
} |
|||
|
|||
public ApiKeyAuthenticationToken(SecurityUser securityUser) { |
|||
super(securityUser.getAuthorities()); |
|||
this.eraseCredentials(); |
|||
this.securityUser = securityUser; |
|||
super.setAuthenticated(true); |
|||
} |
|||
|
|||
@Override |
|||
public Object getCredentials() { |
|||
return rawApiKey; |
|||
} |
|||
|
|||
@Override |
|||
public Object getPrincipal() { |
|||
return this.securityUser; |
|||
} |
|||
|
|||
@Override |
|||
public void eraseCredentials() { |
|||
super.eraseCredentials(); |
|||
this.rawApiKey = null; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.pat; |
|||
|
|||
import jakarta.servlet.FilterChain; |
|||
import jakarta.servlet.ServletException; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.core.context.SecurityContext; |
|||
import org.springframework.security.core.context.SecurityContextHolder; |
|||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; |
|||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
|||
import org.springframework.security.web.util.matcher.RequestMatcher; |
|||
import org.thingsboard.server.service.security.auth.extractor.TokenExtractor; |
|||
import org.thingsboard.server.service.security.model.token.RawApiKey; |
|||
|
|||
import java.io.IOException; |
|||
|
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; |
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER; |
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2; |
|||
|
|||
public class ApiKeyTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { |
|||
|
|||
private final AuthenticationFailureHandler failureHandler; |
|||
private final TokenExtractor tokenExtractor; |
|||
|
|||
@Autowired |
|||
public ApiKeyTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, |
|||
@Qualifier("apiKeyHeaderTokenExtractor") TokenExtractor tokenExtractor, RequestMatcher matcher) { |
|||
super(matcher); |
|||
this.failureHandler = failureHandler; |
|||
this.tokenExtractor = tokenExtractor; |
|||
} |
|||
|
|||
@Override |
|||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { |
|||
RawApiKey rawApiKey = new RawApiKey(tokenExtractor.extract(request)); |
|||
return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(rawApiKey)); |
|||
} |
|||
|
|||
@Override |
|||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, |
|||
Authentication authResult) throws IOException, ServletException { |
|||
SecurityContext context = SecurityContextHolder.createEmptyContext(); |
|||
context.setAuthentication(authResult); |
|||
SecurityContextHolder.setContext(context); |
|||
chain.doFilter(request, response); |
|||
} |
|||
|
|||
@Override |
|||
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { |
|||
if (!super.requiresAuthentication(request, response)) { |
|||
return false; |
|||
} |
|||
String header = request.getHeader(AUTHORIZATION_HEADER); |
|||
if (header == null) { |
|||
header = request.getHeader(AUTHORIZATION_HEADER_V2); |
|||
} |
|||
return header != null && header.startsWith(API_KEY_HEADER_PREFIX); |
|||
} |
|||
|
|||
@Override |
|||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, |
|||
AuthenticationException failed) throws IOException, ServletException { |
|||
SecurityContextHolder.clearContext(); |
|||
failureHandler.onAuthenticationFailure(request, response, failed); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.model.token; |
|||
|
|||
public record RawApiKey(String apiKey) {} |
|||
@ -0,0 +1,56 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.ttl; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
|||
import org.springframework.scheduling.annotation.Scheduled; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.dao.pat.ApiKeyDao; |
|||
import org.thingsboard.server.queue.discovery.PartitionService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
@Slf4j |
|||
@Service |
|||
@TbCoreComponent |
|||
@ConditionalOnExpression("${sql.ttl.api_keys.enabled:true} && ${sql.ttl.api_keys.ttl:0} > 0") |
|||
public class ApiKeysCleanUpService extends AbstractCleanUpService { |
|||
|
|||
public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION = |
|||
"#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.api_keys.checking_interval_ms})}"; |
|||
|
|||
private final ApiKeyDao apiKeyDao; |
|||
|
|||
public ApiKeysCleanUpService(PartitionService partitionService, ApiKeyDao apiKeyDao) { |
|||
super(partitionService); |
|||
this.apiKeyDao = apiKeyDao; |
|||
} |
|||
|
|||
@Scheduled( |
|||
initialDelayString = RANDOM_DELAY_INTERVAL_MS_EXPRESSION, |
|||
fixedDelayString = "${sql.ttl.api_keys.checking_interval_ms:86400000}" |
|||
) |
|||
public void cleanUp() { |
|||
long threshold = System.currentTimeMillis(); |
|||
if (isSystemTenantPartitionMine()) { |
|||
int deleted = apiKeyDao.deleteAllByExpirationTimeBefore(threshold); |
|||
if (deleted > 0) { |
|||
log.info("API key cleanup removed {} keys (thresholdTs={})", deleted, threshold); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.user.cache; |
|||
|
|||
import com.github.benmanes.caffeine.cache.Cache; |
|||
import com.github.benmanes.caffeine.cache.Caffeine; |
|||
import jakarta.annotation.PostConstruct; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.context.event.EventListener; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.UserAuthDetails; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Slf4j |
|||
@Service |
|||
@TbCoreComponent |
|||
@RequiredArgsConstructor |
|||
public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache { |
|||
|
|||
private final UserService userService; |
|||
|
|||
@Value("${cache.userAuthDetails.maxSize:1000}") |
|||
private int cacheMaxSize; |
|||
@Value("${cache.userAuthDetails.timeToLiveInMinutes:30}") |
|||
private int cacheValueTtl; |
|||
private Cache<UserId, UserAuthDetails> cache; |
|||
|
|||
@PostConstruct |
|||
private void init() { |
|||
cache = Caffeine.newBuilder() |
|||
.maximumSize(cacheMaxSize) |
|||
.expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) |
|||
.build(); |
|||
} |
|||
|
|||
@EventListener(ComponentLifecycleMsg.class) |
|||
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) { |
|||
if (event.getEntityId() != null) { |
|||
if (event.getEntityId().getEntityType() == EntityType.USER) { |
|||
evict(new UserId(event.getEntityId().getId())); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public UserAuthDetails getUserAuthDetails(TenantId tenantId, UserId userId) { |
|||
log.trace("Retrieving user with enabled credentials status for id {} for tenant {} from cache", userId, tenantId); |
|||
return cache.get(userId, id -> userService.findUserAuthDetailsByUserId(tenantId, id)); |
|||
} |
|||
|
|||
public void evict(UserId userId) { |
|||
cache.invalidate(userId); |
|||
log.trace("Evicted record for user {} from cache", userId); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.user.cache; |
|||
|
|||
import org.thingsboard.server.common.data.UserAuthDetails; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
|
|||
public interface UserAuthDetailsCache { |
|||
|
|||
UserAuthDetails getUserAuthDetails(TenantId tenantId, UserId userId); |
|||
|
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.core.type.TypeReference; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
|
|||
@DaoSqlTest |
|||
public class ApiKeyControllerTest extends AbstractControllerTest { |
|||
|
|||
@Before |
|||
public void setUp() throws Exception { |
|||
loginTenantAdmin(); |
|||
} |
|||
|
|||
@Test |
|||
public void testSaveApiKey() throws Exception { |
|||
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("New API key description", true); |
|||
|
|||
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
|||
|
|||
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
|||
Assert.assertEquals(1, pageData.getData().size()); |
|||
|
|||
ApiKeyInfo savedApiKey = pageData.getData().get(0); |
|||
Assert.assertNotNull(savedApiKey); |
|||
Assert.assertEquals(apiKeyInfo.getDescription(), savedApiKey.getDescription()); |
|||
Assert.assertEquals(apiKeyInfo.isEnabled(), savedApiKey.isEnabled()); |
|||
Assert.assertEquals(tenantId, savedApiKey.getTenantId()); |
|||
Assert.assertEquals(tenantAdminUser.getId(), savedApiKey.getUserId()); |
|||
|
|||
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void tesFindUserApiKeys() throws Exception { |
|||
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
|||
Assert.assertTrue(pageData.getData().isEmpty()); |
|||
|
|||
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); |
|||
int expectedSize = 10; |
|||
for (int i = 0; i < expectedSize; i++) { |
|||
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
|||
} |
|||
|
|||
PageData<ApiKeyInfo> pageData2 = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
|||
Assert.assertEquals(expectedSize, pageData2.getData().size()); |
|||
|
|||
pageData2.getData().forEach(apiKey -> { |
|||
try { |
|||
doDelete("/api/apiKey/" + apiKey.getId()).andExpect(status().isOk()); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Test |
|||
public void testUpdateApiKeyDescription() throws Exception { |
|||
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); |
|||
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
|||
|
|||
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
|||
Assert.assertEquals(1, pageData.getData().size()); |
|||
|
|||
ApiKeyInfo savedApiKey = pageData.getData().get(0); |
|||
|
|||
String newDescription = "Updated API Key Description"; |
|||
|
|||
ApiKeyInfo updatedApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/description", newDescription, ApiKeyInfo.class); |
|||
Assert.assertNotNull(updatedApiKeyInfo); |
|||
Assert.assertEquals(newDescription, updatedApiKeyInfo.getDescription()); |
|||
|
|||
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testEnableApiKey() throws Exception { |
|||
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); |
|||
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
|||
|
|||
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
|||
Assert.assertEquals(1, pageData.getData().size()); |
|||
|
|||
ApiKeyInfo savedApiKey = pageData.getData().get(0); |
|||
|
|||
ApiKeyInfo disabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/false", Boolean.FALSE, ApiKeyInfo.class); |
|||
Assert.assertNotNull(disabledApiKeyInfo); |
|||
Assert.assertFalse(disabledApiKeyInfo.isEnabled()); |
|||
|
|||
ApiKeyInfo enabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/true", Boolean.TRUE, ApiKeyInfo.class); |
|||
Assert.assertNotNull(enabledApiKeyInfo); |
|||
Assert.assertTrue(enabledApiKeyInfo.isEnabled()); |
|||
|
|||
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testDeleteApiKey() throws Exception { |
|||
doDelete("/api/apiKey/" + UUID.randomUUID()).andExpect(status().isNotFound()); |
|||
|
|||
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", false); |
|||
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
|||
|
|||
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
|||
Assert.assertEquals(1, pageData.getData().size()); |
|||
ApiKeyInfo savedApiKey = pageData.getData().get(0); |
|||
|
|||
doDelete("/api/apiKey/" + savedApiKey.getId().getId()).andExpect(status().isOk()); |
|||
} |
|||
|
|||
private ApiKeyInfo constructApiKeyInfo(String description, boolean enabled) { |
|||
ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); |
|||
apiKeyInfo.setDescription(description); |
|||
apiKeyInfo.setEnabled(enabled); |
|||
apiKeyInfo.setUserId(tenantAdminUserId); |
|||
return apiKeyInfo; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.pat; |
|||
|
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.mockito.Mockito; |
|||
import org.thingsboard.server.common.data.audit.ActionType; |
|||
import org.thingsboard.server.common.data.edge.Edge; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
|||
import org.thingsboard.server.controller.AbstractControllerTest; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; |
|||
|
|||
@DaoSqlTest |
|||
public class ApiKeyAuthenticationProviderTest extends AbstractControllerTest { |
|||
|
|||
ApiKey savedApiKey; |
|||
|
|||
@Before |
|||
public void setUp() throws Exception { |
|||
loginTenantAdmin(); |
|||
|
|||
ApiKeyInfo apiKeyInfo = constructApiKeyInfo(); |
|||
savedApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
|||
setApiKey(savedApiKey.getValue()); |
|||
} |
|||
|
|||
@After |
|||
public void cleanUp() throws Exception { |
|||
resetApiKey(); |
|||
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testSaveEdgeWithApiKey() throws Exception { |
|||
Edge edge = constructEdge("My edge", "default"); |
|||
|
|||
Mockito.reset(tbClusterService, auditLogService); |
|||
|
|||
Edge savedEdge = doPostWithApiKey("/api/edge", edge, Edge.class); |
|||
|
|||
Assert.assertNotNull(savedEdge); |
|||
Assert.assertNotNull(savedEdge.getId()); |
|||
Assert.assertTrue(savedEdge.getCreatedTime() > 0); |
|||
Assert.assertEquals(tenantId, savedEdge.getTenantId()); |
|||
Assert.assertNotNull(savedEdge.getCustomerId()); |
|||
Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); |
|||
Assert.assertEquals(edge.getName(), savedEdge.getName()); |
|||
|
|||
testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), |
|||
tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), |
|||
ActionType.ADDED, 2); |
|||
|
|||
savedEdge.setName("My new edge"); |
|||
doPostWithApiKey("/api/edge", savedEdge, Edge.class); |
|||
|
|||
Edge foundEdge = doGetWithApiKey("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); |
|||
Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); |
|||
|
|||
testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), |
|||
tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), |
|||
ActionType.UPDATED, 1); |
|||
|
|||
doDeleteWithApiKey("/api/edge/" + savedEdge.getId().getId().toString()) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testUnauthorizedWhenKeyDisabled() throws Exception { |
|||
ApiKeyInfo disabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/false", Boolean.FALSE, ApiKeyInfo.class); |
|||
Assert.assertFalse(disabledApiKeyInfo.isEnabled()); |
|||
doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); |
|||
} |
|||
|
|||
@Test |
|||
public void testUnauthorizedWhenKeyExpired() throws Exception { |
|||
ApiKeyInfo apiKeyInfo = constructApiKeyInfo(); |
|||
apiKeyInfo.setExpirationTime(System.currentTimeMillis() - 1000); |
|||
ApiKey savedApiKeyWithBad = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
|||
setApiKey(savedApiKeyWithBad.getValue()); |
|||
doPost("/api/apiKey", savedApiKey, ApiKeyInfo.class); |
|||
doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); |
|||
} |
|||
|
|||
private ApiKeyInfo constructApiKeyInfo() { |
|||
ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); |
|||
apiKeyInfo.setDescription("New API key description"); |
|||
apiKeyInfo.setEnabled(true); |
|||
apiKeyInfo.setUserId(tenantAdminUserId); |
|||
return apiKeyInfo; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.dao.pat; |
|||
|
|||
import org.thingsboard.server.common.data.id.ApiKeyId; |
|||
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.PageLink; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
|||
import org.thingsboard.server.dao.entity.EntityDaoService; |
|||
|
|||
public interface ApiKeyService extends EntityDaoService { |
|||
|
|||
ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKey); |
|||
|
|||
void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force); |
|||
|
|||
void deleteByUserId(TenantId tenantId, UserId userId); |
|||
|
|||
ApiKey findApiKeyByValue(String value); |
|||
|
|||
ApiKey findApiKeyById(TenantId tenantId, ApiKeyId apiKeyId); |
|||
|
|||
PageData<ApiKeyInfo> findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink); |
|||
|
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data; |
|||
|
|||
public record UserAuthDetails(User user, boolean credentialsEnabled) {} |
|||
@ -0,0 +1,36 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.alarm; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
public enum AlarmCommentSubType { |
|||
|
|||
ACKED_BY_USER("Alarm was acknowledged by user %s"), |
|||
CLEARED_BY_USER("Alarm was cleared by user %s"), |
|||
ASSIGNED_TO_USER("Alarm was assigned by user %s to user %s"), |
|||
UNASSIGNED_BY_USER("Alarm was unassigned by user %s"), |
|||
UNASSIGNED_FROM_DELETED_USER("Alarm was unassigned because user %s - was deleted"), |
|||
COMMENT_DELETED("User %s deleted his comment"), |
|||
SEVERITY_CHANGED("Alarm severity was updated from %s to %s"); |
|||
|
|||
@Getter |
|||
private final String text; |
|||
|
|||
AlarmCommentSubType(String text) { |
|||
this.text = text; |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
public class AttributesImmediateOutputStrategy implements AttributesOutputStrategy { |
|||
|
|||
private boolean updateAttributesOnlyOnValueChange; |
|||
|
|||
private boolean saveAttribute; |
|||
private boolean sendWsUpdate; |
|||
private boolean processCfs; |
|||
|
|||
@Override |
|||
public OutputStrategyType getType() { |
|||
return OutputStrategyType.IMMEDIATE; |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.AttributeScope; |
|||
|
|||
@Data |
|||
public class AttributesOutput implements Output { |
|||
|
|||
private String name; |
|||
private AttributeScope scope; |
|||
private Integer decimalsByDefault; |
|||
|
|||
private AttributesOutputStrategy strategy; |
|||
|
|||
public AttributesOutput() { |
|||
this.strategy = new AttributesRuleChainOutputStrategy(); |
|||
} |
|||
|
|||
@Override |
|||
public OutputType getType() { |
|||
return OutputType.ATTRIBUTES; |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
|||
import com.fasterxml.jackson.annotation.JsonSubTypes; |
|||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
|||
|
|||
@JsonIgnoreProperties(ignoreUnknown = true) |
|||
@JsonTypeInfo( |
|||
use = JsonTypeInfo.Id.NAME, |
|||
include = JsonTypeInfo.As.PROPERTY, |
|||
property = "type" |
|||
) |
|||
@JsonSubTypes({ |
|||
@JsonSubTypes.Type(value = AttributesImmediateOutputStrategy.class, name = "IMMEDIATE"), |
|||
@JsonSubTypes.Type(value = AttributesRuleChainOutputStrategy.class, name = "RULE_CHAIN"), |
|||
}) |
|||
public interface AttributesOutputStrategy extends OutputStrategy { |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@NoArgsConstructor |
|||
public class AttributesRuleChainOutputStrategy implements AttributesOutputStrategy { |
|||
|
|||
@Override |
|||
public OutputStrategyType getType() { |
|||
return OutputStrategyType.RULE_CHAIN; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
|
|||
public interface OutputStrategy { |
|||
|
|||
@JsonIgnore |
|||
OutputStrategyType getType(); |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
public enum OutputStrategyType { |
|||
|
|||
IMMEDIATE, RULE_CHAIN |
|||
|
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
public class TimeSeriesImmediateOutputStrategy implements TimeSeriesOutputStrategy { |
|||
|
|||
private long ttl; |
|||
|
|||
private boolean saveTimeSeries; |
|||
private boolean saveLatest; |
|||
private boolean sendWsUpdate; |
|||
private boolean processCfs; |
|||
|
|||
@Override |
|||
public OutputStrategyType getType() { |
|||
return OutputStrategyType.IMMEDIATE; |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class TimeSeriesOutput implements Output { |
|||
|
|||
private String name; |
|||
private Integer decimalsByDefault; |
|||
|
|||
private TimeSeriesOutputStrategy strategy; |
|||
|
|||
public TimeSeriesOutput() { |
|||
this.strategy = new TimeSeriesRuleChainOutputStrategy(); |
|||
} |
|||
|
|||
@Override |
|||
public OutputType getType() { |
|||
return OutputType.TIME_SERIES; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonSubTypes; |
|||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
|||
|
|||
@JsonTypeInfo( |
|||
use = JsonTypeInfo.Id.NAME, |
|||
include = JsonTypeInfo.As.PROPERTY, |
|||
property = "type" |
|||
) |
|||
@JsonSubTypes({ |
|||
@JsonSubTypes.Type(value = TimeSeriesImmediateOutputStrategy.class, name = "IMMEDIATE"), |
|||
@JsonSubTypes.Type(value = TimeSeriesRuleChainOutputStrategy.class, name = "RULE_CHAIN") |
|||
}) |
|||
public interface TimeSeriesOutputStrategy extends OutputStrategy { |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.cf.configuration; |
|||
|
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@Data |
|||
@NoArgsConstructor |
|||
public class TimeSeriesRuleChainOutputStrategy implements TimeSeriesOutputStrategy { |
|||
|
|||
@Override |
|||
public OutputStrategyType getType() { |
|||
return OutputStrategyType.RULE_CHAIN; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.id; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
|
|||
import java.io.Serial; |
|||
import java.util.UUID; |
|||
|
|||
public class ApiKeyId extends UUIDBased implements EntityId { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = -273913539653684641L; |
|||
|
|||
@JsonCreator |
|||
public ApiKeyId(@JsonProperty("id") UUID id) { |
|||
super(id); |
|||
} |
|||
|
|||
public static ApiKeyId fromString(String secretId) { |
|||
return new ApiKeyId(UUID.fromString(secretId)); |
|||
} |
|||
|
|||
@Override |
|||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_KEY", allowableValues = "API_KEY") |
|||
public EntityType getEntityType() { |
|||
return EntityType.API_KEY; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.pat; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.thingsboard.server.common.data.id.ApiKeyId; |
|||
import org.thingsboard.server.common.data.validation.NoXss; |
|||
|
|||
import java.io.Serial; |
|||
|
|||
@Schema |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class ApiKey extends ApiKeyInfo { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = -2313196723950490263L; |
|||
|
|||
@NoXss |
|||
@Schema(description = "Api key value", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private String value; |
|||
|
|||
public ApiKey() { |
|||
super(); |
|||
} |
|||
|
|||
public ApiKey(ApiKeyId id) { |
|||
super(id); |
|||
} |
|||
|
|||
public ApiKey(ApiKey apiKey) { |
|||
super(apiKey); |
|||
this.value = apiKey.getValue(); |
|||
} |
|||
|
|||
public ApiKey(ApiKeyInfo apiKeyInfo) { |
|||
super(apiKeyInfo); |
|||
this.value = null; |
|||
} |
|||
|
|||
public ApiKey(ApiKeyInfo apiKeyInfo, String value) { |
|||
super(apiKeyInfo); |
|||
this.value = value; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.pat; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.thingsboard.server.common.data.BaseData; |
|||
import org.thingsboard.server.common.data.HasTenantId; |
|||
import org.thingsboard.server.common.data.id.ApiKeyId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.validation.Length; |
|||
import org.thingsboard.server.common.data.validation.NoXss; |
|||
|
|||
import java.io.Serial; |
|||
|
|||
@Schema |
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class ApiKeyInfo extends BaseData<ApiKeyId> implements HasTenantId { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = -2313196723950490263L; |
|||
|
|||
@Schema(description = "JSON object with Tenant Id. Tenant Id of the api key cannot be changed.", accessMode = Schema.AccessMode.READ_ONLY) |
|||
private TenantId tenantId; |
|||
|
|||
@Schema(description = "JSON object with User Id. User Id of the api key cannot be changed.") |
|||
private UserId userId; |
|||
|
|||
@Schema(description = "Expiration time of the api key.") |
|||
private long expirationTime; |
|||
|
|||
@NoXss |
|||
@NotBlank |
|||
@Length(fieldName = "description") |
|||
@Schema(description = "Api Key description.", example = "Api Key description") |
|||
private String description; |
|||
|
|||
@Schema(description = "Enabled/disabled api key.", example = "true") |
|||
private boolean enabled; |
|||
|
|||
@JsonProperty(access = JsonProperty.Access.READ_ONLY) |
|||
@Schema(description = "Indicates if the api key is expired based on current time. Returns false if expirationTime is 0 (no expiry).", |
|||
example = "false", |
|||
accessMode = Schema.AccessMode.READ_ONLY) |
|||
public boolean isExpired() { |
|||
if (expirationTime == 0) { |
|||
return false; |
|||
} |
|||
return System.currentTimeMillis() > expirationTime; |
|||
} |
|||
|
|||
@Schema(description = "JSON object with the Api Key Id. " + |
|||
"Specify this field to update the Api Key. " + |
|||
"Referencing non-existing Api Key Id will cause error. " + |
|||
"Omit this field to create new Api Key.") |
|||
@Override |
|||
public ApiKeyId getId() { |
|||
return super.getId(); |
|||
} |
|||
|
|||
public ApiKeyInfo() { |
|||
super(); |
|||
} |
|||
|
|||
public ApiKeyInfo(ApiKeyId id) { |
|||
super(id); |
|||
} |
|||
|
|||
public ApiKeyInfo(ApiKeyInfo apiKeyInfo) { |
|||
super(apiKeyInfo); |
|||
this.tenantId = apiKeyInfo.getTenantId(); |
|||
this.userId = apiKeyInfo.getUserId(); |
|||
this.expirationTime = apiKeyInfo.getExpirationTime(); |
|||
this.enabled = apiKeyInfo.isEnabled(); |
|||
this.description = apiKeyInfo.getDescription(); |
|||
} |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue