diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d8b04471..e5af93eaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# UTMStack 10.9.5 Release Notes +# UTMStack 10.9.4 Release Notes – Visual adjustments applied to the SOC AI Integration to ensure consistent behavior and user interaction. – Updated the header component to improve version visibility and overall UI consistency. diff --git a/backend/pom.xml b/backend/pom.xml index 68cdcd6e5..c491a3e65 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -273,7 +273,7 @@ org.springdoc springdoc-openapi-ui - 1.6.15 + 1.6.7 com.utmstack @@ -351,11 +351,6 @@ tika-core 2.9.1 - - commons-net - commons-net - 3.9.0 - diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index 07b0bcf66..ae801f44c 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -2,9 +2,7 @@ import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; public final class Constants { @@ -130,7 +128,6 @@ public final class Constants { // Defines the index pattern for querying Elasticsearch statistics indexes. // ---------------------------------------------------------------------------------- public static final String STATISTICS_INDEX_PATTERN = "v11-statistics-*"; - public static final String API_ACCESS_LOGS = ".utmstack-api-logs"; // Logging public static final String TRACE_ID_KEY = "traceId"; @@ -142,10 +139,7 @@ public final class Constants { public static final String DURATION_KEY = "duration"; public static final String CAUSE_KEY = "cause"; public static final String LAYER_KEY = "layer"; - - public static final String API_KEY_HEADER = "Utm-Api-Key"; - public static final List API_ENDPOINT_IGNORE = Collections.emptyList(); - + public static final String TFA_EXEMPTION_HEADER = "X-Bypass-TFA"; private Constants() { } diff --git a/backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java b/backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java index e3030bf09..46a30dd02 100644 --- a/backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java @@ -24,13 +24,10 @@ public OpenApiConfiguration(InfoEndpoint infoEndpoint) { public OpenAPI customOpenAPI() { final String securitySchemeBearer = "bearerAuth"; final String securitySchemeApiKey = "ApiKeyAuth"; - final String apiTitle = "UTMStack API"; String version = MapUtil.flattenToStringMap(infoEndpoint.info(), true).get("build.version"); return new OpenAPI() - .addSecurityItem(new SecurityRequirement() - .addList(securitySchemeBearer) - .addList(securitySchemeApiKey)) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeBearer).addList(securitySchemeApiKey)) .components(new Components() .addSecuritySchemes(securitySchemeBearer, new SecurityScheme() @@ -39,7 +36,7 @@ public OpenAPI customOpenAPI() { .scheme("bearer") .bearerFormat("JWT")) .addSecuritySchemes(securitySchemeApiKey, new SecurityScheme() - .name(Constants.API_KEY_HEADER) + .name("Utm-Internal-Key") .type(SecurityScheme.Type.APIKEY) .in(SecurityScheme.In.HEADER))) .info(new Info().title(apiTitle).version(version)) diff --git a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java index bf936fa57..1aa8c037c 100644 --- a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java @@ -1,13 +1,10 @@ package com.park.utmstack.config; import com.park.utmstack.security.AuthoritiesConstants; -import com.park.utmstack.security.api_key.ApiKeyConfigurer; -import com.park.utmstack.security.api_key.ApiKeyFilter; import com.park.utmstack.security.internalApiKey.InternalApiKeyConfigurer; import com.park.utmstack.security.internalApiKey.InternalApiKeyProvider; import com.park.utmstack.security.jwt.JWTConfigurer; import com.park.utmstack.security.jwt.TokenProvider; -import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,7 +29,6 @@ import javax.servlet.http.HttpServletResponse; @Configuration -@RequiredArgsConstructor @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) @Import(SecurityProblemSupport.class) @@ -43,7 +39,17 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private final TokenProvider tokenProvider; private final CorsFilter corsFilter; private final InternalApiKeyProvider internalApiKeyProvider; - private final ApiKeyFilter apiKeyFilter; + + public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, + UserDetailsService userDetailsService, + TokenProvider tokenProvider, + CorsFilter corsFilter, InternalApiKeyProvider internalApiKeyProvider) { + this.authenticationManagerBuilder = authenticationManagerBuilder; + this.userDetailsService = userDetailsService; + this.tokenProvider = tokenProvider; + this.corsFilter = corsFilter; + this.internalApiKeyProvider = internalApiKeyProvider; + } @PostConstruct public void init() { @@ -116,10 +122,7 @@ public void configure(HttpSecurity http) throws Exception { .and() .apply(securityConfigurerAdapterForJwt()) .and() - .apply(securityConfigurerAdapterForInternalApiKey()) - .and() - .apply(securityConfigurerAdapterForApiKey()) ; - + .apply(securityConfigurerAdapterForInternalApiKey()); } @@ -130,9 +133,4 @@ private JWTConfigurer securityConfigurerAdapterForJwt() { private InternalApiKeyConfigurer securityConfigurerAdapterForInternalApiKey() { return new InternalApiKeyConfigurer(internalApiKeyProvider); } - - private ApiKeyConfigurer securityConfigurerAdapterForApiKey() { - return new ApiKeyConfigurer(apiKeyFilter); - } - } diff --git a/backend/src/main/java/com/park/utmstack/domain/User.java b/backend/src/main/java/com/park/utmstack/domain/User.java index 46e3a6c93..a9432af28 100644 --- a/backend/src/main/java/com/park/utmstack/domain/User.java +++ b/backend/src/main/java/com/park/utmstack/domain/User.java @@ -90,7 +90,7 @@ public class User extends AbstractAuditingEntity implements Serializable { private Boolean defaultPassword; @JsonIgnore - @ManyToMany(fetch = FetchType.EAGER) + @ManyToMany @JoinTable(name = "jhi_user_authority", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")}) @BatchSize(size = 20) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java deleted file mode 100644 index 4986a7b58..000000000 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.park.utmstack.domain.api_keys; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.persistence.*; -import java.io.Serializable; -import java.time.Instant; -import java.util.UUID; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Entity -@Table(name = "api_keys") -public class ApiKey implements Serializable { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private Long userId; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String apiKey; - - @Column - private String allowedIp; - - @Column(nullable = false) - private Instant createdAt; - - private Instant generatedAt; - - @Column - private Instant expiresAt; -} diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java deleted file mode 100644 index 54e0c8d15..000000000 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.park.utmstack.domain.api_keys; - -import com.park.utmstack.service.dto.auditable.AuditableDTO; -import lombok.*; - -import java.util.HashMap; -import java.util.Map; - -@Builder -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -public class ApiKeyUsageLog implements AuditableDTO { - - private String id; - private String apiKeyId; - private String apiKeyName; - private String userId; - private String timestamp; - private String endpoint; - private String address; - private String errorMessage; - private String queryParams; - private String payload; - private String userAgent; - private String httpMethod; - private String statusCode; - - @Override - public Map toAuditMap() { - Map map = new HashMap<>(); - - map.put("id", id); - map.put("api_key_id", apiKeyId); - map.put("api_key_name", apiKeyName); - map.put("user_id", userId); - map.put("timestamp", timestamp != null ? timestamp : null); - map.put("endpoint", endpoint); - map.put("address", address); - map.put("error_message", errorMessage); - map.put("query_params", queryParams); - map.put("user_agent", userAgent); - map.put("http_method", httpMethod); - map.put("status_code", statusCode); - - return map; - } -} diff --git a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java index eca45ec19..9df92c191 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java @@ -42,9 +42,5 @@ public enum ApplicationEventType { ERROR, WARNING, INFO, - MODULE_ACTIVATION_ATTEMPT, - MODULE_ACTIVATION_SUCCESS, - API_KEY_ACCESS_SUCCESS, - API_KEY_ACCESS_FAILURE, - UNDEFINED + MODULE_ACTIVATION_ATTEMPT, MODULE_ACTIVATION_SUCCESS, UNDEFINED } diff --git a/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java deleted file mode 100644 index 444f22576..000000000 --- a/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.park.utmstack.loggin.api_key; - -import com.park.utmstack.domain.api_keys.ApiKey; -import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; -import com.park.utmstack.domain.application_events.enums.ApplicationEventType; -import com.park.utmstack.service.api_key.ApiKeyService; -import com.park.utmstack.service.application_events.ApplicationEventService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.springframework.web.util.ContentCachingRequestWrapper; -import org.springframework.web.util.ContentCachingResponseWrapper; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.UUID; - -import static org.postgresql.PGProperty.APPLICATION_NAME; - -@Service -@Slf4j -@RequiredArgsConstructor -public class ApiKeyUsageLoggingService { - - private final ApiKeyService apiKeyService; - private final ApplicationEventService applicationEventService; - private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; - - public void logUsage(ContentCachingRequestWrapper request, - ContentCachingResponseWrapper response, - ApiKey apiKey, - String ipAddress, - String message) { - - if (Boolean.TRUE.equals(request.getAttribute(LOG_USAGE_FLAG))) { - return; - } - - try { - String payload = extractPayload(request); - String errorText = extractErrorText(response); - int status = safeStatus(response); - - ApiKeyUsageLog usage = buildUsageLog(apiKey, ipAddress, request, status, errorText, payload); - - apiKeyService.logUsage(usage); - - ApplicationEventType eventType = (status >= 400) - ? ApplicationEventType.API_KEY_ACCESS_FAILURE - : ApplicationEventType.API_KEY_ACCESS_SUCCESS; - - String eventMessage = (status >= 400) - ? "API key access failure" - : "API key access"; - - applicationEventService.createEvent(eventMessage, eventType, usage.toAuditMap()); - - } catch (Exception e) { - log.error("Error while logging API key usage: {}", e.getMessage(), e); - } finally { - request.setAttribute(LOG_USAGE_FLAG, Boolean.TRUE); - } - } - - private int safeStatus(HttpServletResponse response) { - try { - return response.getStatus(); - } catch (Exception e) { - return 0; - } - } - - private String extractPayload(ContentCachingRequestWrapper request) { - try { - if (!"GET".equalsIgnoreCase(request.getMethod()) && !"DELETE".equalsIgnoreCase(request.getMethod())) { - byte[] content = request.getContentAsByteArray(); - return content.length > 0 ? new String(content, StandardCharsets.UTF_8) : null; - } - } catch (Exception ex) { - log.error("Error extracting payload: {}", ex.getMessage()); - } - return null; - } - - private String extractErrorText(ContentCachingResponseWrapper response) { - int statusCode = response.getStatus(); - if (statusCode >= 400) { - byte[] content = response.getContentAsByteArray(); - String responseError = content.length > 0 ? new String(content, StandardCharsets.UTF_8) : null; - String errorHeader = response.getHeader("X-" + APPLICATION_NAME + "-error"); - return StringUtils.hasText(responseError) ? responseError : errorHeader; - } - return null; - } - - private ApiKeyUsageLog buildUsageLog(ApiKey apiKey, - String ipAddress, - HttpServletRequest request, - int status, - String errorText, - String payload) { - - String id = UUID.randomUUID().toString(); - String apiKeyId = apiKey != null && apiKey.getId() != null ? apiKey.getId().toString() : null; - String apiKeyName = apiKey != null ? apiKey.getName() : null; - String userId = apiKey != null && apiKey.getUserId() != null ? apiKey.getUserId().toString() : null; - String timestamp = Instant.now().toString(); - String endpoint = request != null ? request.getRequestURI() : null; - String queryParams = request != null ? request.getQueryString() : null; - String userAgent = request != null ? request.getHeader("User-Agent") : null; - String httpMethod = request != null ? request.getMethod() : null; - String statusCode = String.valueOf(status); - - String safePayload = null; - if (payload != null) { - int PAYLOAD_MAX_LENGTH = 2000; - safePayload = payload.length() > PAYLOAD_MAX_LENGTH ? payload.substring(0, PAYLOAD_MAX_LENGTH) : payload; - } - - return ApiKeyUsageLog.builder() - .id(id) - .apiKeyId(apiKeyId) - .apiKeyName(apiKeyName) - .userId(userId) - .timestamp(timestamp) - .endpoint(endpoint) - .address(ipAddress) - .errorMessage(errorText) - .queryParams(queryParams) - .payload(safePayload) - .userAgent(userAgent) - .httpMethod(httpMethod) - .statusCode(statusCode) - .build(); - } -} diff --git a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java deleted file mode 100644 index ece32eba9..000000000 --- a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.park.utmstack.repository.api_key; - -import com.park.utmstack.domain.api_keys.ApiKey; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import javax.validation.constraints.NotNull; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Repository -public interface ApiKeyRepository extends JpaRepository { - - Optional findByIdAndUserId(Long id, Long userId); - - Page findByUserId(Long userId, Pageable pageable); - - @Cacheable(cacheNames = "apikey", key = "#root.args[0]") - Optional findOneByApiKey(@NotNull String apiKey); - - Optional findByNameAndUserId(@NotNull String name, Long userId); - - List findAllByExpiresAtAfterAndExpiresAtLessThanEqual(Instant now, Instant fiveDaysFromNow); -} diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyConfigurer.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyConfigurer.java deleted file mode 100644 index 871260236..000000000 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyConfigurer.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.park.utmstack.security.api_key; - -import com.park.utmstack.security.jwt.JWTFilter; -import org.springframework.security.config.annotation.SecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.web.DefaultSecurityFilterChain; - -public class ApiKeyConfigurer extends SecurityConfigurerAdapter { - - private final ApiKeyFilter apiKeyFilter; - - public ApiKeyConfigurer(ApiKeyFilter apiKeyFilter) { - this.apiKeyFilter = apiKeyFilter; - } - - @Override - public void configure(HttpSecurity http) throws Exception { - http.addFilterAfter(apiKeyFilter, JWTFilter.class); - } -} diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java deleted file mode 100644 index cb3624567..000000000 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.park.utmstack.security.api_key; - - -import com.park.utmstack.config.Constants; -import com.park.utmstack.domain.api_keys.ApiKey; -import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; -import com.park.utmstack.repository.UserRepository; -import com.park.utmstack.service.api_key.ApiKeyService; -import com.park.utmstack.service.application_events.ApplicationEventService; -import com.park.utmstack.util.exceptions.ApiKeyInvalidAccessException; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.net.util.SubnetUtils; -import org.springframework.lang.NonNull; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.util.ContentCachingRequestWrapper; -import org.springframework.web.util.ContentCachingResponseWrapper; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.time.Instant; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static com.park.utmstack.config.Constants.API_ENDPOINT_IGNORE; - -@Slf4j -@Component -@AllArgsConstructor -public class ApiKeyFilter extends OncePerRequestFilter { - - private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; - private static final Pattern CIDR_PATTERN = Pattern.compile( - "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)/(\\d|[1-2]\\d|3[0-2])$" - ); - - - private final UserRepository userRepository; - private final ApiKeyService apiKeyService; - private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); - private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); - private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; - - - @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) throws ServletException, IOException { - - if (API_ENDPOINT_IGNORE.contains(request.getRequestURI())) { - filterChain.doFilter(request, response); - return; - } - - if (request.getAttribute(LOG_USAGE_FLAG) != null) { - filterChain.doFilter(request, response); - return; - } - - String apiKey = request.getHeader(Constants.API_KEY_HEADER); - - if (!StringUtils.hasText(apiKey)) { - filterChain.doFilter(request, response); - return; - } - - String ipAddress = request.getRemoteAddr(); - var key = getApiKey(apiKey); - - var wrappedRequest = new ContentCachingRequestWrapper(request); - var wrappedResponse = new ContentCachingResponseWrapper(response); - - UsernamePasswordAuthenticationToken authentication; - - try { - authentication = getAuthentication(key, ipAddress); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(wrappedRequest)); - SecurityContextHolder.getContext().setAuthentication(authentication); - - } catch (ApiKeyInvalidAccessException e) { - apiKeyUsageLoggingService.logUsage(wrappedRequest, wrappedResponse, key, ipAddress, e.getMessage()); - throw e; - } - - filterChain.doFilter(wrappedRequest, wrappedResponse); - wrappedResponse.copyBodyToResponse(); - - apiKeyUsageLoggingService.logUsage(wrappedRequest, wrappedResponse, key, ipAddress, null); - } - - private ApiKey getApiKey(String apiKey) { - if (invalidApiKeyBlackList.containsKey(apiKey)) { - log.warn("Access attempt with invalid API key (cached)"); - throw new ApiKeyInvalidAccessException("Invalid API key"); - } - - return apiKeyService.findOneByApiKey(apiKey) - .orElseGet(() -> { - invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); - log.warn("Access attempt with invalid API key (not found in DB)"); - throw new ApiKeyInvalidAccessException("Invalid API key"); - }); - } - - public UsernamePasswordAuthenticationToken getAuthentication(ApiKey apiKey, String remoteIpAddress) { - Objects.requireNonNull(apiKey, "API key must not be null"); - Objects.requireNonNull(remoteIpAddress, "Remote IP address must not be null"); - - if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { - log.warn("Access denied: IP [{}] not allowed for API key [{}]", remoteIpAddress, apiKey.getApiKey()); - throw new ApiKeyInvalidAccessException( - "Invalid IP address: " + remoteIpAddress + ". If you recognize this IP, add it to allowed IP list." - ); - } - - if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { - log.warn("Access denied: API key [{}] expired at {}", apiKey.getApiKey(), apiKey.getExpiresAt()); - throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); - } - - var userEntityOpt = userRepository.findById(apiKey.getUserId()); - if (userEntityOpt.isEmpty()) { - log.warn("Access denied: User [{}] not found for API key [{}]", apiKey.getUserId(), apiKey.getApiKey()); - throw new ApiKeyInvalidAccessException("User not found for API key"); - } - - var userEntity = userEntityOpt.get(); - - if (!userEntity.getActivated()) { - log.warn("Access denied: User [{}] not activated", userEntity.getLogin()); - throw new ApiKeyInvalidAccessException("User not activated"); - } - - List authorities = userEntity.getAuthorities().stream() - .map(auth -> new SimpleGrantedAuthority(auth.getName())) - .collect(Collectors.toList()); - - User principal = new User(userEntity.getLogin(), "", authorities); - - return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); - } - - public boolean allowAccessToRemoteIp(String allowedIpList, String remoteIp) { - if (allowedIpList == null || allowedIpList.trim().isEmpty()) { - return true; - } - String[] whitelistIps = allowedIpList.split(","); - for (String ip : whitelistIps) { - String allowed = ip.trim(); - if (allowed.isEmpty()) { - continue; - } - if (CIDR_PATTERN.matcher(allowed).matches()) { - try { - SubnetUtils subnetUtils = cidrCache.computeIfAbsent(allowed, key -> { - SubnetUtils su = new SubnetUtils(key); - su.setInclusiveHostCount(true); - return su; - }); - if (subnetUtils.getInfo().isInRange(remoteIp)) { - return true; - } - } catch (IllegalArgumentException e) { - log.error("Invalid CIDR notation: {}", allowed); - } - } else if (allowed.equals(remoteIp)) { - return true; - } - } - return false; - } -} diff --git a/backend/src/main/java/com/park/utmstack/security/jwt/TokenProvider.java b/backend/src/main/java/com/park/utmstack/security/jwt/TokenProvider.java index 246a5dad0..e48f9baed 100644 --- a/backend/src/main/java/com/park/utmstack/security/jwt/TokenProvider.java +++ b/backend/src/main/java/com/park/utmstack/security/jwt/TokenProvider.java @@ -1,6 +1,7 @@ package com.park.utmstack.security.jwt; +import com.park.utmstack.config.Constants; import com.park.utmstack.security.AuthoritiesConstants; import com.park.utmstack.util.CipherUtil; import io.jsonwebtoken.*; @@ -16,10 +17,12 @@ import org.springframework.stereotype.Component; import tech.jhipster.config.JHipsterProperties; +import javax.servlet.http.HttpServletRequest; import java.security.Key; import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.Optional; import java.util.stream.Collectors; @Component @@ -116,4 +119,16 @@ public boolean validateToken(String authToken) { } return false; } + + public boolean canBypassTwoFactorAuth(HttpServletRequest request) { + boolean tfaExemptionRequested = Boolean.parseBoolean(request.getHeader(Constants.TFA_EXEMPTION_HEADER)); + + boolean forceTfaAuth = Boolean.parseBoolean( + Optional.ofNullable(System.getenv(Constants.PROP_TFA_ENABLE)).orElse("true") + ); + + return tfaExemptionRequested || !forceTfaAuth; + } + + } diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java deleted file mode 100644 index 8a73a748d..000000000 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.park.utmstack.service.api_key; - -import com.park.utmstack.domain.api_keys.ApiKey; -import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; -import com.park.utmstack.repository.api_key.ApiKeyRepository; -import com.park.utmstack.service.UserService; -import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; -import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; -import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; -import com.park.utmstack.service.mapper.ApiKeyMapper; -import com.park.utmstack.util.exceptions.ApiKeyExistException; -import com.park.utmstack.util.exceptions.ApiKeyNotFoundException; -import lombok.AllArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.security.SecureRandom; -import java.time.Duration; -import java.time.Instant; -import java.util.Base64; -import java.util.Optional; -import java.util.UUID; - -import static com.park.utmstack.config.Constants.API_ACCESS_LOGS; - -@Service -@AllArgsConstructor -public class ApiKeyService { - - private static final String CLASSNAME = "ApiKeyService"; - private final Logger log = LoggerFactory.getLogger(ApiKeyService.class); - private final ApiKeyRepository apiKeyRepository; - private final ApiKeyMapper apiKeyMapper; - private final OpensearchClientBuilder client; - - - public ApiKeyResponseDTO createApiKey(Long userId,ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".createApiKey"; - try { - apiKeyRepository.findByNameAndUserId(dto.getName(), userId) - .ifPresent(apiKey -> { - throw new ApiKeyExistException("Api key already exists"); - }); - - var apiKey = ApiKey.builder() - .userId(userId) - .name(dto.getName()) - .expiresAt(dto.getExpiresAt()) - .allowedIp(String.join(",", dto.getAllowedIp())) - .createdAt(Instant.now()) - .generatedAt(Instant.now()) - .apiKey(generateRandomKey()) - .build(); - - return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); - } catch (Exception e) { - throw new ApiKeyExistException(ctx + ": " + e.getMessage()); - } - } - - public String generateApiKey(Long userId, Long apiKeyId) { - final String ctx = CLASSNAME + ".generateApiKey"; - try { - ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) - .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); - - Instant now = Instant.now(); - Instant originalCreated = apiKey.getGeneratedAt() != null ? apiKey.getGeneratedAt() : apiKey.getCreatedAt(); - Instant originalExpires = apiKey.getExpiresAt(); - - Duration duration; - if (originalCreated != null && originalExpires != null && !originalExpires.isBefore(originalCreated)) { - duration = Duration.between(originalCreated, originalExpires); - } else { - duration = Duration.ofDays(7); - } - - String plainKey = generateRandomKey(); - apiKey.setApiKey(plainKey); - apiKey.setGeneratedAt(Instant.now()); - apiKey.setExpiresAt(now.plus(duration)); - apiKeyRepository.save(apiKey); - return plainKey; - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - - public ApiKeyResponseDTO updateApiKey(Long userId, Long apiKeyId, ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".updateApiKey"; - try { - ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) - .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); - apiKey.setName(dto.getName()); - if (dto.getAllowedIp() != null) { - apiKey.setAllowedIp(String.join(",", dto.getAllowedIp())); - } else { - apiKey.setAllowedIp(null); - } - apiKey.setExpiresAt(dto.getExpiresAt()); - ApiKey updated = apiKeyRepository.save(apiKey); - return apiKeyMapper.toDto(updated); - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - - public ApiKeyResponseDTO getApiKey(Long userId, Long apiKeyId) { - final String ctx = CLASSNAME + ".getApiKey"; - try { - ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) - .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); - return apiKeyMapper.toDto(apiKey); - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - - public Page listApiKeys(Long userId, Pageable pageable) { - final String ctx = CLASSNAME + ".listApiKeys"; - try { - return apiKeyRepository.findByUserId(userId, pageable).map(apiKeyMapper::toDto); - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - - - public void deleteApiKey(Long userId, Long apiKeyId) { - final String ctx = CLASSNAME + ".deleteApiKey"; - try { - ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) - .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); - apiKeyRepository.delete(apiKey); - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - - private String generateRandomKey() { - final String ctx = CLASSNAME + ".generateRandomKey"; - try { - SecureRandom random = new SecureRandom(); - byte[] keyBytes = new byte[32]; - random.nextBytes(keyBytes); - return Base64.getUrlEncoder().withoutPadding().encodeToString(keyBytes); - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - - @Async - public void logUsage(ApiKeyUsageLog apiKeyUsageLog) { - final String ctx = CLASSNAME + ".logUsage"; - try { - client.getClient().index(API_ACCESS_LOGS, apiKeyUsageLog); - } catch (Exception e) { - log.error(ctx + ": {}", e.getMessage()); - } - } - - public Optional findOneByApiKey(String apiKey) { - return apiKeyRepository.findOneByApiKey(apiKey); - } - -} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java deleted file mode 100644 index abfa4d02d..000000000 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.park.utmstack.service.dto.api_key; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; -import java.util.List; -import java.util.UUID; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ApiKeyResponseDTO { - - @Schema(description = "Unique identifier of the API key") - private Long id; - - @Schema(description = "User-friendly API key name") - private String name; - - @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24')") - private List allowedIp; - - @Schema(description = "API key creation timestamp") - private Instant createdAt; - - @Schema(description = "API key expiration timestamp (if applicable)") - private Instant expiresAt; - - @Schema(description = "Generated At") - private Instant generatedAt; -} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java deleted file mode 100644 index 6345f2032..000000000 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.park.utmstack.service.dto.api_key; - -import com.park.utmstack.validation.api_key.ValidIPOrCIDR; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.validation.constraints.NotNull; -import java.time.Instant; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ApiKeyUpsertDTO { - @NotNull - @Schema(description = "API Key name", requiredMode = Schema.RequiredMode.REQUIRED) - private String name; - - @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24'). If null, no IP restrictions are applied.") - private List<@ValidIPOrCIDR String> allowedIp; - - @Schema(description = "Expiration timestamp of the API key") - private Instant expiresAt; -} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/collectors/dto/CollectorConfigDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/collectors/dto/CollectorConfigDTO.java new file mode 100644 index 000000000..108df0e4d --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/collectors/dto/CollectorConfigDTO.java @@ -0,0 +1,20 @@ +package com.park.utmstack.service.dto.collectors.dto; + +import com.park.utmstack.domain.application_modules.UtmModuleGroupConfiguration; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Setter +@Getter +public class CollectorConfigDTO { + @NotNull + CollectorDTO collector; + @NotNull + private Long moduleId; + @NotNull + private List keys; + +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/collectors/dto/CollectorConfigKeysDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/collectors/dto/CollectorConfigKeysDTO.java deleted file mode 100644 index 40af52707..000000000 --- a/backend/src/main/java/com/park/utmstack/service/dto/collectors/dto/CollectorConfigKeysDTO.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.park.utmstack.service.dto.collectors.dto; - -import com.park.utmstack.domain.application_modules.UtmModuleGroupConfiguration; - -import javax.validation.constraints.NotNull; -import java.util.List; - -public class CollectorConfigKeysDTO { - @NotNull - CollectorDTO collector; - @NotNull - private Long moduleId; - @NotNull - private List keys; - - public Long getModuleId() { - return moduleId; - } - - public void setModuleId(Long moduleId) { - this.moduleId = moduleId; - } - - public List getKeys() { - return keys; - } - - public void setKeys(List keys) { - this.keys = keys; - } - - public CollectorDTO getCollector() { - return collector; - } - - public void setCollector(CollectorDTO collector) { - this.collector = collector; - } -} diff --git a/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java deleted file mode 100644 index 0439c563b..000000000 --- a/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.park.utmstack.service.mapper; - -import com.park.utmstack.domain.api_keys.ApiKey; -import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; -import org.mapstruct.Mapper; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Optional; -import java.util.stream.Collectors; - -@Mapper(componentModel = "spring") -public class ApiKeyMapper { - - public ApiKeyResponseDTO toDto(ApiKey apiKey){ - return ApiKeyResponseDTO.builder() - .id(apiKey.getId()) - .name(apiKey.getName()) - .createdAt(apiKey.getCreatedAt()) - .expiresAt(apiKey.getExpiresAt()) - .allowedIp( - Optional.ofNullable(apiKey.getAllowedIp()) - .map(s -> Arrays.stream(s.split(",")) - .map(String::trim) - .filter(str -> !str.isEmpty()) - .collect(Collectors.toList())) - .orElse(Collections.emptyList()) - ) - .build(); - } -} diff --git a/backend/src/main/java/com/park/utmstack/service/validators/collector/CollectorValidatorService.java b/backend/src/main/java/com/park/utmstack/service/validators/collector/CollectorValidatorService.java index ffc3156ac..02d1bca1b 100644 --- a/backend/src/main/java/com/park/utmstack/service/validators/collector/CollectorValidatorService.java +++ b/backend/src/main/java/com/park/utmstack/service/validators/collector/CollectorValidatorService.java @@ -1,7 +1,7 @@ package com.park.utmstack.service.validators.collector; import com.park.utmstack.domain.application_modules.UtmModuleGroupConfiguration; -import com.park.utmstack.service.dto.collectors.dto.CollectorConfigKeysDTO; +import com.park.utmstack.service.dto.collectors.dto.CollectorConfigDTO; import org.springframework.stereotype.Service; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @@ -13,12 +13,12 @@ public class CollectorValidatorService implements Validator { @Override public boolean supports(Class> clazz) { - return CollectorConfigKeysDTO.class.equals(clazz); + return CollectorConfigDTO.class.equals(clazz); } @Override public void validate(Object target, Errors errors) { - CollectorConfigKeysDTO updateConfigurationKeysBody = (CollectorConfigKeysDTO) target; + CollectorConfigDTO updateConfigurationKeysBody = (CollectorConfigDTO) target; Map hostNames = updateConfigurationKeysBody.getKeys().stream() .filter(config -> config.getConfName().equals("Hostname")) diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java deleted file mode 100644 index 20577f508..000000000 --- a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.park.utmstack.util.exceptions; - -public class ApiKeyExistException extends RuntimeException { - public ApiKeyExistException(String message) { - super(message); - } -} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java deleted file mode 100644 index c5a13ead4..000000000 --- a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.park.utmstack.util.exceptions; - -import org.springframework.security.core.AuthenticationException; - -public class ApiKeyInvalidAccessException extends AuthenticationException { - public ApiKeyInvalidAccessException(String message) { - super(message); - } -} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java deleted file mode 100644 index 173d8f442..000000000 --- a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.park.utmstack.util.exceptions; - -public class ApiKeyNotFoundException extends RuntimeException { - public ApiKeyNotFoundException(String message) { - super(message); - } -} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java deleted file mode 100644 index 55dfe9593..000000000 --- a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.park.utmstack.validation.api_key; - - -import javax.validation.Constraint; -import javax.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Documented -@Constraint(validatedBy = ValidIPOrCIDRValidator.class) -@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) -@Retention(RUNTIME) -public @interface ValidIPOrCIDR { - String message() default "Invalid IP address or CIDR notation"; - - Class>[] groups() default {}; - - Class extends Payload>[] payload() default {}; -} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java deleted file mode 100644 index 8324094f8..000000000 --- a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.park.utmstack.validation.api_key; - - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import java.util.regex.Pattern; - -public class ValidIPOrCIDRValidator implements ConstraintValidator { - - private static final Pattern IPV4_PATTERN = Pattern.compile( - "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)$" - ); - - private static final Pattern IPV4_CIDR_PATTERN = Pattern.compile( - "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)/(\\d|[1-2]\\d|3[0-2])$" - ); - private static final Pattern IPV6_PATTERN = Pattern.compile( - "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}$" - ); - - private static final Pattern IPV6_CIDR_PATTERN = Pattern.compile( - "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}/(\\d|[1-9]\\d|1[01]\\d|12[0-8])$" - ); - - @Override - public boolean isValid(String value, ConstraintValidatorContext context) { - // Allow null or empty values; use @NotNull/@NotEmpty to enforce non-null if needed. - if (value == null || value.trim().isEmpty()) { - return true; - } - String trimmed = value.trim(); - if (IPV4_PATTERN.matcher(trimmed).matches() || IPV4_CIDR_PATTERN.matcher(trimmed).matches()) { - return true; - } - return IPV6_PATTERN.matcher(trimmed).matches() || IPV6_CIDR_PATTERN.matcher(trimmed).matches(); - } -} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java b/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java index e73efc8bd..cb0a403b6 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java @@ -77,7 +77,8 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, H throw new TooMuchLoginAttemptsException(String.format("Authentication blocked: IP %s exceeded login attempt threshold", ip)); } - boolean authenticated = !Boolean.parseBoolean(Constants.CFG.get(Constants.PROP_TFA_ENABLE)); + boolean isTfaExempted = this.tokenProvider.canBypassTwoFactorAuth(request); + boolean authenticated = !Boolean.parseBoolean(Constants.CFG.get(Constants.PROP_TFA_ENABLE)) || isTfaExempted; UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword()); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java deleted file mode 100644 index aa6b2052a..000000000 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.park.utmstack.web.rest.api_key; - - -import com.park.utmstack.domain.chart_builder.types.query.FilterType; -import com.park.utmstack.security.AuthoritiesConstants; -import com.park.utmstack.service.UserService; -import com.park.utmstack.service.api_key.ApiKeyService; -import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; -import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; -import com.park.utmstack.service.elasticsearch.ElasticsearchService; -import com.park.utmstack.util.UtilPagination; -import com.park.utmstack.web.rest.elasticsearch.ElasticsearchResource; -import io.swagger.v3.oas.annotations.Hidden; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.headers.Header; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.opensearch.client.opensearch.core.SearchResponse; -import org.opensearch.client.opensearch.core.search.Hit; -import org.opensearch.client.opensearch.core.search.HitsMetadata; -import org.springdoc.api.annotations.ParameterObject; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import tech.jhipster.web.util.PaginationUtil; - -import java.util.*; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/api-keys") -@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") -@Slf4j -@AllArgsConstructor -@Hidden -public class ApiKeyResource { - - private final ApiKeyService apiKeyService; - private final ElasticsearchService elasticsearchService; - private final UserService userService; - - @Operation(summary = "Create API key", - description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "API key created successfully", - content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), - @ApiResponse(responseCode = "409", description = "API key already exists", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @PostMapping - public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { - Long userId = userService.getCurrentUserLogin().getId(); - ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(userId, dto); - return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); - } - - @Operation(summary = "Generate a new API key", - description = "Generates (or renews) a new random API key for the specified API key record. The plain text key is returned only once.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "API key generated successfully", - content = @Content(schema = @Schema(type = "string"))), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @PostMapping("/{id}/generate") - public ResponseEntity generateApiKey(@PathVariable("id") Long apiKeyId) { - Long userId = userService.getCurrentUserLogin().getId(); - String plainKey = apiKeyService.generateApiKey(userId, apiKeyId); - return ResponseEntity.ok(plainKey); - } - - @Operation(summary = "Retrieve API key", - description = "Retrieves the API key details for the specified API key record.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "API key retrieved successfully", - content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @GetMapping("/{id}") - public ResponseEntity getApiKey(@PathVariable("id") Long apiKeyId) { - Long userId = userService.getCurrentUserLogin().getId(); - ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(userId, apiKeyId); - return ResponseEntity.ok(responseDTO); - } - - @Operation(summary = "List API keys", - description = "Retrieves the API key list.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "API key retrieved successfully", - content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @GetMapping - public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { - Long userId = userService.getCurrentUserLogin().getId(); - Page page = apiKeyService.listApiKeys(userId,pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); - - return ResponseEntity.ok().headers(headers).body(page.getContent()); - } - - @Operation(summary = "Update API key", - description = "Updates mutable fields (name, allowed IPs, expiration) for the specified API key record.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "API key updated successfully", - content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @PutMapping("/{id}") - public ResponseEntity updateApiKey(@PathVariable("id") Long apiKeyId, - @RequestBody ApiKeyUpsertDTO dto) { - - Long userId = userService.getCurrentUserLogin().getId(); - ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(userId, apiKeyId, dto); - return ResponseEntity.ok(responseDTO); - - } - - @Operation(summary = "Delete API key", - description = "Deletes the specified API key record for the authenticated user.") - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "API key deleted successfully", content = @Content), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @DeleteMapping("/{id}") - public ResponseEntity deleteApiKey(@PathVariable("id") Long apiKeyId) { - - Long userId = userService.getCurrentUserLogin().getId(); - apiKeyService.deleteApiKey(userId, apiKeyId); - return ResponseEntity.noContent().build(); - } - - @PostMapping("/usage") - public ResponseEntity> search(@RequestBody(required = false) List filters, - @RequestParam Integer top, @RequestParam String indexPattern, - @RequestParam(required = false, defaultValue = "false") boolean includeChildren, - Pageable pageable) { - - SearchResponse searchResponse = elasticsearchService.search(filters, top, indexPattern, - pageable, Map.class); - - if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) - return ResponseEntity.ok(Collections.emptyList()); - - HitsMetadata hits = searchResponse.hits(); - HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), - pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); - - return ResponseEntity.ok().headers(headers).body(hits.hits().stream() - .map(Hit::source).collect(Collectors.toList())); - - } -} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/collectors/UtmCollectorResource.java b/backend/src/main/java/com/park/utmstack/web/rest/collectors/UtmCollectorResource.java index 7f97bc9b5..728af040e 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/collectors/UtmCollectorResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/collectors/UtmCollectorResource.java @@ -16,14 +16,13 @@ import com.park.utmstack.service.application_modules.UtmModuleGroupService; import com.park.utmstack.service.collectors.CollectorOpsService; import com.park.utmstack.service.collectors.UtmCollectorService; -import com.park.utmstack.service.dto.collectors.dto.CollectorConfigKeysDTO; +import com.park.utmstack.service.dto.collectors.dto.CollectorConfigDTO; import com.park.utmstack.service.dto.collectors.dto.CollectorDTO; import com.park.utmstack.service.dto.collectors.CollectorModuleEnum; import com.park.utmstack.service.dto.collectors.dto.ListCollectorsResponseDTO; import com.park.utmstack.service.dto.network_scan.AssetGroupDTO; import com.park.utmstack.service.validators.collector.CollectorValidatorService; import com.park.utmstack.util.UtilResponse; -import com.park.utmstack.web.rest.application_modules.UtmModuleGroupConfigurationResource; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import com.park.utmstack.web.rest.errors.InternalServerErrorException; import com.park.utmstack.web.rest.network_scan.UtmNetworkScanResource; @@ -94,9 +93,7 @@ public UtmCollectorResource(CollectorOpsService collectorService, * persist the configurations. */ @PostMapping("/collector-config") - public ResponseEntity upsertCollectorConfig( - @Valid @RequestBody CollectorConfigKeysDTO collectorConfig, - CollectorDTO collectorDTO) { + public ResponseEntity upsertCollectorConfig(@Valid @RequestBody CollectorConfigDTO collectorConfig) { final String ctx = CLASSNAME + ".upsertCollectorConfig"; try { @@ -112,11 +109,11 @@ public ResponseEntity upsertCollectorConfig( // Get the actual configuration just in case of error when updating local db. CollectorConfig cacheConfig = collectorService.getCollectorConfig( - ConfigRequest.newBuilder().setModule(CollectorModule.valueOf(collectorDTO.getModule().toString())).build(), - AuthResponse.newBuilder().setId(collectorDTO.getId()).setKey(collectorDTO.getCollectorKey()).build()); + ConfigRequest.newBuilder().setModule(CollectorModule.valueOf(collectorConfig.getCollector().getModule().toString())).build(), + AuthResponse.newBuilder().setId(collectorConfig.getCollector().getId()).setKey(collectorConfig.getCollector().getCollectorKey()).build()); // Map the configurations to gRPC CollectorConfig and try to insert/update the collector config collectorService.upsertCollectorConfig(collectorService.mapToCollectorConfig( - collectorService.mapPasswordConfiguration(collectorConfig.getKeys()), collectorDTO)); + collectorService.mapPasswordConfiguration(collectorConfig.getKeys()), collectorConfig.getCollector())); // If the update is fine via gRPC, then update the configurations in local db. try { moduleGroupConfigurationService.updateConfigurationKeys(collectorConfig.getModuleId(), collectorConfig.getKeys()); diff --git a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml deleted file mode 100644 index e73010f6a..000000000 --- a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/src/main/resources/config/liquibase/master.xml b/backend/src/main/resources/config/liquibase/master.xml index 17b6c5c3b..f6ce831a6 100644 --- a/backend/src/main/resources/config/liquibase/master.xml +++ b/backend/src/main/resources/config/liquibase/master.xml @@ -113,7 +113,5 @@ - - diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html deleted file mode 100644 index e2a5158ff..000000000 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - API Keys - - - The API key is a simple encrypted string that identifies you in the application. With this key, you can access the REST API. - - - - Create New API Key - - - - - - - - - Name - - - Allowed IPs - - - Expires At - - - Created At - - ACTIONS - - - - - {{ key.name }} - {{ key.allowedIp?.join(', ') || '—' }} - - = 0 && getDaysUntilExpire(key.expiresAt) <= 7"> - - - - - - - - - - - - {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm':'UTC') : '—' }} - - - {{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Copy it now because it will be shown only once! - - - {{ maskSecrets(generatedApiKey) }} - - - {{ copied ? 'copied' : 'copy' }} - - - - - {{ 'Keep this key safeKeep this key safe' }} - - - - - {{ 'Close' }} - - - diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.scss b/frontend/src/app/app-management/api-keys/api-keys.component.scss deleted file mode 100644 index b3751c83a..000000000 --- a/frontend/src/app/app-management/api-keys/api-keys.component.scss +++ /dev/null @@ -1,7 +0,0 @@ -:host{ - display: flex; - flex-direction: column; - flex: 1 1 auto; - min-height: 0; - height: 100%; -} diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts deleted file mode 100644 index 82d9e3450..000000000 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ /dev/null @@ -1,232 +0,0 @@ -import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; -import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; -import * as moment from 'moment'; -import {UtmToastService} from '../../shared/alert/utm-toast.service'; -import { - ModalConfirmationComponent -} from '../../shared/components/utm/util/modal-confirmation/modal-confirmation.component'; -import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; -import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; -import {ApiKeyModalComponent} from './shared/components/api-key-modal/api-key-modal.component'; -import {ApiKeyResponse} from './shared/models/ApiKeyResponse'; -import {ApiKeysService} from './shared/service/api-keys.service'; - -@Component({ - selector: 'app-api-keys', - templateUrl: './api-keys.component.html', - styleUrls: ['./api-keys.component.scss'] -}) -export class ApiKeysComponent implements OnInit { - - generating: string[] = []; - noData = false; - apiKeys: ApiKeyResponse[] = []; - loading = false; - generatedApiKey = ''; - @ViewChild('generatedModal') generatedModal!: TemplateRef; - generatedModalRef!: NgbModalRef; - copied = false; - readonly itemsPerPage = ITEMS_PER_PAGE; - totalItems = 0; - page = 0; - size = this.itemsPerPage; - - request = { - sort: 'createdAt,desc', - page: this.page, - size: this.size - }; - - constructor( private toastService: UtmToastService, - private apiKeyService: ApiKeysService, - private modalService: NgbModal - ) {} - - ngOnInit(): void { - this.loadKeys(); - } - - loadKeys(): void { - this.loading = true; - this.apiKeyService.list(this.request).subscribe({ - next: (res) => { - this.totalItems = Number(res.headers.get('X-Total-Count')); - this.apiKeys = res.body || []; - this.noData = this.apiKeys.length === 0; - this.loading = false; - }, - error: () => { - this.loading = false; - this.apiKeys = []; - } - }); - } - - copyToClipboard(): void { - if (!this.generatedApiKey) { return; } - - if (navigator && (navigator as any).clipboard && (navigator as any).clipboard.writeText) { - (navigator as any).clipboard.writeText(this.generatedApiKey) - .then(() => this.copied = true) - .catch(err => { - console.error('Error al copiar con clipboard API', err); - this.fallbackCopy(this.generatedApiKey); - }); - } else { - this.fallbackCopy(this.generatedApiKey); - } - } - - private fallbackCopy(text: string): void { - try { - const textarea = document.createElement('textarea'); - textarea.value = text; - - textarea.style.position = 'fixed'; - textarea.style.top = '0'; - textarea.style.left = '0'; - textarea.style.opacity = '0'; - - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - - const successful = document.execCommand('copy'); - document.body.removeChild(textarea); - - if (successful) { - this.showCopiedFeedback(); - } else { - console.warn('Fallback copy failed'); - } - } catch (err) { - console.error('Error en fallback copy', err); - } - } - - private showCopiedFeedback(): void { - this.copied = true; - setTimeout(() => this.copied = false, 2000); - } - - openCreateModal(): void { - const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true }); - - modalRef.result.then((key: ApiKeyResponse) => { - if (key) { - this.generateKey(key); - } - }); - } - - editKey(key: ApiKeyResponse): void { - const modalRef = this.modalService.open(ApiKeyModalComponent, {centered: true}); - modalRef.componentInstance.apiKey = key; - - modalRef.result.then((key: ApiKeyResponse) => { - if (key) { - this.generateKey(key); - } - }); - } - - deleteKey(apiKey: ApiKeyResponse): void { - const modalRef = this.modalService.open(ModalConfirmationComponent, {centered: true}); - modalRef.componentInstance.header = `Delete API Key: ${apiKey.name}`; - modalRef.componentInstance.message = 'Are you sure you want to delete this API key?'; - modalRef.componentInstance.confirmBtnType = 'delete'; - modalRef.componentInstance.type = 'danger'; - modalRef.componentInstance.confirmBtnText = 'Delete'; - modalRef.componentInstance.confirmBtnIcon = 'icon-cross-circle'; - - modalRef.result.then(reason => { - if (reason === 'ok') { - this.delete(apiKey); - } - }); - } - - delete(apiKey: ApiKeyResponse): void { - this.apiKeyService.delete(apiKey.id).subscribe({ - next: () => { - this.toastService.showSuccess('API key deleted successfully.'); - this.loadKeys(); - }, - error: (err) => { - this.toastService.showError('Error', 'An error occurred while deleting the API key.'); - throw err; - } - }); - } - - getDaysUntilExpire(expiresAt: string): number { - if (!expiresAt) { - return -1; - } - - const today = moment().startOf('day'); - const expireDate = moment(expiresAt).startOf('day'); - return expireDate.diff(today, 'days'); - } - - onSortBy($event: SortEvent) { - this.request.sort = $event.column + ',' + $event.direction; - this.loadKeys(); - } - - maskSecrets(str: string): string { - if (!str || str.length <= 10) { - return str; - } - const prefix = str.substring(0, 10); - const maskLength = str.length - 30; - const maskedPart = '*'.repeat(maskLength); - return prefix + maskedPart; - } - - generateKey(apiKey: ApiKeyResponse): void { - this.generating.push(apiKey.id); - this.apiKeyService.generateApiKey(apiKey.id).subscribe(response => { - this.generatedApiKey = response.body ? response.body : ""; - this.generatedModalRef = this.modalService.open(this.generatedModal, {centered: true}); - const index = this.generating.indexOf(apiKey.id); - if (index > -1) { - this.generating.splice(index, 1); - } - this.loadKeys(); - }); - } - - isApiKeyExpired(expiresAt?: string | null ): boolean { - if (!expiresAt) { - return false; - } - const expirationTime = new Date(expiresAt).getTime(); - return expirationTime < Date.now(); - } - - close() { - this.generatedModalRef.close(); - this.copied = false; - this.generatedApiKey = ''; - } - - loadPage($event: number) { - this.page = $event - 1; - this.request = { - ...this.request, - page: this.page - }; - this.loadKeys(); - } - - onItemsPerPageChange($event: number) { - this.request = { - ...this.request, - size: $event, - page: 0 - }; - this.page = 0; - this.loadKeys(); - } -} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html deleted file mode 100644 index 7e1399bbb..000000000 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - {{ errorMsg }} - - - - - Name - - - - - Expires At - - - - - - - - - - - - Allowed IPs - - - - - - - - - - {{ ipInputError }} - - - 0"> - - - - - - {{ ip.value }} - {{ getIpType(ip.value) }} - - - - - - - - - - - - - - - - - - Cancel - - - - - {{apiKey?.id ? 'Edit Api Key' : 'Create Api Key'}} - - - - - - diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss deleted file mode 100644 index e31427b61..000000000 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss +++ /dev/null @@ -1,12 +0,0 @@ -.disabled-rounded-start { - border-top-left-radius: 0 !important; - border-bottom-left-radius: 0 !important; -} -.disabled-rounded-end { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - -.mt-4 { - margin-top: 9rem !important; -} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts deleted file mode 100644 index 8e83c56b6..000000000 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ /dev/null @@ -1,139 +0,0 @@ -import {HttpErrorResponse} from '@angular/common/http'; -import {Component, Input, OnInit} from '@angular/core'; -import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; -import {ApiKeyResponse} from '../../models/ApiKeyResponse'; -import { ApiKeysService } from '../../service/api-keys.service'; -import {IpFormsValidators} from "../../../../../shared/util/custom-form-validators"; - -@Component({ - selector: 'app-api-key-modal', - templateUrl: './api-key-modal.component.html', - styleUrls: ['./api-key-modal.component.scss'] -}) -export class ApiKeyModalComponent implements OnInit { - - @Input() apiKey: ApiKeyResponse = null; - - apiKeyForm: FormGroup; - ipInput = ''; - loading = false; - errorMsg = ''; - isSaving: string | string[] | Set | { [p: string]: any }; - minDate = { year: new Date().getFullYear(), month: new Date().getMonth() + 1, day: new Date().getDate() }; - ipInputError = ''; - - constructor( public activeModal: NgbActiveModal, - private apiKeyService: ApiKeysService, - private fb: FormBuilder, - private toastService: UtmToastService) { - } - - ngOnInit(): void { - - const expiresAtDate = this.apiKey && this.apiKey.expiresAt ? new Date(this.apiKey.expiresAt) : null; - const expiresAtNgbDate = expiresAtDate ? { - year: expiresAtDate.getUTCFullYear(), - month: expiresAtDate.getUTCMonth() + 1, - day: expiresAtDate.getUTCDate() - } : null; - - this.apiKeyForm = this.fb.group({ - name: [ this.apiKey ? this.apiKey.name : '', Validators.required], - allowedIp: this.fb.array(this.apiKey ? this.apiKey.allowedIp : []), - expiresAt: [expiresAtNgbDate, Validators.required], - }); - } - - get allowedIp(): FormArray { - return this.apiKeyForm.get('allowedIp') as FormArray; - } - - addIp(): void { - const trimmedIp = this.ipInput.trim(); - - if (!trimmedIp) { - this.ipInputError = 'Please enter an IP address or CIDR'; // Error is assigned - return; - } - - const tempControl = this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()]); - - if (tempControl.invalid) { - if (tempControl.hasError('invalidIp')) { - this.ipInputError = 'Invalid IP address format'; - } else if (tempControl.hasError('invalidCidr')) { - this.ipInputError = 'Invalid CIDR format'; - } - return; - } - - const isDuplicate = this.allowedIp.controls.some( - control => control.value === trimmedIp - ); - - if (isDuplicate) { - this.ipInputError = 'This IP is already added'; - return; - } - - this.allowedIp.push(this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()])); - this.ipInput = ''; - this.ipInputError = ''; - } - - removeIp(index: number): void { - this.allowedIp.removeAt(index); - } - - create(): void { - this.errorMsg = ''; - - if (this.apiKeyForm.invalid) { - this.errorMsg = 'Name is required.'; - return; - } - - this.loading = true; - - const rawDate = this.apiKeyForm.get('expiresAt').value; - let formattedDate = rawDate; - - if (rawDate && typeof rawDate === 'object') { - formattedDate = `${rawDate.year}-${String(rawDate.month).padStart(2, '0')}-${String(rawDate.day).padStart(2, '0')}T00:00:00.000Z`; - } - - const payload = { - ...this.apiKeyForm.value, - expiresAt: formattedDate, - }; - - const save = this.apiKey ? this.apiKeyService.update(this.apiKey.id, payload) : - this.apiKeyService.create(payload); - - save.subscribe({ - next: (response) => { - this.loading = false; - this.activeModal.close(response.body as ApiKeyResponse); - }, - error: (err: HttpErrorResponse) => { - this.loading = false; - if (err.status === 409) { - this.toastService.showError('Error', 'An API key with this name already exists.'); - } else if (err.status === 500) { - this.toastService.showError('Error', 'Server error occurred while creating the API key.'); - } - } - }); - } - - getIpType(value: string): string { - if (!value) { return ''; } - if (value.includes('/')) { - return value.includes(':') ? 'IPv6 CIDR' : 'IPv4 CIDR'; - } - return value.includes(':') ? 'IPv6' : 'IPv4'; - } -} - diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts deleted file mode 100644 index 3f6b890d7..000000000 --- a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ApiKeyResponse { - id: string; - name: string; - allowedIp: string[]; - createdAt: string; - expiresAt?: string; - generatedAt?: string; -} diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts deleted file mode 100644 index 7ae630a43..000000000 --- a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ApiKeyUpsert { - id: string; - name: string; - allowedIp?: string[]; - expiresAt?: Date; -} diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts deleted file mode 100644 index e239d5e4d..000000000 --- a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpResponse } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { SERVER_API_URL } from '../../../../app.constants'; -import { ApiKeyResponse } from '../models/ApiKeyResponse'; -import { ApiKeyUpsert } from '../models/ApiKeyUpsert'; -import {createRequestOption} from "../../../../shared/util/request-util"; - -/** - * Service for managing API keys - */ -@Injectable({ - providedIn: 'root' -}) -export class ApiKeysService { - public resourceUrl = SERVER_API_URL + 'api/api-keys'; - - constructor(private http: HttpClient) {} - - /** - * Create a new API key - */ - create(dto: ApiKeyUpsert): Observable> { - return this.http.post( - this.resourceUrl, - dto, - { observe: 'response' } - ); - } - - /** - * Generate (or renew) a plain API key for the given id - * Returns the plain text key (only once) - */ - generate(id: string): Observable> { - return this.http.post( - `${this.resourceUrl}/${id}/generate`, - {}, - { observe: 'response', responseType: 'text' } - ); - } - - /** - * Get API key by id - */ - get(id: string): Observable> { - return this.http.get( - `${this.resourceUrl}/${id}`, - { observe: 'response' } - ); - } - - /** - * List all API keys (with optional pagination) - */ - list(params?: any): Observable> { - const httpParams = createRequestOption(params); - return this.http.get( - this.resourceUrl, - { observe: 'response', params: httpParams }, - ); - } - - /** - * Update an existing API key - */ - update(id: string, dto: ApiKeyUpsert): Observable> { - return this.http.put( - `${this.resourceUrl}/${id}`, - dto, - { observe: 'response' } - ); - } - - /** - * Delete API key - */ - delete(id: string): Observable> { - return this.http.delete( - `${this.resourceUrl}/${id}`, - { observe: 'response' } - ); - } - - generateApiKey(apiKeyId: string): Observable> { - return this.http.post(`${this.resourceUrl}/${apiKeyId}/generate`, null, { - observe: 'response', - responseType: 'text' - }); - } - - /** - * Search API key usage in Elasticsearch - */ - usage(params: { - filters?: any[]; - top: number; - indexPattern: string; - includeChildren?: boolean; - page?: number; - size?: number; - }): Observable { - return this.http.get( - `${this.resourceUrl}/usage`, - { - params: { - top: params.top.toString(), - indexPattern: params.indexPattern, - includeChildren: params.includeChildren.toString() || 'false', - page: params.page.toString() || '0', - size: params.size.toString() || '10' - } - } - ); - } -} - diff --git a/frontend/src/app/app-management/app-management-routing.module.ts b/frontend/src/app/app-management/app-management-routing.module.ts index 5747a2235..73af5c968 100644 --- a/frontend/src/app/app-management/app-management-routing.module.ts +++ b/frontend/src/app/app-management/app-management-routing.module.ts @@ -15,7 +15,6 @@ import {IndexPatternListComponent} from './index-pattern/index-pattern-list/inde import {MenuComponent} from './menu/menu.component'; import {RolloverConfigComponent} from './rollover-config/rollover-config.component'; import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; -import {ApiKeysComponent} from "./api-keys/api-keys.component"; const routes: Routes = [ {path: '', redirectTo: 'settings', pathMatch: 'full'}, @@ -115,16 +114,7 @@ const routes: Routes = [ data: { authorities: [ADMIN_ROLE] }, - }, - { - path: 'api-keys', - component: ApiKeysComponent, - canActivate: [UserRouteAccessService], - data: { - authorities: [ADMIN_ROLE] - }, - } - ], + }], }, ]; diff --git a/frontend/src/app/app-management/app-management.module.ts b/frontend/src/app/app-management/app-management.module.ts index 79ce3a029..6e26285a0 100644 --- a/frontend/src/app/app-management/app-management.module.ts +++ b/frontend/src/app/app-management/app-management.module.ts @@ -11,8 +11,6 @@ import {ComplianceManagementModule} from '../compliance/compliance-management/co import {NavBehavior} from '../shared/behaviors/nav.behavior'; import {VersionUpdateBehavior} from '../shared/behaviors/version-update.behavior'; import {UtmSharedModule} from '../shared/utm-shared.module'; -import {ApiKeysComponent} from './api-keys/api-keys.component'; -import {ApiKeyModalComponent} from './api-keys/shared/components/api-key-modal/api-key-modal.component'; import {AppConfigComponent} from './app-config/app-config.component'; import {AppLogsComponent} from './app-logs/app-logs.component'; import {AppManagementRoutingModule} from './app-management-routing.module'; @@ -48,8 +46,6 @@ import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; @NgModule({ declarations: [ - ApiKeysComponent, - ApiKeyModalComponent, AppManagementComponent, AppManagementSidebarComponent, IndexPatternHelpComponent, @@ -85,7 +81,6 @@ import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; HealthDetailComponent, MenuDeleteDialogComponent, TokenActivateComponent, - ApiKeyModalComponent, IndexDeleteComponent], imports: [ CommonModule, diff --git a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html index 60e12d3be..5d39a9d66 100644 --- a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html +++ b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html @@ -115,16 +115,6 @@ - - - - API Keys - - - - Save collector + Save configuration diff --git a/frontend/src/app/app-module/conf/int-generic-group-config/int-generic-group-config.component.ts b/frontend/src/app/app-module/conf/int-generic-group-config/int-generic-group-config.component.ts index ec445e924..ad85ba394 100644 --- a/frontend/src/app/app-module/conf/int-generic-group-config/int-generic-group-config.component.ts +++ b/frontend/src/app/app-module/conf/int-generic-group-config/int-generic-group-config.component.ts @@ -285,14 +285,12 @@ export class IntGenericGroupConfigComponent implements OnInit { this.configs.push(...configurations); }); const body = { - collectorConfig: { + collector: { + ... collectorDto, + group: null, + }, moduleId: this.moduleId, keys: this.configs - }, - collector: { - ... collectorDto, - group: null, - }, }; this.collectorService.create(body).subscribe(response => { this.savingConfig = false; diff --git a/frontend/src/app/app-module/guides/guide-as400/constants.ts b/frontend/src/app/app-module/guides/guide-as400/constants.ts index 5b1c43b73..45663c52f 100644 --- a/frontend/src/app/app-module/guides/guide-as400/constants.ts +++ b/frontend/src/app/app-module/guides/guide-as400/constants.ts @@ -7,7 +7,7 @@ export const PLATFORM = [ `cd "C:\\Program Files\\UTMStack\\UTMStack Collectors\\AS400"; ` + `& curl.exe -k -o ".\\windows-as400-collector.zip" ` + `"https://V_IP:9001/private/dependencies/collector/windows-as400-collector.zip"; ` + - `Expand-Archive -Path ".\\windows-as400-collector.zip" -DestinationPath "."; ` + + `Expand-Archive -Path ".\\windows-as400-collector.zip" -DestinationPath "."; ` + `Remove-Item ".\\windows-as400-collector.zip"; Start-Process ".\\utmstack_collectors_installer.exe" ` + `-ArgumentList 'install', 'as400', 'V_IP', 'V_TOKEN' -NoNewWindow -Wait`, @@ -34,7 +34,7 @@ export const PLATFORM = [ `https://V_IP:9001/private/dependencies/collector/linux-as400-collector.zip ` + `&& unzip linux-as400-collector.zip && rm linux-as400-collector.zip && chmod -R 755 ` + `utmstack_collectors_installer && ./utmstack_collectors_installer install as400 ` + - `V_IP V_TOKEN"`, + `V_IP V_TOKEN"`, uninstall: `sudo bash -c " cd /opt/utmstack-linux-collectors/as400 && ./utmstack_collectors_installer ` + diff --git a/frontend/src/app/app-module/shared/services/utm-module-collector.service.ts b/frontend/src/app/app-module/shared/services/utm-module-collector.service.ts index 9e1a37398..b5b5fc330 100644 --- a/frontend/src/app/app-module/shared/services/utm-module-collector.service.ts +++ b/frontend/src/app/app-module/shared/services/utm-module-collector.service.ts @@ -20,8 +20,7 @@ export class UtmModuleCollectorService { } create(body: any): Observable> { - const options = createRequestOption(body.collector); - return this.http.post(`${this.resourceUrl}/collector-config/`, body.collectorConfig, {params: options, observe: 'response'}); + return this.http.post(`${this.resourceUrl}/collector-config/`, body, {observe: 'response'}); } update(conf: UtmModuleGroupType): Observable> { diff --git a/frontend/src/app/shared/util/custom-form-validators.ts b/frontend/src/app/shared/util/custom-form-validators.ts deleted file mode 100644 index b82b574d6..000000000 --- a/frontend/src/app/shared/util/custom-form-validators.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms'; - -export class IpFormsValidators { - - static ipOrCidr(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - if (!control.value) { - return null; - } - - const value = control.value.trim(); - - if (value.includes('/')) { - return IpFormsValidators.validateCIDR(value) ? null : { invalidCidr: true }; - } - - const isValidIPv4 = IpFormsValidators.validateIPv4(value); - const isValidIPv6 = IpFormsValidators.validateIPv6(value); - - return (isValidIPv4 || isValidIPv6) ? null : { invalidIp: true }; - }; - } - - private static validateIPv4(ip: string): boolean { - const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; - const match = ip.match(ipv4Regex); - - if (!match) { - return false; - } - - for (let i = 1; i <= 4; i++) { - const octet = parseInt(match[i], 10); - if (octet < 0 || octet > 255) { - return false; - } - } - - return true; - } - - private static validateIPv6(ip: string): boolean { - // tslint:disable-next-line:max-line-length - const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; - - return ipv6Regex.test(ip); - } - - private static validateCIDR(cidr: string): boolean { - const parts = cidr.split('/'); - - if (parts.length !== 2) { - return false; - } - - const [ip, prefix] = parts; - const prefixNum = parseInt(prefix, 10); - - const isIPv4 = ip.includes('.') && !ip.includes(':'); - const isIPv6 = ip.includes(':'); - - if (isIPv4) { - if (!IpFormsValidators.validateIPv4(ip)) { - return false; - } - - if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 32) { - return false; - } - } else if (isIPv6) { - - if (!IpFormsValidators.validateIPv6(ip)) { - return false; - } - - if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) { - return false; - } - } else { - return false; - } - - return true; - } - -} diff --git a/version.yml b/version.yml index f633540cf..e2b56628b 100644 --- a/version.yml +++ b/version.yml @@ -1 +1 @@ -version: 10.9.5 \ No newline at end of file +version: 10.9.4 \ No newline at end of file
{{ maskSecrets(generatedApiKey) }}