Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# UTMStack 10.9.3 Release Notes
# UTMStack 10.9.5 Release Notes

- Enriched the TIMEZONES constant to include additional IANA zones for broader coverage.
- Support for additional syslog framing methods (RFC 5424 octet counting).
– 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.
7 changes: 6 additions & 1 deletion backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.7</version>
<version>1.6.15</version>
</dependency>
<dependency>
<groupId>com.utmstack</groupId>
Expand Down Expand Up @@ -351,6 +351,11 @@
<artifactId>tika-core</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.9.0</version>
</dependency>
</dependencies>

<build>
Expand Down
6 changes: 6 additions & 0 deletions backend/src/main/java/com/park/utmstack/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

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 {
Expand Down Expand Up @@ -128,6 +130,7 @@ 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";
Expand All @@ -140,6 +143,9 @@ public final class Constants {
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<String> API_ENDPOINT_IGNORE = Collections.emptyList();


private Constants() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ 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()
Expand All @@ -36,7 +39,7 @@ public OpenAPI customOpenAPI() {
.scheme("bearer")
.bearerFormat("JWT"))
.addSecuritySchemes(securitySchemeApiKey, new SecurityScheme()
.name("Utm-Internal-Key")
.name(Constants.API_KEY_HEADER)
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)))
.info(new Info().title(apiTitle).version(version))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
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;
Expand All @@ -29,6 +32,7 @@
import javax.servlet.http.HttpServletResponse;

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Import(SecurityProblemSupport.class)
Expand All @@ -39,17 +43,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final CorsFilter corsFilter;
private final InternalApiKeyProvider internalApiKeyProvider;

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;
}
private final ApiKeyFilter apiKeyFilter;

@PostConstruct
public void init() {
Expand Down Expand Up @@ -122,7 +116,10 @@ public void configure(HttpSecurity http) throws Exception {
.and()
.apply(securityConfigurerAdapterForJwt())
.and()
.apply(securityConfigurerAdapterForInternalApiKey());
.apply(securityConfigurerAdapterForInternalApiKey())
.and()
.apply(securityConfigurerAdapterForApiKey()) ;


}

Expand All @@ -133,4 +130,9 @@ private JWTConfigurer securityConfigurerAdapterForJwt() {
private InternalApiKeyConfigurer securityConfigurerAdapterForInternalApiKey() {
return new InternalApiKeyConfigurer(internalApiKeyProvider);
}

private ApiKeyConfigurer securityConfigurerAdapterForApiKey() {
return new ApiKeyConfigurer(apiKeyFilter);
}

}
2 changes: 1 addition & 1 deletion backend/src/main/java/com/park/utmstack/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public class User extends AbstractAuditingEntity implements Serializable {
private Boolean defaultPassword;

@JsonIgnore
@ManyToMany
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "jhi_user_authority", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")})

@BatchSize(size = 20)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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<String, Object> toAuditMap() {
Map<String, Object> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@ public enum ApplicationEventType {
ERROR,
WARNING,
INFO,
MODULE_ACTIVATION_ATTEMPT, MODULE_ACTIVATION_SUCCESS, UNDEFINED
MODULE_ACTIVATION_ATTEMPT,
MODULE_ACTIVATION_SUCCESS,
API_KEY_ACCESS_SUCCESS,
API_KEY_ACCESS_FAILURE,
UNDEFINED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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();
}
}
Loading
Loading