Skip to content

Commit 497e674

Browse files
committed
feat(api-keys): implement API key management with creation, retrieval, update, and deletion functionalities
1 parent a574169 commit 497e674

File tree

24 files changed

+1083
-17
lines changed

24 files changed

+1083
-17
lines changed

backend/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@
273273
<dependency>
274274
<groupId>org.springdoc</groupId>
275275
<artifactId>springdoc-openapi-ui</artifactId>
276-
<version>1.6.7</version>
276+
<version>1.6.15</version>
277277
</dependency>
278278
<dependency>
279279
<groupId>com.utmstack</groupId>
@@ -351,6 +351,11 @@
351351
<artifactId>tika-core</artifactId>
352352
<version>2.9.1</version>
353353
</dependency>
354+
<dependency>
355+
<groupId>commons-net</groupId>
356+
<artifactId>commons-net</artifactId>
357+
<version>3.9.0</version>
358+
</dependency>
354359
</dependencies>
355360

356361
<build>

backend/src/main/java/com/park/utmstack/config/Constants.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern;
44

5+
import java.util.Collections;
56
import java.util.HashMap;
7+
import java.util.List;
68
import java.util.Map;
79

810
public final class Constants {
@@ -128,6 +130,7 @@ public final class Constants {
128130
// Defines the index pattern for querying Elasticsearch statistics indexes.
129131
// ----------------------------------------------------------------------------------
130132
public static final String STATISTICS_INDEX_PATTERN = "v11-statistics-*";
133+
public static final String API_ACCESS_LOGS = ".utmstack-api-logs";
131134

132135
// Logging
133136
public static final String TRACE_ID_KEY = "traceId";
@@ -140,6 +143,9 @@ public final class Constants {
140143
public static final String CAUSE_KEY = "cause";
141144
public static final String LAYER_KEY = "layer";
142145

146+
public static final String API_KEY_HEADER = "Utm-Api-Key";
147+
public static final List<String> API_ENDPOINT_IGNORE = Collections.emptyList();
148+
143149

144150
private Constants() {
145151
}

backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ public OpenApiConfiguration(InfoEndpoint infoEndpoint) {
2424
public OpenAPI customOpenAPI() {
2525
final String securitySchemeBearer = "bearerAuth";
2626
final String securitySchemeApiKey = "ApiKeyAuth";
27+
2728
final String apiTitle = "UTMStack API";
2829
String version = MapUtil.flattenToStringMap(infoEndpoint.info(), true).get("build.version");
2930
return new OpenAPI()
30-
.addSecurityItem(new SecurityRequirement().addList(securitySchemeBearer).addList(securitySchemeApiKey))
31+
.addSecurityItem(new SecurityRequirement()
32+
.addList(securitySchemeBearer)
33+
.addList(securitySchemeApiKey))
3134
.components(new Components()
3235
.addSecuritySchemes(securitySchemeBearer,
3336
new SecurityScheme()
@@ -36,7 +39,7 @@ public OpenAPI customOpenAPI() {
3639
.scheme("bearer")
3740
.bearerFormat("JWT"))
3841
.addSecuritySchemes(securitySchemeApiKey, new SecurityScheme()
39-
.name("Utm-Internal-Key")
42+
.name(Constants.API_KEY_HEADER)
4043
.type(SecurityScheme.Type.APIKEY)
4144
.in(SecurityScheme.In.HEADER)))
4245
.info(new Info().title(apiTitle).version(version))

backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.park.utmstack.config;
22

33
import com.park.utmstack.security.AuthoritiesConstants;
4+
import com.park.utmstack.security.api_key.ApiKeyConfigurer;
5+
import com.park.utmstack.security.api_key.ApiKeyFilter;
46
import com.park.utmstack.security.internalApiKey.InternalApiKeyConfigurer;
57
import com.park.utmstack.security.internalApiKey.InternalApiKeyProvider;
68
import com.park.utmstack.security.jwt.JWTConfigurer;
79
import com.park.utmstack.security.jwt.TokenProvider;
10+
import lombok.RequiredArgsConstructor;
811
import org.springframework.beans.factory.BeanInitializationException;
912
import org.springframework.context.annotation.Bean;
1013
import org.springframework.context.annotation.Configuration;
@@ -29,6 +32,7 @@
2932
import javax.servlet.http.HttpServletResponse;
3033

3134
@Configuration
35+
@RequiredArgsConstructor
3236
@EnableWebSecurity
3337
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
3438
@Import(SecurityProblemSupport.class)
@@ -39,17 +43,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
3943
private final TokenProvider tokenProvider;
4044
private final CorsFilter corsFilter;
4145
private final InternalApiKeyProvider internalApiKeyProvider;
42-
43-
public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder,
44-
UserDetailsService userDetailsService,
45-
TokenProvider tokenProvider,
46-
CorsFilter corsFilter, InternalApiKeyProvider internalApiKeyProvider) {
47-
this.authenticationManagerBuilder = authenticationManagerBuilder;
48-
this.userDetailsService = userDetailsService;
49-
this.tokenProvider = tokenProvider;
50-
this.corsFilter = corsFilter;
51-
this.internalApiKeyProvider = internalApiKeyProvider;
52-
}
46+
private final ApiKeyFilter apiKeyFilter;
5347

5448
@PostConstruct
5549
public void init() {
@@ -122,7 +116,10 @@ public void configure(HttpSecurity http) throws Exception {
122116
.and()
123117
.apply(securityConfigurerAdapterForJwt())
124118
.and()
125-
.apply(securityConfigurerAdapterForInternalApiKey());
119+
.apply(securityConfigurerAdapterForInternalApiKey())
120+
.and()
121+
.apply(securityConfigurerAdapterForApiKey()) ;
122+
126123

127124
}
128125

@@ -133,4 +130,9 @@ private JWTConfigurer securityConfigurerAdapterForJwt() {
133130
private InternalApiKeyConfigurer securityConfigurerAdapterForInternalApiKey() {
134131
return new InternalApiKeyConfigurer(internalApiKeyProvider);
135132
}
133+
134+
private ApiKeyConfigurer securityConfigurerAdapterForApiKey() {
135+
return new ApiKeyConfigurer(apiKeyFilter);
136+
}
137+
136138
}

backend/src/main/java/com/park/utmstack/domain/User.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public class User extends AbstractAuditingEntity implements Serializable {
9090
private Boolean defaultPassword;
9191

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

9696
@BatchSize(size = 20)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.park.utmstack.domain.api_keys;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import javax.persistence.*;
9+
import java.io.Serializable;
10+
import java.time.Instant;
11+
import java.util.UUID;
12+
13+
@Data
14+
@Builder
15+
@NoArgsConstructor
16+
@AllArgsConstructor
17+
@Entity
18+
@Table(name = "api_keys")
19+
public class ApiKey implements Serializable {
20+
21+
@Id
22+
@GeneratedValue(strategy = GenerationType.IDENTITY)
23+
private Long id;
24+
25+
@Column(nullable = false)
26+
private Long userId;
27+
28+
@Column(nullable = false)
29+
private String name;
30+
31+
@Column(nullable = false)
32+
private String apiKey;
33+
34+
@Column
35+
private String allowedIp;
36+
37+
@Column(nullable = false)
38+
private Instant createdAt;
39+
40+
private Instant generatedAt;
41+
42+
@Column
43+
private Instant expiresAt;
44+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.park.utmstack.domain.api_keys;
2+
3+
import com.park.utmstack.service.dto.auditable.AuditableDTO;
4+
import lombok.*;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
@Builder
10+
@Getter
11+
@Setter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class ApiKeyUsageLog implements AuditableDTO {
15+
16+
private String id;
17+
private String apiKeyId;
18+
private String apiKeyName;
19+
private String userId;
20+
private String timestamp;
21+
private String endpoint;
22+
private String address;
23+
private String errorMessage;
24+
private String queryParams;
25+
private String payload;
26+
private String userAgent;
27+
private String httpMethod;
28+
private String statusCode;
29+
30+
@Override
31+
public Map<String, Object> toAuditMap() {
32+
Map<String, Object> map = new HashMap<>();
33+
34+
map.put("id", id);
35+
map.put("api_key_id", apiKeyId);
36+
map.put("api_key_name", apiKeyName);
37+
map.put("user_id", userId);
38+
map.put("timestamp", timestamp != null ? timestamp : null);
39+
map.put("endpoint", endpoint);
40+
map.put("address", address);
41+
map.put("error_message", errorMessage);
42+
map.put("query_params", queryParams);
43+
map.put("user_agent", userAgent);
44+
map.put("http_method", httpMethod);
45+
map.put("status_code", statusCode);
46+
47+
return map;
48+
}
49+
}

backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,9 @@ public enum ApplicationEventType {
4242
ERROR,
4343
WARNING,
4444
INFO,
45-
MODULE_ACTIVATION_ATTEMPT, MODULE_ACTIVATION_SUCCESS, UNDEFINED
45+
MODULE_ACTIVATION_ATTEMPT,
46+
MODULE_ACTIVATION_SUCCESS,
47+
API_KEY_ACCESS_SUCCESS,
48+
API_KEY_ACCESS_FAILURE,
49+
UNDEFINED
4650
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package com.park.utmstack.loggin.api_key;
2+
3+
import com.park.utmstack.domain.api_keys.ApiKey;
4+
import com.park.utmstack.domain.api_keys.ApiKeyUsageLog;
5+
import com.park.utmstack.domain.application_events.enums.ApplicationEventType;
6+
import com.park.utmstack.service.api_key.ApiKeyService;
7+
import com.park.utmstack.service.application_events.ApplicationEventService;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.util.StringUtils;
12+
import org.springframework.web.util.ContentCachingRequestWrapper;
13+
import org.springframework.web.util.ContentCachingResponseWrapper;
14+
15+
import javax.servlet.http.HttpServletRequest;
16+
import javax.servlet.http.HttpServletResponse;
17+
import java.nio.charset.StandardCharsets;
18+
import java.time.Instant;
19+
import java.util.UUID;
20+
21+
import static org.postgresql.PGProperty.APPLICATION_NAME;
22+
23+
@Service
24+
@Slf4j
25+
@RequiredArgsConstructor
26+
public class ApiKeyUsageLoggingService {
27+
28+
private final ApiKeyService apiKeyService;
29+
private final ApplicationEventService applicationEventService;
30+
private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE";
31+
32+
public void logUsage(ContentCachingRequestWrapper request,
33+
ContentCachingResponseWrapper response,
34+
ApiKey apiKey,
35+
String ipAddress,
36+
String message) {
37+
38+
if (Boolean.TRUE.equals(request.getAttribute(LOG_USAGE_FLAG))) {
39+
return;
40+
}
41+
42+
try {
43+
String payload = extractPayload(request);
44+
String errorText = extractErrorText(response);
45+
int status = safeStatus(response);
46+
47+
ApiKeyUsageLog usage = buildUsageLog(apiKey, ipAddress, request, status, errorText, payload);
48+
49+
apiKeyService.logUsage(usage);
50+
51+
ApplicationEventType eventType = (status >= 400)
52+
? ApplicationEventType.API_KEY_ACCESS_FAILURE
53+
: ApplicationEventType.API_KEY_ACCESS_SUCCESS;
54+
55+
String eventMessage = (status >= 400)
56+
? "API key access failure"
57+
: "API key access";
58+
59+
applicationEventService.createEvent(eventMessage, eventType, usage.toAuditMap());
60+
61+
} catch (Exception e) {
62+
log.error("Error while logging API key usage: {}", e.getMessage(), e);
63+
} finally {
64+
request.setAttribute(LOG_USAGE_FLAG, Boolean.TRUE);
65+
}
66+
}
67+
68+
private int safeStatus(HttpServletResponse response) {
69+
try {
70+
return response.getStatus();
71+
} catch (Exception e) {
72+
return 0;
73+
}
74+
}
75+
76+
private String extractPayload(ContentCachingRequestWrapper request) {
77+
try {
78+
if (!"GET".equalsIgnoreCase(request.getMethod()) && !"DELETE".equalsIgnoreCase(request.getMethod())) {
79+
byte[] content = request.getContentAsByteArray();
80+
return content.length > 0 ? new String(content, StandardCharsets.UTF_8) : null;
81+
}
82+
} catch (Exception ex) {
83+
log.error("Error extracting payload: {}", ex.getMessage());
84+
}
85+
return null;
86+
}
87+
88+
private String extractErrorText(ContentCachingResponseWrapper response) {
89+
int statusCode = response.getStatus();
90+
if (statusCode >= 400) {
91+
byte[] content = response.getContentAsByteArray();
92+
String responseError = content.length > 0 ? new String(content, StandardCharsets.UTF_8) : null;
93+
String errorHeader = response.getHeader("X-" + APPLICATION_NAME + "-error");
94+
return StringUtils.hasText(responseError) ? responseError : errorHeader;
95+
}
96+
return null;
97+
}
98+
99+
private ApiKeyUsageLog buildUsageLog(ApiKey apiKey,
100+
String ipAddress,
101+
HttpServletRequest request,
102+
int status,
103+
String errorText,
104+
String payload) {
105+
106+
String id = UUID.randomUUID().toString();
107+
String apiKeyId = apiKey != null && apiKey.getId() != null ? apiKey.getId().toString() : null;
108+
String apiKeyName = apiKey != null ? apiKey.getName() : null;
109+
String userId = apiKey != null && apiKey.getUserId() != null ? apiKey.getUserId().toString() : null;
110+
String timestamp = Instant.now().toString();
111+
String endpoint = request != null ? request.getRequestURI() : null;
112+
String queryParams = request != null ? request.getQueryString() : null;
113+
String userAgent = request != null ? request.getHeader("User-Agent") : null;
114+
String httpMethod = request != null ? request.getMethod() : null;
115+
String statusCode = String.valueOf(status);
116+
117+
String safePayload = null;
118+
if (payload != null) {
119+
int PAYLOAD_MAX_LENGTH = 2000;
120+
safePayload = payload.length() > PAYLOAD_MAX_LENGTH ? payload.substring(0, PAYLOAD_MAX_LENGTH) : payload;
121+
}
122+
123+
return ApiKeyUsageLog.builder()
124+
.id(id)
125+
.apiKeyId(apiKeyId)
126+
.apiKeyName(apiKeyName)
127+
.userId(userId)
128+
.timestamp(timestamp)
129+
.endpoint(endpoint)
130+
.address(ipAddress)
131+
.errorMessage(errorText)
132+
.queryParams(queryParams)
133+
.payload(safePayload)
134+
.userAgent(userAgent)
135+
.httpMethod(httpMethod)
136+
.statusCode(statusCode)
137+
.build();
138+
}
139+
}

0 commit comments

Comments
 (0)