Browse Source

Add Sysadmin to controller authority, minor changes, add cleanup service to delete expired api keys

pull/14074/head
Andrii Landiak 1 year ago
parent
commit
399def92de
  1. 23
      application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java
  2. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/extractor/ApiKeyHeaderTokenExtractor.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java
  4. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java
  5. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  7. 62
      application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java
  8. 4
      application/src/main/resources/thingsboard.yml
  9. 1
      application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java
  10. 2
      application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java
  11. 7
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  12. 2
      common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKeyInfo.java
  13. 2
      dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java
  14. 4
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java
  15. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyInfoRepository.java
  16. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java
  17. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java
  18. 2
      dao/src/main/resources/sql/schema-entities.sql
  19. 2
      dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java

23
application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java

@ -51,7 +51,7 @@ import static org.thingsboard.server.controller.ControllerConstants.API_KEY_ID_P
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.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION;
@RestController
@ -64,23 +64,22 @@ 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 <hash>'." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey <hash>'." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@PostMapping(value = "/apiKey")
public String saveApiKey(
@Parameter(description = "A JSON value representing the Api Key token.")
@RequestBody @Valid ApiKeyInfo apiKeyInfo) throws ThingsboardException {
SecurityUser securityUser = getCurrentUser();
apiKeyInfo.setTenantId(securityUser.getTenantId());
apiKeyInfo.setUserId(securityUser.getId());
checkEntity(apiKeyInfo.getId(), apiKeyInfo, Resource.API_KEY);
return toUserApiKey(checkNotNull(apiKeyService.saveApiKey(securityUser.getTenantId(), apiKeyInfo)).getHash());
}
@ApiOperation(value = "Get User Api Keys (getUserApiKeys)",
notes = "Returns a page of api keys owned by user. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@GetMapping(value = "/apiKeys/{userId}")
public PageData<ApiKeyInfo> getUserApiKeys(
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@ -99,8 +98,8 @@ public class ApiKeyController extends BaseController {
@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." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
"Referencing a non-existing ApiKey Id will cause a 'Not Found' error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@PutMapping("/apiKey/{id}/description")
public ApiKeyInfo updateApiKeyDescription(
@Parameter(description = API_KEY_ID_PARAM_DESCRIPTION, required = true)
@ -114,8 +113,8 @@ public class ApiKeyController extends BaseController {
}
@ApiOperation(value = "Enable or disable API key (enableApiKey)",
notes = "Updates api key with enabled = true/false. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
notes = "Updates api key with enabled = true/false. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@PutMapping(value = "/apiKey/{id}/enabled/{enabledValue}")
public ApiKeyInfo enableApiKey(
@Parameter(description = "Unique identifier of the API key to enable/disable", required = true)
@ -129,8 +128,8 @@ public class ApiKeyController extends BaseController {
}
@ApiOperation(value = "Delete API key by ID (deleteApiKey)",
notes = "Deletes the API key. Referencing non-existing ApiKey Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
notes = "Deletes the API key. Referencing non-existing ApiKey Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@DeleteMapping(value = "/apiKey/{id}")
public void deleteApiKey(@PathVariable UUID id) throws ThingsboardException {
ApiKeyId apiKeyId = new ApiKeyId(id);

3
application/src/main/java/org/thingsboard/server/service/security/auth/extractor/ApiKeyHeaderTokenExtractor.java

@ -20,8 +20,7 @@ import org.springframework.stereotype.Component;
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX;
@Component
@Qualifier("apiKeyHeaderTokenExtractor")
@Component(value = "apiKeyHeaderTokenExtractor")
public class ApiKeyHeaderTokenExtractor extends AbstractHeaderTokenExtractor {
public ApiKeyHeaderTokenExtractor() {

2
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java

@ -22,7 +22,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtSettings;
/**
* During Install or upgrade the validation is suppressed to keep existing data
* During Install or upgrade, the validation is suppressed to keep existing data
* */
@Primary
@Profile("install")

5
application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java

@ -17,6 +17,7 @@ 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;
@ -60,13 +61,13 @@ public class ApiKeyAuthenticationProvider implements org.springframework.securit
}
ApiKey apiKey = apiKeyService.findApiKeyByHash(key);
if (apiKey == null) {
throw new UsernameNotFoundException("User not found for the provided API key");
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 BadCredentialsException("API key is expired");
throw new CredentialsExpiredException("API key is expired");
}
TenantId tenantId = apiKey.getTenantId();
UserId userId = apiKey.getUserId();

1
application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java

@ -45,6 +45,7 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.QUEUE, systemEntityPermissionChecker);
put(Resource.NOTIFICATION, systemEntityPermissionChecker);
put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker);
put(Resource.API_KEY, systemEntityPermissionChecker);
}
private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() {

2
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -176,7 +176,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
@Override
public boolean hasPermission(SecurityUser user, Operation operation, ApiKeyId entityId, ApiKeyInfo entity) {
return user.getTenantId().equals(entity.getTenantId());
return user.getId().equals(entity.getUserId());
}
};

62
application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java

@ -0,0 +1,62 @@
/**
* 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.beans.factory.annotation.Value;
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;
import java.util.concurrent.TimeUnit;
@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})}";
@Value("${sql.ttl.api_keys.ttl:2592000}")
private long ttl;
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() - TimeUnit.SECONDS.toMillis(ttl);
if (isSystemTenantPartitionMine()) {
int deleted = apiKeyDao.deleteAllByExpirationTimeBefore(threshold);
if (deleted > 0) {
log.info("API key cleanup removed {} keys (thresholdTs={})", deleted, threshold);
}
}
}
}

4
application/src/main/resources/thingsboard.yml

@ -427,6 +427,10 @@ sql:
enabled: "${SQL_TTL_NOTIFICATIONS_ENABLED:true}" # Enable/disable TTL (Time To Live) for notification center records
ttl: "${SQL_TTL_NOTIFICATIONS_SECS:2592000}" # Default value - 30 days
checking_interval_ms: "${SQL_TTL_NOTIFICATIONS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
api_keys:
enabled: "${SQL_TTL_API_KEYS_ENABLED:true}" # Enable/disable TTL (Time To Live) for api keys records
ttl: "${SQL_TTL_API_KEYS_SECS:2592000}" # Default value - 30 days
checking_interval_ms: "${SQL_TTL_API_KEYS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
relations:
max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # This value has to be reasonably small to prevent infinite recursion as early as possible
pool_size: "${SQL_RELATIONS_POOL_SIZE:4}" # This value has to be reasonably small to prevent the relation query from blocking all other DB calls

1
application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java

@ -138,6 +138,7 @@ public class ApiKeyControllerTest extends AbstractControllerTest {
ApiKeyInfo apiKeyInfo = new ApiKeyInfo();
apiKeyInfo.setDescription(description);
apiKeyInfo.setEnabled(enabled);
apiKeyInfo.setUserId(tenantAdminUserId);
return apiKeyInfo;
}

2
application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java

@ -48,7 +48,7 @@ import static org.mockito.Mockito.when;
public class ApiKeyAuthenticationProviderTest {
private static final String TEST_API_KEY = "test_api_key";
private static final String USER_EMAIL = "test@example.com";
private static final String USER_EMAIL = "tenant@thingsboard.org";
@Mock
private ApiKeyService apiKeyService;

7
common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java

@ -71,12 +71,7 @@ public enum EntityType {
return "AI model";
}
},
API_KEY(44, "api_key") {
@Override
public String getNormalName() {
return "API key";
}
};
API_KEY(44);
@Getter
private final int protoNumber; // Corresponds to EntityTypeProto

2
common/data/src/main/java/org/thingsboard/server/common/data/pat/ApiKeyInfo.java

@ -39,7 +39,7 @@ public class ApiKeyInfo extends BaseData<ApiKeyId> implements HasTenantId {
@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.", accessMode = Schema.AccessMode.READ_ONLY)
@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.")

2
dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java

@ -30,4 +30,6 @@ public interface ApiKeyDao extends Dao<ApiKey> {
Set<String> deleteByUserId(TenantId tenantId, UserId userId);
int deleteAllByExpirationTimeBefore(long ts);
}

4
dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java

@ -47,14 +47,14 @@ public class ApiKeyDataValidator extends DataValidator<ApiKey> {
if (apiKey.getTenantId() == null || apiKey.getTenantId().getId() == null) {
throw new DataValidationException("API key should be assigned to tenant!");
}
if (tenantDao.findById(apiKey.getTenantId(), apiKey.getTenantId().getId()) == null) {
if (!TenantId.SYS_TENANT_ID.equals(apiKey.getTenantId()) && tenantDao.findById(apiKey.getTenantId(), apiKey.getTenantId().getId()) == null) {
throw new DataValidationException("API key reference a non-existent tenant!");
}
if (apiKey.getUserId() == null || apiKey.getUserId().getId() == null) {
throw new DataValidationException("API key should be assigned to user!");
}
if (userDao.findById(tenantId, apiKey.getUserId().getId()) == null) {
if (userDao.findById(apiKey.getTenantId(), apiKey.getUserId().getId()) == null) {
throw new DataValidationException("API key reference a non-existent user!");
}
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyInfoRepository.java

@ -26,7 +26,7 @@ import java.util.UUID;
public interface ApiKeyInfoRepository extends JpaRepository<ApiKeyInfoEntity, UUID> {
@Query("SELECT k FROM ApiKeyInfoEntity k WHERE k.tenantId = :tenantId AND k.userId = :userId")
@Query("SELECT ak FROM ApiKeyInfoEntity ak WHERE ak.tenantId = :tenantId AND ak.userId = :userId")
Page<ApiKeyInfoEntity> findByUserId(@Param("tenantId") UUID tenantId,
@Param("userId") UUID userId,
Pageable pageable);

5
dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java

@ -50,4 +50,9 @@ public interface ApiKeyRepository extends JpaRepository<ApiKeyEntity, UUID> {
Set<String> deleteByUserId(@Param("tenantId") UUID tenantId,
@Param("userId") UUID userId);
@Transactional
@Modifying
@Query("DELETE FROM ApiKeyEntity ak WHERE ak.expirationTime > 0 AND ak.expirationTime < :ts")
int deleteAllByExpirationTimeBefore(@Param("ts") long ts);
}

5
dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java

@ -55,6 +55,11 @@ public class JpaApiKeyDao extends JpaAbstractDao<ApiKeyEntity, ApiKey> implement
return apiKeyRepository.deleteByUserId(tenantId.getId(), userId.getId());
}
@Override
public int deleteAllByExpirationTimeBefore(long ts) {
return apiKeyRepository.deleteAllByExpirationTimeBefore(ts);
}
@Override
protected Class<ApiKeyEntity> getEntityClass() {
return ApiKeyEntity.class;

2
dao/src/main/resources/sql/schema-entities.sql

@ -716,7 +716,7 @@ CREATE TABLE IF NOT EXISTS api_key (
user_id uuid,
hash varchar(255),
enabled boolean NOT NULL DEFAULT TRUE,
expiration_time bigint,
expiration_time bigint DEFAULT 0,
description varchar(1024),
CONSTRAINT api_hash_unq_key UNIQUE (hash)
);

2
dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java

@ -184,7 +184,6 @@ public class ApiKeyServiceTest extends AbstractServiceTest {
@Test
public void testDeleteByTenantId() {
// Create 3 API keys for the user
for (int i = 0; i < 3; i++) {
ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i);
apiKeyService.saveApiKey(tenantId, apiKeyInfo);
@ -213,7 +212,6 @@ public class ApiKeyServiceTest extends AbstractServiceTest {
Assert.assertEquals(size, pageData.getData().size());
Assert.assertEquals(size, pageData.getTotalElements());
// delete by user id
apiKeyService.deleteByUserId(tenantId, userId);
pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, new PageLink(10));

Loading…
Cancel
Save