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
78 changes: 78 additions & 0 deletions core/src/main/java/com/opencontext/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.opencontext.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

Copilot AI Aug 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import for UsernamePasswordAuthenticationFilter is unused since the API key authentication filter is commented out. This unused import should be removed to maintain clean code.

Suggested change
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

Copilot uses AI. Check for mistakes.

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* Spring Security configuration for OpenContext API.
* Implements API Key authentication for Admin APIs and allows unrestricted access
* to MCP APIs and development endpoints.
*/
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final Environment environment;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("Configuring Spring Security with profile-specific rules");

http
// Disable CSRF for REST API
.csrf(AbstractHttpConfigurer::disable)

// Disable form login and basic auth
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)

// Stateless session management for REST API
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// Configure authorization rules
.authorizeHttpRequests(authz -> authz
// Allow health check endpoints
.requestMatchers("/actuator/health/**", "/actuator/info").permitAll()

// Allow Swagger UI and API documentation (development)
.requestMatchers(
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/webjars/**"
).permitAll()

// Allow MCP API endpoints (no authentication required per PRD)
.requestMatchers("/api/v1/search/**", "/api/v1/get-content/**").permitAll()

// Allow development mock data endpoints (development only)
.requestMatchers("/api/v1/dev/**").permitAll()

// Admin APIs will require API Key authentication (TODO: implement in next step)
.requestMatchers("/api/v1/sources/**").permitAll() // Temporary - will add API key auth

// All other requests require authentication
.anyRequest().authenticated()
);

// TODO: Add API Key authentication filter for Admin APIs
// http.addFilterBefore(apiKeyAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

log.info("Security configuration completed successfully");
return http.build();
}

Copilot AI Aug 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented-out API key authentication filter import and usage on line 11 and 73 indicates incomplete security implementation. Admin APIs at /api/v1/sources/** are currently allowing unrestricted access (line 66) which poses a security risk in production environments.

Suggested change
}
.requestMatchers("/api/v1/sources/**").authenticated() // Require authentication (API key)
// All other requests require authentication
.anyRequest().authenticated()
);
// Add API Key authentication filter for Admin APIs
http.addFilterBefore(apiKeyAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
log.info("Security configuration completed successfully");
return http.build();
}
/**
* Simple API Key Authentication Filter for Admin APIs.
* Checks for X-API-KEY header and authenticates if valid.
*/
private class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
private final AntPathRequestMatcher adminApiMatcher = new AntPathRequestMatcher("/api/v1/sources/**");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (adminApiMatcher.matches(request)) {
String apiKey = request.getHeader(API_KEY_HEADER);
if (ADMIN_API_KEY.equals(apiKey)) {
Authentication auth = new PreAuthenticatedAuthenticationToken(
"admin", apiKey, Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN")));
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: Invalid API Key");
return;
}
}
filterChain.doFilter(request, response);
}
}

Copilot uses AI. Check for mistakes.
}
68 changes: 68 additions & 0 deletions core/src/main/java/com/opencontext/dto/ChunkMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.opencontext.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.PositiveOrZero;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

/**
* Hierarchical and contextual metadata for document chunks.
* Supports document structure navigation and search result organization.
*/
@Schema(description = "Hierarchical and contextual metadata for document chunks")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ChunkMetadata {

/**
* Title or heading of this chunk.
* Used for displaying chunk context to users in search results.
*/
@Schema(description = "Title or heading of the chunk", example = "5.8.2. JWT Authentication Converter")
private String title;

/**
* Hierarchical breadcrumbs from document root to this chunk.
* Stored as array for flexibility in UI display and filtering.
* ES keyword type naturally supports arrays.
*/
@Schema(description = "Breadcrumb path from root to current chunk",
example = "[\"Chapter 5\", \"Security\", \"JWT\", \"Configuration\"]")
private List<String> breadcrumbs;

/**
* Depth level in document hierarchy (0 for root chunks).
* Used for hierarchy-aware search and display organization.
*/
@Schema(description = "Hierarchical depth level (0 for root)", example = "2")
@PositiveOrZero
private Integer hierarchyLevel;

/**
* Sequential order within document structure.
* Maintains original document flow for coherent retrieval.
*/
@Schema(description = "Sequential position in document", example = "15")
@PositiveOrZero
private Integer sequenceInDocument;

/**
* Document language for proper text processing.
* Currently supports Korean (ko) and English (en).
*/
@Schema(description = "Document language code", example = "ko")
private String language;

/**
* Source file type for context and processing hints.
*/
@Schema(description = "Source file type", example = "PDF")
private String fileType;
}
38 changes: 38 additions & 0 deletions core/src/main/java/com/opencontext/dto/GetContentResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.opencontext.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* Response DTO for the get_content API endpoint.
* Provides complete chunk content with token information for LLM context management.
*/
@Schema(description = "Response for focused content retrieval (get_content)")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class GetContentResponse {

/**
* Complete text content of the requested chunk.
* May be truncated if exceeds maxTokens limit, but still returns 200 OK.
*/
@Schema(description = "Complete chunk content (may be token-limited)",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
private String content;

/**
* Token processing information for LLM context management.
*/
@Schema(description = "Token processing information", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private TokenInfo tokenInfo;
}
67 changes: 67 additions & 0 deletions core/src/main/java/com/opencontext/dto/SearchResultItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.opencontext.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.UUID;

/**
* Individual search result item for exploratory knowledge discovery.
* Contains essential information for users to identify and select relevant chunks.
*/
@Schema(description = "Individual search result item for exploratory search (find_knowledge)")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class SearchResultItem {

/**
* Unique identifier of the chunk for subsequent get_content requests.
*/
@Schema(description = "Chunk identifier for content retrieval", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private UUID chunkId;

/**
* Title or heading of the chunk for user context.
*/
@Schema(description = "Chunk title or heading", example = "5.8.2. JWT Authentication Converter")
private String title;

/**
* Content preview generated by extracting first 50 characters + "...".
* Following PRD specification for simple extraction over AI summarization.
*/
@Schema(description = "Content preview (first 50 chars + '...')",
example = "To customize the conversion from a JWT to an Auth...")
private String snippet;

/**
* Relevance score from hybrid search (BM25 + Vector similarity).
* Normalized to 0.0-1.0 range for consistent interpretation.
* Always present in search results.
*/
@Schema(description = "Search relevance score", example = "0.92", minimum = "0.0", maximum = "1.0")
@DecimalMin(value = "0.0")
@DecimalMax(value = "1.0")
@NotNull
private Double relevanceScore;

/**
* Hierarchical breadcrumbs for user navigation context.
* Helps users understand the chunk's position within document structure.
* Optional field that enhances UX when available.
*/
@Schema(description = "Breadcrumb path for navigation context",
example = "[\"Chapter 5\", \"Security\", \"JWT\", \"Configuration\"]")
private List<String> breadcrumbs;
}
71 changes: 71 additions & 0 deletions core/src/main/java/com/opencontext/dto/StructuredChunk.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.opencontext.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.UUID;

/**
* Structured document chunk for Elasticsearch indexing and storage.
* Contains content, embeddings, and hierarchical metadata for semantic search.
*/
@Schema(description = "Elasticsearch document structure for structured chunks")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class StructuredChunk {

/**
* Unique identifier for the chunk, identical to PostgreSQL document_chunks.id.
* This maintains data consistency between relational and search stores.
*/
@Schema(description = "Unique chunk identifier", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private UUID chunkId;

/**
* UUID of the source document this chunk belongs to.
* Used for filtering and document-level operations.
*/
@Schema(description = "Source document identifier", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private UUID sourceDocumentId;

/**
* Original filename of the source document.
* Provides context for users to identify document source.
*/
@Schema(description = "Original filename of source document", example = "spring-security-guide.pdf")
private String originalFilename;

/**
* The actual text content of this chunk.
* This is the primary searchable content processed by Korean Nori analyzer.
*/
@Schema(description = "Text content of the chunk", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
private String content;

/**
* 1024-dimensional embedding vector for semantic search.
* Generated by Qwen3-Embedding-0.6B model via Ollama.
* Uses float[] for optimal performance with Elasticsearch dense_vector type.
*/
@Schema(description = "Embedding vector for semantic search (1024 dimensions)")

Copilot AI Aug 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embedding field lacks validation to ensure it contains exactly 1024 dimensions as described in the documentation comment. Consider adding a validation annotation like @Size(min = 1024, max = 1024) to enforce the documented constraint.

Suggested change
@Schema(description = "Embedding vector for semantic search (1024 dimensions)")
@Schema(description = "Embedding vector for semantic search (1024 dimensions)")
@jakarta.validation.constraints.Size(min = 1024, max = 1024)

Copilot uses AI. Check for mistakes.
private float[] embedding;

/**
* Metadata containing hierarchical and contextual information.
* Separated into its own class following PRD coding standards.
*/
@Schema(description = "Chunk metadata including hierarchy and context")
@NotNull
private ChunkMetadata metadata;
}
40 changes: 40 additions & 0 deletions core/src/main/java/com/opencontext/dto/TokenInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.opencontext.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.PositiveOrZero;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* Token processing information for LLM context management.
*/
@Schema(description = "Token processing information for LLM context management")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class TokenInfo {

/**
* Name of the tokenizer used for token counting.
* Currently fixed to tiktoken-cl100k_base but extensible for future LLM support.
*/
@Schema(description = "Tokenizer used for counting", example = "tiktoken-cl100k_base",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
private String tokenizer;

/**
* Actual number of tokens in the returned content.
* May be less than original if content was truncated due to maxTokens limit.
* Can be 0 in extreme cases (empty content after processing).
*/
@Schema(description = "Actual token count of returned content", example = "7999",
requiredMode = Schema.RequiredMode.REQUIRED)
@PositiveOrZero
private Integer actualTokens;
}
6 changes: 5 additions & 1 deletion core/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ spring:
# JPA Configuration
jpa:
hibernate:
ddl-auto: create-drop
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true

# Flyway Configuration
flyway:
baseline-on-migrate: true

# Servlet Configuration
servlet:
multipart:
Expand Down