70 changed files with 2341 additions and 146 deletions
@ -0,0 +1,145 @@ |
|||
/** |
|||
* 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 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.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.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; |
|||
import static org.thingsboard.server.controller.ControllerConstants.API_KEY_ID_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.TENANT_AUTHORITY_PARAGRAPH; |
|||
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 <hash>'." + TENANT_AUTHORITY_PARAGRAPH) |
|||
@PreAuthorize("hasAuthority('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')") |
|||
@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) throws ThingsboardException { |
|||
SecurityUser securityUser = getCurrentUser(); |
|||
PageLink pageLink = createPageLink(pageSize, page, null, null, null); |
|||
UserId userId = new UserId(toUUID(userIdStr)); |
|||
accessControlService.checkPermission(securityUser, Resource.API_KEY, Operation.READ); |
|||
return apiKeyService.findApiKeysByUserId(securityUser.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." + TENANT_AUTHORITY_PARAGRAPH) |
|||
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
|||
@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); |
|||
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. " + TENANT_AUTHORITY_PARAGRAPH) |
|||
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
|||
@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); |
|||
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." + TENANT_AUTHORITY_PARAGRAPH) |
|||
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
|||
@DeleteMapping(value = "/apiKey/{id}") |
|||
public void deleteApiKey(@PathVariable UUID id) throws ThingsboardException { |
|||
ApiKeyId apiKeyId = new ApiKeyId(id); |
|||
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.DELETE); |
|||
apiKeyService.deleteApiKey(apiKey.getTenantId(), apiKey, false); |
|||
} |
|||
|
|||
private String toUserApiKey(String hash) { |
|||
return API_KEY_HEADER_PREFIX + hash; |
|||
} |
|||
|
|||
} |
|||
@ -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.service.security.auth.extractor; |
|||
|
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; |
|||
|
|||
@Component |
|||
@Qualifier("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,93 @@ |
|||
/** |
|||
* 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.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.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.common.data.security.UserCredentials; |
|||
import org.thingsboard.server.dao.pat.ApiKeyService; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.model.UserPrincipal; |
|||
import org.thingsboard.server.service.security.model.token.RawApiKeyToken; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class ApiKeyAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider { |
|||
|
|||
private final ApiKeyService apiKeyService; |
|||
private final UserService userService; |
|||
|
|||
@Override |
|||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
|||
RawApiKeyToken raw = (RawApiKeyToken) authentication.getCredentials(); |
|||
SecurityUser securityUser = authenticate(raw.token()); |
|||
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.findApiKeyByHash(key); |
|||
if (apiKey == null) { |
|||
throw new UsernameNotFoundException("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"); |
|||
} |
|||
TenantId tenantId = apiKey.getTenantId(); |
|||
UserId userId = apiKey.getUserId(); |
|||
User user = userService.findUserById(tenantId, userId); |
|||
if (user == null) { |
|||
throw new UsernameNotFoundException("User for the provided API key is no longer exists"); |
|||
} |
|||
UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, userId); |
|||
if (userCredentials == null) { |
|||
throw new UsernameNotFoundException("User credentials not found"); |
|||
} |
|||
if (!userCredentials.isEnabled()) { |
|||
throw new DisabledException("User is not active"); |
|||
} |
|||
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, userCredentials.isEnabled(), 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.RawApiKeyToken; |
|||
|
|||
import java.io.Serial; |
|||
|
|||
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 2978710889397403536L; |
|||
|
|||
private RawApiKeyToken rawApiKeyToken; |
|||
private SecurityUser securityUser; |
|||
|
|||
public ApiKeyAuthenticationToken(RawApiKeyToken raw) { |
|||
super(null); |
|||
this.rawApiKeyToken = raw; |
|||
setAuthenticated(false); |
|||
} |
|||
|
|||
public ApiKeyAuthenticationToken(SecurityUser securityUser) { |
|||
super(securityUser.getAuthorities()); |
|||
this.eraseCredentials(); |
|||
this.securityUser = securityUser; |
|||
super.setAuthenticated(true); |
|||
} |
|||
|
|||
@Override |
|||
public Object getCredentials() { |
|||
return rawApiKeyToken; |
|||
} |
|||
|
|||
@Override |
|||
public Object getPrincipal() { |
|||
return this.securityUser; |
|||
} |
|||
|
|||
@Override |
|||
public void eraseCredentials() { |
|||
super.eraseCredentials(); |
|||
this.rawApiKeyToken = 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.RawApiKeyToken; |
|||
|
|||
import java.io.IOException; |
|||
|
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; |
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM; |
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM_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 { |
|||
RawApiKeyToken token = new RawApiKeyToken(tokenExtractor.extract(request)); |
|||
return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(token)); |
|||
} |
|||
|
|||
@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(JWT_TOKEN_HEADER_PARAM); |
|||
if (header == null) { |
|||
header = request.getHeader(JWT_TOKEN_HEADER_PARAM_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,20 @@ |
|||
/** |
|||
* 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 RawApiKeyToken(String token) { |
|||
|
|||
} |
|||
@ -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.ApiKeyInfo; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; |
|||
|
|||
@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); |
|||
|
|||
String apiKeyStr = doPost("/api/apiKey", apiKeyInfo, String.class); |
|||
Assert.assertTrue(apiKeyStr.startsWith(API_KEY_HEADER_PREFIX)); |
|||
|
|||
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, String.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, String.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, String.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, String.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); |
|||
return apiKeyInfo; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,189 @@ |
|||
/** |
|||
* 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.Before; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.MockitoJUnitRunner; |
|||
import org.springframework.security.authentication.BadCredentialsException; |
|||
import org.springframework.security.authentication.DisabledException; |
|||
import org.springframework.security.authentication.InsufficientAuthenticationException; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
|||
import org.thingsboard.server.common.data.User; |
|||
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.pat.ApiKey; |
|||
import org.thingsboard.server.common.data.security.Authority; |
|||
import org.thingsboard.server.common.data.security.UserCredentials; |
|||
import org.thingsboard.server.dao.pat.ApiKeyService; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.model.token.RawApiKeyToken; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertNotNull; |
|||
import static org.junit.Assert.assertTrue; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@RunWith(MockitoJUnitRunner.class) |
|||
public class ApiKeyAuthenticationProviderTest { |
|||
|
|||
private static final String TEST_API_KEY = "test_api_key"; |
|||
private static final String USER_EMAIL = "test@example.com"; |
|||
|
|||
@Mock |
|||
private ApiKeyService apiKeyService; |
|||
|
|||
@Mock |
|||
private UserService userService; |
|||
|
|||
private ApiKeyAuthenticationProvider provider; |
|||
private TenantId tenantId; |
|||
private UserId userId; |
|||
private User user; |
|||
private UserCredentials userCredentials; |
|||
private ApiKey apiKey; |
|||
|
|||
@Before |
|||
public void setUp() { |
|||
provider = new ApiKeyAuthenticationProvider(apiKeyService, userService); |
|||
tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|||
userId = new UserId(UUID.randomUUID()); |
|||
|
|||
user = new User(); |
|||
user.setId(userId); |
|||
user.setTenantId(tenantId); |
|||
user.setEmail(USER_EMAIL); |
|||
user.setAuthority(Authority.TENANT_ADMIN); |
|||
|
|||
userCredentials = new UserCredentials(); |
|||
userCredentials.setEnabled(true); |
|||
|
|||
apiKey = new ApiKey(); |
|||
apiKey.setId(new ApiKeyId(UUID.randomUUID())); |
|||
apiKey.setTenantId(tenantId); |
|||
apiKey.setUserId(userId); |
|||
apiKey.setHash(TEST_API_KEY); |
|||
apiKey.setEnabled(true); |
|||
apiKey.setExpirationTime(0); |
|||
} |
|||
|
|||
@Test |
|||
public void testSuccessfulAuthentication() { |
|||
when(apiKeyService.findApiKeyByHash(TEST_API_KEY)).thenReturn(apiKey); |
|||
when(userService.findUserById(tenantId, userId)).thenReturn(user); |
|||
when(userService.findUserCredentialsByUserId(tenantId, userId)).thenReturn(userCredentials); |
|||
|
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); |
|||
|
|||
Authentication authentication = provider.authenticate(token); |
|||
|
|||
assertNotNull(authentication); |
|||
assertTrue(authentication.isAuthenticated()); |
|||
assertTrue(authentication instanceof ApiKeyAuthenticationToken); |
|||
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); |
|||
assertEquals(userId, securityUser.getId()); |
|||
assertEquals(tenantId, securityUser.getTenantId()); |
|||
assertEquals(USER_EMAIL, securityUser.getEmail()); |
|||
assertEquals(Authority.TENANT_ADMIN, securityUser.getAuthority()); |
|||
} |
|||
|
|||
@Test(expected = BadCredentialsException.class) |
|||
public void testEmptyApiKey() { |
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken("")); |
|||
|
|||
provider.authenticate(token); |
|||
} |
|||
|
|||
@Test(expected = UsernameNotFoundException.class) |
|||
public void testNonExistentApiKey() { |
|||
when(apiKeyService.findApiKeyByHash(TEST_API_KEY)).thenReturn(null); |
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); |
|||
|
|||
provider.authenticate(token); |
|||
} |
|||
|
|||
@Test(expected = DisabledException.class) |
|||
public void testDisabledApiKey() { |
|||
apiKey.setEnabled(false); |
|||
when(apiKeyService.findApiKeyByHash(TEST_API_KEY)).thenReturn(apiKey); |
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); |
|||
|
|||
provider.authenticate(token); |
|||
} |
|||
|
|||
@Test(expected = BadCredentialsException.class) |
|||
public void testExpiredApiKey() { |
|||
apiKey.setExpirationTime(System.currentTimeMillis() - 10000); // Expired 10 seconds ago
|
|||
when(apiKeyService.findApiKeyByHash(TEST_API_KEY)).thenReturn(apiKey); |
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); |
|||
|
|||
provider.authenticate(token); |
|||
} |
|||
|
|||
@Test(expected = UsernameNotFoundException.class) |
|||
public void testNonExistentUser() { |
|||
when(apiKeyService.findApiKeyByHash(TEST_API_KEY)).thenReturn(apiKey); |
|||
when(userService.findUserById(tenantId, userId)).thenReturn(null); |
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); |
|||
|
|||
provider.authenticate(token); |
|||
} |
|||
|
|||
@Test(expected = UsernameNotFoundException.class) |
|||
public void testNonExistentUserCredentials() { |
|||
when(apiKeyService.findApiKeyByHash(TEST_API_KEY)).thenReturn(apiKey); |
|||
when(userService.findUserById(tenantId, userId)).thenReturn(user); |
|||
when(userService.findUserCredentialsByUserId(tenantId, userId)).thenReturn(null); |
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); |
|||
|
|||
provider.authenticate(token); |
|||
} |
|||
|
|||
@Test(expected = DisabledException.class) |
|||
public void testDisabledUser() { |
|||
userCredentials.setEnabled(false); |
|||
when(apiKeyService.findApiKeyByHash(TEST_API_KEY)).thenReturn(apiKey); |
|||
when(userService.findUserById(tenantId, userId)).thenReturn(user); |
|||
when(userService.findUserCredentialsByUserId(tenantId, userId)).thenReturn(userCredentials); |
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); |
|||
|
|||
provider.authenticate(token); |
|||
} |
|||
|
|||
@Test(expected = InsufficientAuthenticationException.class) |
|||
public void testUserWithoutAuthority() { |
|||
user.setAuthority(null); |
|||
when(apiKeyService.findApiKeyByHash(TEST_API_KEY)).thenReturn(apiKey); |
|||
when(userService.findUserById(tenantId, userId)).thenReturn(user); |
|||
when(userService.findUserCredentialsByUserId(tenantId, userId)).thenReturn(userCredentials); |
|||
ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); |
|||
|
|||
provider.authenticate(token); |
|||
} |
|||
|
|||
@Test |
|||
public void testSupports() { |
|||
assertTrue(provider.supports(ApiKeyAuthenticationToken.class)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
/** |
|||
* 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 deleteByTenantId(TenantId tenantId); |
|||
|
|||
void deleteByUserId(TenantId tenantId, UserId userId); |
|||
|
|||
ApiKey findApiKeyByHash(String hash); |
|||
|
|||
ApiKey findApiKeyById(TenantId tenantId, ApiKeyId apiKeyId); |
|||
|
|||
PageData<ApiKeyInfo> findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink); |
|||
|
|||
} |
|||
@ -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,63 @@ |
|||
/** |
|||
* 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.JsonIgnore; |
|||
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 hash value", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
@JsonIgnore |
|||
private String hash; |
|||
|
|||
public ApiKey() { |
|||
super(); |
|||
} |
|||
|
|||
public ApiKey(ApiKeyId id) { |
|||
super(id); |
|||
} |
|||
|
|||
public ApiKey(ApiKey apiKey) { |
|||
super(apiKey); |
|||
this.hash = apiKey.getHash(); |
|||
} |
|||
|
|||
public ApiKey(ApiKeyInfo apiKeyInfo) { |
|||
super(apiKeyInfo); |
|||
this.hash = null; |
|||
} |
|||
|
|||
public ApiKey(ApiKeyInfo apiKeyInfo, String hash) { |
|||
super(apiKeyInfo); |
|||
this.hash = hash; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
/** |
|||
* 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.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.", accessMode = Schema.AccessMode.READ_ONLY) |
|||
private UserId userId; |
|||
|
|||
@Schema(description = "Expiration time of the api key.") |
|||
private long expirationTime; |
|||
|
|||
@NoXss |
|||
@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; |
|||
|
|||
@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(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
/** |
|||
* 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.model.sql; |
|||
|
|||
import jakarta.persistence.Column; |
|||
import jakarta.persistence.MappedSuperclass; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
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.pat.ApiKeyInfo; |
|||
import org.thingsboard.server.dao.model.BaseEntity; |
|||
import org.thingsboard.server.dao.model.BaseSqlEntity; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_DESCRIPTION_COLUMN_NAME; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_ENABLED_COLUMN_NAME; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_EXPIRATION_TIME_COLUMN_NAME; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TENANT_ID_COLUMN_NAME; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_USER_ID_COLUMN_NAME; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@MappedSuperclass |
|||
public abstract class AbstractApiKeyInfoEntity<T extends ApiKeyInfo> extends BaseSqlEntity<T> implements BaseEntity<T> { |
|||
|
|||
@Column(name = API_KEY_TENANT_ID_COLUMN_NAME) |
|||
private UUID tenantId; |
|||
|
|||
@Column(name = API_KEY_USER_ID_COLUMN_NAME) |
|||
private UUID userId; |
|||
|
|||
@Column(name = API_KEY_EXPIRATION_TIME_COLUMN_NAME) |
|||
private long expirationTime; |
|||
|
|||
@Column(name = API_KEY_ENABLED_COLUMN_NAME) |
|||
private boolean enabled; |
|||
|
|||
@Column(name = API_KEY_DESCRIPTION_COLUMN_NAME) |
|||
private String description; |
|||
|
|||
public AbstractApiKeyInfoEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public AbstractApiKeyInfoEntity(ApiKeyInfo apiKeyInfo) { |
|||
super(apiKeyInfo); |
|||
this.tenantId = apiKeyInfo.getTenantId().getId(); |
|||
this.userId = apiKeyInfo.getUserId().getId(); |
|||
this.expirationTime = apiKeyInfo.getExpirationTime(); |
|||
this.description = apiKeyInfo.getDescription(); |
|||
this.enabled = apiKeyInfo.isEnabled(); |
|||
} |
|||
|
|||
protected ApiKeyInfo toApiKeyInfo() { |
|||
ApiKeyInfo apiKeyInfo = new ApiKeyInfo(new ApiKeyId(getUuid())); |
|||
apiKeyInfo.setCreatedTime(createdTime); |
|||
apiKeyInfo.setTenantId(TenantId.fromUUID(tenantId)); |
|||
apiKeyInfo.setUserId(new UserId(userId)); |
|||
apiKeyInfo.setEnabled(enabled); |
|||
apiKeyInfo.setExpirationTime(expirationTime); |
|||
apiKeyInfo.setDescription(description); |
|||
return apiKeyInfo; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
/** |
|||
* 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.model.sql; |
|||
|
|||
import jakarta.persistence.Column; |
|||
import jakarta.persistence.Entity; |
|||
import jakarta.persistence.Table; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
|
|||
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_HASH_COLUMN_NAME; |
|||
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TABLE_NAME; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@Entity |
|||
@Table(name = API_KEY_TABLE_NAME) |
|||
public class ApiKeyEntity extends AbstractApiKeyInfoEntity<ApiKey> { |
|||
|
|||
@Column(name = API_KEY_HASH_COLUMN_NAME) |
|||
private String hash; |
|||
|
|||
public ApiKeyEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public ApiKeyEntity(ApiKey apiKey) { |
|||
super(apiKey); |
|||
this.hash = apiKey.getHash(); |
|||
} |
|||
|
|||
@Override |
|||
public ApiKey toData() { |
|||
return new ApiKey(super.toApiKeyInfo(), hash); |
|||
} |
|||
|
|||
} |
|||
@ -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.dao.model.sql; |
|||
|
|||
import jakarta.persistence.Entity; |
|||
import jakarta.persistence.Table; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
|||
|
|||
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TABLE_NAME; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@Entity |
|||
@Table(name = API_KEY_TABLE_NAME) |
|||
public class ApiKeyInfoEntity extends AbstractApiKeyInfoEntity<ApiKeyInfo> { |
|||
|
|||
public ApiKeyInfoEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public ApiKeyInfoEntity(ApiKey apiKey) { |
|||
super(apiKey); |
|||
} |
|||
|
|||
@Override |
|||
public ApiKeyInfo toData() { |
|||
return super.toApiKeyInfo(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
/** |
|||
* 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.checkerframework.checker.nullness.qual.NonNull; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
import static java.util.Objects.requireNonNull; |
|||
|
|||
record ApiKeyCacheKey(String hash) implements Serializable { |
|||
|
|||
ApiKeyCacheKey { |
|||
requireNonNull(hash); |
|||
} |
|||
|
|||
static ApiKeyCacheKey of(String hash) { |
|||
return new ApiKeyCacheKey(hash); |
|||
} |
|||
|
|||
@NonNull |
|||
@Override |
|||
public String toString() { |
|||
return /* cache name */ "_" + hash; |
|||
} |
|||
|
|||
} |
|||
@ -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.dao.pat; |
|||
|
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.cache.CacheManager; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.cache.CaffeineTbTransactionalCache; |
|||
import org.thingsboard.server.common.data.CacheConstants; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
|
|||
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) |
|||
@Service("ApiKeyCache") |
|||
public class ApiKeyCaffeineCache extends CaffeineTbTransactionalCache<ApiKeyCacheKey, ApiKey> { |
|||
|
|||
public ApiKeyCaffeineCache(CacheManager cacheManager) { |
|||
super(cacheManager, CacheConstants.API_KEYS_CACHE); |
|||
} |
|||
|
|||
} |
|||
@ -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.dao.pat; |
|||
|
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.dao.Dao; |
|||
|
|||
import java.util.Set; |
|||
|
|||
public interface ApiKeyDao extends Dao<ApiKey> { |
|||
|
|||
ApiKey findByHash(String hash); |
|||
|
|||
Set<String> deleteByTenantId(TenantId tenantId); |
|||
|
|||
Set<String> deleteByUserId(TenantId tenantId, UserId userId); |
|||
|
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
public record ApiKeyEvictEvent(String hash) { |
|||
} |
|||
@ -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.dao.pat; |
|||
|
|||
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.ApiKeyInfo; |
|||
import org.thingsboard.server.dao.Dao; |
|||
|
|||
public interface ApiKeyInfoDao extends Dao<ApiKeyInfo> { |
|||
|
|||
PageData<ApiKeyInfo> findByUserId(TenantId tenantId, UserId userId, PageLink pageLink); |
|||
|
|||
} |
|||
@ -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.dao.pat; |
|||
|
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.cache.CacheSpecsMap; |
|||
import org.thingsboard.server.cache.RedisTbTransactionalCache; |
|||
import org.thingsboard.server.cache.TBRedisCacheConfiguration; |
|||
import org.thingsboard.server.cache.TbJsonRedisSerializer; |
|||
import org.thingsboard.server.common.data.CacheConstants; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
|
|||
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") |
|||
@Service("ApiKeyCache") |
|||
public class ApiKeyRedisCache extends RedisTbTransactionalCache<ApiKeyCacheKey, ApiKey> { |
|||
|
|||
public ApiKeyRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { |
|||
super(CacheConstants.API_KEYS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(ApiKey.class)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
/** |
|||
* 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 lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.event.TransactionalEventListener; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.id.ApiKeyId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.HasId; |
|||
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.AbstractCachedEntityService; |
|||
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; |
|||
import org.thingsboard.server.dao.service.validator.ApiKeyDataValidator; |
|||
|
|||
import java.util.Optional; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
|
|||
import static org.thingsboard.server.dao.service.Validator.validateId; |
|||
import static org.thingsboard.server.dao.user.UserServiceImpl.INCORRECT_TENANT_ID; |
|||
import static org.thingsboard.server.dao.user.UserServiceImpl.INCORRECT_USER_ID; |
|||
|
|||
@Slf4j |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class ApiKeyServiceImpl extends AbstractCachedEntityService<ApiKeyCacheKey, ApiKey, ApiKeyEvictEvent> implements ApiKeyService { |
|||
|
|||
private static final String INCORRECT_API_KEY_ID = "Incorrect ApiKeyId "; |
|||
private static final int DEFAULT_API_KEY_BYTES = 32; |
|||
|
|||
private final ApiKeyDao apiKeyDao; |
|||
private final ApiKeyInfoDao apiKeyInfoDao; |
|||
private final ApiKeyDataValidator apiKeyValidator; |
|||
|
|||
@Override |
|||
@TransactionalEventListener |
|||
public void handleEvictEvent(ApiKeyEvictEvent event) { |
|||
cache.evict(ApiKeyCacheKey.of(event.hash())); |
|||
} |
|||
|
|||
@Override |
|||
public ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo) { |
|||
log.trace("Executing saveApiKey [{}]", apiKeyInfo); |
|||
try { |
|||
var apiKey = new ApiKey(apiKeyInfo); |
|||
var old = apiKeyValidator.validate(apiKey, ApiKeyInfo::getTenantId); |
|||
if (old == null) { |
|||
String hash = generateApiKeySecret(); |
|||
apiKey.setHash(hash); |
|||
} else { |
|||
apiKey.setHash(old.getHash()); |
|||
} |
|||
var savedApiKey = apiKeyDao.save(tenantId, apiKey); |
|||
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedApiKey.getId()).entity(savedApiKey).created(apiKey.getId() == null).build()); |
|||
if (old != null && old.isEnabled() != apiKey.isEnabled()) { |
|||
publishEvictEvent(new ApiKeyEvictEvent(apiKey.getHash())); |
|||
} |
|||
return savedApiKey; |
|||
} catch (Exception e) { |
|||
checkConstraintViolation(e, "api_hash_unq_key", "Api Key with such hash already exists!"); |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public ApiKey findApiKeyById(TenantId tenantId, ApiKeyId apiKeyId) { |
|||
log.trace("Executing findApiKeyById [{}] [{}]", tenantId, apiKeyId); |
|||
validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id); |
|||
return apiKeyDao.findById(tenantId, apiKeyId.getId()); |
|||
} |
|||
|
|||
@Override |
|||
public PageData<ApiKeyInfo> findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink) { |
|||
log.trace("Executing findApiKeysByUserId [{}][{}]", tenantId, userId); |
|||
validateId(userId, id -> INCORRECT_USER_ID + id); |
|||
return apiKeyInfoDao.findByUserId(tenantId, userId, pageLink); |
|||
} |
|||
|
|||
@Override |
|||
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) { |
|||
return Optional.ofNullable(findApiKeyById(tenantId, new ApiKeyId(entityId.getId()))); |
|||
} |
|||
|
|||
@Override |
|||
public void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force) { |
|||
deleteApiKey(tenantId, apiKey.getId()); |
|||
} |
|||
|
|||
@Override |
|||
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { |
|||
deleteApiKey(tenantId, id); |
|||
} |
|||
|
|||
private void deleteApiKey(TenantId tenantId, EntityId entityId) { |
|||
UUID apiKeyId = entityId.getId(); |
|||
validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id); |
|||
ApiKey apiKey = apiKeyDao.findById(tenantId, apiKeyId); |
|||
if (apiKey == null) { |
|||
return; |
|||
} |
|||
apiKeyDao.removeById(tenantId, apiKeyId); |
|||
publishEvictEvent(new ApiKeyEvictEvent(apiKey.getHash())); |
|||
} |
|||
|
|||
@Override |
|||
public void deleteByTenantId(TenantId tenantId) { |
|||
log.trace("Executing deleteApiKeysByTenantId, tenantId [{}]", tenantId); |
|||
validateId(tenantId, id -> INCORRECT_TENANT_ID + id); |
|||
Set<String> hashes = apiKeyDao.deleteByTenantId(tenantId); |
|||
hashes.forEach(hash -> publishEvictEvent(new ApiKeyEvictEvent(hash))); |
|||
} |
|||
|
|||
@Override |
|||
public void deleteByUserId(TenantId tenantId, UserId userId) { |
|||
log.trace("Executing deleteApiKeysByUserId, tenantId [{}]", tenantId); |
|||
validateId(userId, id -> INCORRECT_USER_ID + id); |
|||
Set<String> hashes = apiKeyDao.deleteByUserId(tenantId, userId); |
|||
hashes.forEach(hash -> publishEvictEvent(new ApiKeyEvictEvent(hash))); |
|||
} |
|||
|
|||
@Override |
|||
public ApiKey findApiKeyByHash(String hash) { |
|||
log.trace("Executing findApiKeyByHash [{}]", hash); |
|||
var cacheKey = ApiKeyCacheKey.of(hash); |
|||
return cache.getAndPutInTransaction(cacheKey, () -> apiKeyDao.findByHash(hash), true); |
|||
} |
|||
|
|||
private static String generateApiKeySecret() { |
|||
return StringUtils.generateSafeToken(DEFAULT_API_KEY_BYTES); |
|||
} |
|||
|
|||
@Override |
|||
public EntityType getEntityType() { |
|||
return EntityType.API_KEY; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
/** |
|||
* 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.service.validator; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
import org.thingsboard.server.dao.pat.ApiKeyDao; |
|||
import org.thingsboard.server.dao.service.DataValidator; |
|||
import org.thingsboard.server.dao.tenant.TenantDao; |
|||
import org.thingsboard.server.dao.user.UserDao; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class ApiKeyDataValidator extends DataValidator<ApiKey> { |
|||
|
|||
private final ApiKeyDao apiKeyDao; |
|||
private final TenantDao tenantDao; |
|||
private final UserDao userDao; |
|||
|
|||
@Override |
|||
protected void validateDataImpl(TenantId tenantId, ApiKey apiKey) { |
|||
if (apiKey.getId() != null) { |
|||
if (apiKey.getUuidId() == null) { |
|||
throw new DataValidationException("Api Key UUID should be specified!"); |
|||
} |
|||
if (apiKey.getId().isNullUid()) { |
|||
throw new DataValidationException("API key UUID must not be the reserved null value!"); |
|||
} |
|||
} |
|||
|
|||
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) { |
|||
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) { |
|||
throw new DataValidationException("API key reference a non-existent user!"); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected ApiKey validateUpdate(TenantId tenantId, ApiKey apiKey) { |
|||
ApiKey old = apiKeyDao.findById(tenantId, apiKey.getUuidId()); |
|||
if (old == null) { |
|||
throw new DataValidationException("Cannot update non-existent API key!"); |
|||
} |
|||
if (!old.getUserId().equals(apiKey.getUserId())) { |
|||
throw new DataValidationException("Cannot update api key user id!"); |
|||
} |
|||
if (old.getExpirationTime() != apiKey.getExpirationTime()) { |
|||
throw new DataValidationException("Cannot update api key expiration time!"); |
|||
} |
|||
return old; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
/** |
|||
* 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.sql.pat; |
|||
|
|||
import org.springframework.data.domain.Page; |
|||
import org.springframework.data.domain.Pageable; |
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.springframework.data.jpa.repository.Query; |
|||
import org.springframework.data.repository.query.Param; |
|||
import org.thingsboard.server.dao.model.sql.ApiKeyInfoEntity; |
|||
|
|||
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") |
|||
Page<ApiKeyInfoEntity> findByUserId(@Param("tenantId") UUID tenantId, |
|||
@Param("userId") UUID userId, |
|||
Pageable pageable); |
|||
|
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
/** |
|||
* 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.sql.pat; |
|||
|
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.springframework.data.jpa.repository.Modifying; |
|||
import org.springframework.data.jpa.repository.Query; |
|||
import org.springframework.data.repository.query.Param; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import org.thingsboard.server.dao.model.sql.ApiKeyEntity; |
|||
|
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
|
|||
public interface ApiKeyRepository extends JpaRepository<ApiKeyEntity, UUID> { |
|||
|
|||
ApiKeyEntity findByHash(String hash); |
|||
|
|||
@Transactional |
|||
@Modifying |
|||
@Query(value = """ |
|||
DELETE FROM api_key |
|||
WHERE tenant_id = :tenantId |
|||
RETURNING hash |
|||
""", nativeQuery = true |
|||
) |
|||
Set<String> deleteByTenantId(@Param("tenantId") UUID tenantId); |
|||
|
|||
@Transactional |
|||
@Modifying |
|||
@Query(value = """ |
|||
DELETE FROM api_key |
|||
WHERE tenant_id = :tenantId AND user_id = :userId |
|||
RETURNING hash |
|||
""", nativeQuery = true |
|||
) |
|||
Set<String> deleteByUserId(@Param("tenantId") UUID tenantId, |
|||
@Param("userId") UUID userId); |
|||
|
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
/** |
|||
* 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.sql.pat; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.pat.ApiKey; |
|||
import org.thingsboard.server.dao.DaoUtil; |
|||
import org.thingsboard.server.dao.model.sql.ApiKeyEntity; |
|||
import org.thingsboard.server.dao.pat.ApiKeyDao; |
|||
import org.thingsboard.server.dao.sql.JpaAbstractDao; |
|||
import org.thingsboard.server.dao.util.SqlDao; |
|||
|
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
|
|||
@Slf4j |
|||
@SqlDao |
|||
@Component |
|||
public class JpaApiKeyDao extends JpaAbstractDao<ApiKeyEntity, ApiKey> implements ApiKeyDao { |
|||
|
|||
@Autowired |
|||
private ApiKeyRepository apiKeyRepository; |
|||
|
|||
@Override |
|||
public ApiKey findByHash(String hash) { |
|||
return DaoUtil.getData(apiKeyRepository.findByHash(hash)); |
|||
} |
|||
|
|||
@Override |
|||
public Set<String> deleteByTenantId(TenantId tenantId) { |
|||
return apiKeyRepository.deleteByTenantId(tenantId.getId()); |
|||
} |
|||
|
|||
@Override |
|||
public Set<String> deleteByUserId(TenantId tenantId, UserId userId) { |
|||
return apiKeyRepository.deleteByUserId(tenantId.getId(), userId.getId()); |
|||
} |
|||
|
|||
@Override |
|||
protected Class<ApiKeyEntity> getEntityClass() { |
|||
return ApiKeyEntity.class; |
|||
} |
|||
|
|||
@Override |
|||
protected JpaRepository<ApiKeyEntity, UUID> getRepository() { |
|||
return apiKeyRepository; |
|||
} |
|||
|
|||
@Override |
|||
public EntityType getEntityType() { |
|||
return EntityType.API_KEY; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
/** |
|||
* 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.sql.pat; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.springframework.stereotype.Component; |
|||
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.ApiKeyInfo; |
|||
import org.thingsboard.server.dao.DaoUtil; |
|||
import org.thingsboard.server.dao.model.sql.ApiKeyInfoEntity; |
|||
import org.thingsboard.server.dao.pat.ApiKeyInfoDao; |
|||
import org.thingsboard.server.dao.sql.JpaAbstractDao; |
|||
import org.thingsboard.server.dao.util.SqlDao; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
@Slf4j |
|||
@SqlDao |
|||
@Component |
|||
public class JpaApiKeyInfoDao extends JpaAbstractDao<ApiKeyInfoEntity, ApiKeyInfo> implements ApiKeyInfoDao { |
|||
|
|||
@Autowired |
|||
private ApiKeyInfoRepository apiKeyInfoRepository; |
|||
|
|||
@Override |
|||
public PageData<ApiKeyInfo> findByUserId(TenantId tenantId, UserId userId, PageLink pageLink) { |
|||
return DaoUtil.toPageData(apiKeyInfoRepository.findByUserId(tenantId.getId(), userId.getId(), DaoUtil.toPageable(pageLink))); |
|||
} |
|||
|
|||
@Override |
|||
protected Class<ApiKeyInfoEntity> getEntityClass() { |
|||
return ApiKeyInfoEntity.class; |
|||
} |
|||
|
|||
@Override |
|||
protected JpaRepository<ApiKeyInfoEntity, UUID> getRepository() { |
|||
return apiKeyInfoRepository; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,234 @@ |
|||
/** |
|||
* 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.service; |
|||
|
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.User; |
|||
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.common.data.security.Authority; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
import org.thingsboard.server.dao.pat.ApiKeyService; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|||
|
|||
@DaoSqlTest |
|||
public class ApiKeyServiceTest extends AbstractServiceTest { |
|||
|
|||
private static final String TEST_API_KEY_DESCRIPTION = "Test API Key Description"; |
|||
|
|||
@Autowired |
|||
ApiKeyService apiKeyService; |
|||
@Autowired |
|||
UserService userService; |
|||
|
|||
private UserId userId; |
|||
|
|||
@Before |
|||
public void before() { |
|||
User tenantAdmin = new User(); |
|||
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
|||
tenantAdmin.setTenantId(tenantId); |
|||
tenantAdmin.setEmail("tenant@thingsboard.org"); |
|||
User user = userService.saveUser(TenantId.SYS_TENANT_ID, tenantAdmin); |
|||
userId = user.getId(); |
|||
} |
|||
|
|||
@After |
|||
public void after() { |
|||
apiKeyService.deleteByTenantId(tenantId); |
|||
User user = userService.findUserById(tenantId, userId); |
|||
userService.deleteUser(tenantId, user); |
|||
} |
|||
|
|||
@Test |
|||
public void testSaveApiKey() { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
|||
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
|
|||
Assert.assertNotNull(savedApiKey); |
|||
Assert.assertNotNull(savedApiKey.getId()); |
|||
Assert.assertEquals(tenantId, savedApiKey.getTenantId()); |
|||
Assert.assertEquals(TEST_API_KEY_DESCRIPTION, savedApiKey.getDescription()); |
|||
Assert.assertTrue(savedApiKey.isEnabled()); |
|||
Assert.assertNotNull(savedApiKey.getHash()); |
|||
} |
|||
|
|||
@Test |
|||
public void testSaveApiKeyWithEmptyDescription() { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo(null); |
|||
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
|
|||
Assert.assertNotNull(savedApiKey); |
|||
Assert.assertNotNull(savedApiKey.getId()); |
|||
Assert.assertEquals(tenantId, savedApiKey.getTenantId()); |
|||
Assert.assertNull(savedApiKey.getDescription()); |
|||
Assert.assertTrue(savedApiKey.isEnabled()); |
|||
Assert.assertNotNull(savedApiKey.getHash()); |
|||
} |
|||
|
|||
@Test |
|||
public void testSaveApiKeyWithTooLongDescription() { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo(StringUtils.randomAlphabetic(300)); |
|||
|
|||
assertThatThrownBy(() -> apiKeyService.saveApiKey(tenantId, apiKeyInfo)) |
|||
.isInstanceOf(DataValidationException.class) |
|||
.hasMessageContaining("description length must be equal or less than 255"); |
|||
} |
|||
|
|||
@Test |
|||
public void testUpdateDescriptionApiKey() { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
|||
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
|
|||
String newDescription = "Updated API Key Description"; |
|||
savedApiKey.setDescription(newDescription); |
|||
ApiKey updatedApiKey = apiKeyService.saveApiKey(tenantId, savedApiKey); |
|||
|
|||
Assert.assertNotNull(updatedApiKey); |
|||
Assert.assertEquals(savedApiKey.getId(), updatedApiKey.getId()); |
|||
Assert.assertEquals(newDescription, updatedApiKey.getDescription()); |
|||
Assert.assertEquals(savedApiKey.getHash(), updatedApiKey.getHash()); |
|||
} |
|||
|
|||
@Test |
|||
public void testDisableApiKey() { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
|||
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
|
|||
savedApiKey.setEnabled(false); |
|||
ApiKey disabledApiKey = apiKeyService.saveApiKey(tenantId, savedApiKey); |
|||
|
|||
Assert.assertNotNull(disabledApiKey); |
|||
Assert.assertEquals(savedApiKey.getId(), disabledApiKey.getId()); |
|||
Assert.assertFalse(disabledApiKey.isEnabled()); |
|||
} |
|||
|
|||
@Test |
|||
public void testFindApiKeyById() { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
|||
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
|
|||
ApiKey foundApiKey = apiKeyService.findApiKeyById(tenantId, savedApiKey.getId()); |
|||
|
|||
Assert.assertNotNull(foundApiKey); |
|||
Assert.assertEquals(savedApiKey.getId(), foundApiKey.getId()); |
|||
Assert.assertEquals(savedApiKey.getDescription(), foundApiKey.getDescription()); |
|||
Assert.assertEquals(savedApiKey.isEnabled(), foundApiKey.isEnabled()); |
|||
Assert.assertEquals(savedApiKey.getHash(), foundApiKey.getHash()); |
|||
} |
|||
|
|||
@Test |
|||
public void testFindApiKeyByHash() { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
|||
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
|
|||
ApiKey foundApiKey = apiKeyService.findApiKeyByHash(savedApiKey.getHash()); |
|||
|
|||
Assert.assertNotNull(foundApiKey); |
|||
Assert.assertEquals(savedApiKey.getId(), foundApiKey.getId()); |
|||
Assert.assertEquals(savedApiKey.getDescription(), foundApiKey.getDescription()); |
|||
Assert.assertEquals(savedApiKey.isEnabled(), foundApiKey.isEnabled()); |
|||
Assert.assertEquals(savedApiKey.getHash(), foundApiKey.getHash()); |
|||
} |
|||
|
|||
@Test |
|||
public void testFindApiKeysByUserId() { |
|||
int size = 3; |
|||
for (int i = 0; i < size; i++) { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i); |
|||
apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
} |
|||
|
|||
PageLink pageLink = new PageLink(10); |
|||
PageData<ApiKeyInfo> pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, pageLink); |
|||
|
|||
Assert.assertNotNull(pageData); |
|||
Assert.assertEquals(size, pageData.getData().size()); |
|||
Assert.assertEquals(size, pageData.getTotalElements()); |
|||
} |
|||
|
|||
@Test |
|||
public void testDeleteApiKey() { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
|||
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
|
|||
apiKeyService.deleteApiKey(tenantId, savedApiKey, false); |
|||
|
|||
ApiKey foundApiKey = apiKeyService.findApiKeyById(tenantId, savedApiKey.getId()); |
|||
Assert.assertNull(foundApiKey); |
|||
} |
|||
|
|||
@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); |
|||
} |
|||
|
|||
apiKeyService.deleteByTenantId(tenantId); |
|||
|
|||
PageLink pageLink = new PageLink(10); |
|||
PageData<ApiKeyInfo> pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, pageLink); |
|||
|
|||
Assert.assertNotNull(pageData); |
|||
Assert.assertEquals(0, pageData.getData().size()); |
|||
Assert.assertEquals(0, pageData.getTotalElements()); |
|||
} |
|||
|
|||
@Test |
|||
public void testDeleteByUserId() { |
|||
int size = 3; |
|||
for (int i = 0; i < size; i++) { |
|||
ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i); |
|||
apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
|||
} |
|||
|
|||
PageData<ApiKeyInfo> pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, new PageLink(10)); |
|||
Assert.assertNotNull(pageData); |
|||
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)); |
|||
Assert.assertNotNull(pageData); |
|||
Assert.assertEquals(0, pageData.getData().size()); |
|||
Assert.assertEquals(0, pageData.getTotalElements()); |
|||
} |
|||
|
|||
private ApiKeyInfo createApiKeyInfo(String description) { |
|||
ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); |
|||
apiKeyInfo.setTenantId(tenantId); |
|||
apiKeyInfo.setUserId(userId); |
|||
apiKeyInfo.setDescription(description); |
|||
apiKeyInfo.setEnabled(true); |
|||
return apiKeyInfo; |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue