ADR-073: Knowledge Security Layer
Status
Proposed
Date
2025-12-27
Context
The knowledge system exposes sensitive information through multiple interfaces:
- MCP Server (port 8765) - AI assistant access
- Docs Server (port 8080) - Web documentation
- Chat API - Conversational interface
Security requirements:
- Authentication - Verify user identity
- Authorization - Domain-based access control (RBAC)
- Rate limiting - Prevent abuse
- Audit logging - Track access for compliance
This ADR focuses on security architecture. Related:
Decision
1. Authentication via OIDC
Use Quarkus OIDC for JWT-based authentication:
# application.properties
quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=${OIDC_SERVER_URL}
quarkus.oidc.client-id=${OIDC_CLIENT_ID}
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
quarkus.oidc.application-type=service
Token validation:
- MCP requests include
Authorization: Bearer <token>header - Docs server supports optional auth (public by default)
- Token contains user roles as claims
2. Role-Based Access Control (RBAC)
Domain access controlled by user roles:
| Role | Accessible Domains |
|---|---|
developer |
idempiere, cloudempiere, angular, mobile |
senior_dev |
All of developer + devops |
support |
support, business |
team_lead |
All domains |
admin |
All domains + admin functions |
Domain-role mapping in database:
CREATE TABLE knowledge_domains (
name VARCHAR(50) PRIMARY KEY,
required_roles TEXT[], -- e.g., ARRAY['developer', 'team_lead']
...
);
DomainAccessService implementation:
@ApplicationScoped
public class DomainAccessService {
private static final Set<String> ADMIN_ROLES = Set.of("admin", "superuser");
private static final Set<String> PUBLIC_DOMAINS = Set.of("idempiere", "support");
public boolean canAccess(List<String> userRoles, String domainName) {
// Admin bypass
if (hasAdminRole(userRoles)) return true;
// Public domains
if (PUBLIC_DOMAINS.contains(domainName)) return true;
// Check domain-specific roles
return domainRepository.findByName(domainName)
.map(domain -> domain.canAccess(userRoles))
.orElse(false);
}
}
Implementation Status: DomainAccessService.java - Implemented
3. Rate Limiting
Prevent abuse with tiered rate limits:
| Tier | Requests/min | Applies To |
|---|---|---|
| Anonymous | 10 | Unauthenticated |
| Authenticated | 60 | Normal users |
| Premium | 300 | Paid/enterprise |
| Unlimited | - | Internal services |
Implementation using Bucket4j:
@ApplicationScoped
public class RateLimiter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
public boolean tryConsume(String clientId, RateTier tier) {
Bucket bucket = buckets.computeIfAbsent(clientId,
k -> createBucket(tier));
return bucket.tryConsume(1);
}
private Bucket createBucket(RateTier tier) {
return Bucket.builder()
.addLimit(Bandwidth.classic(tier.requestsPerMinute,
Refill.greedy(tier.requestsPerMinute, Duration.ofMinutes(1))))
.build();
}
}
4. Audit Logging
Track all knowledge access:
CREATE TABLE knowledge_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(255),
action VARCHAR(50) NOT NULL, -- 'search', 'read', 'ingest'
domain VARCHAR(50),
resource_path TEXT,
request_params JSONB,
response_size INTEGER,
latency_ms INTEGER,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP DEFAULT now()
);
CREATE INDEX idx_audit_user ON knowledge_audit_log(user_id, created_at);
CREATE INDEX idx_audit_domain ON knowledge_audit_log(domain, created_at);
Audit service:
@ApplicationScoped
public class KnowledgeAuditService {
public void logAccess(AuditEvent event) {
// Async insert to avoid blocking
executor.submit(() -> auditRepository.insert(event));
}
public void logSearch(String userId, String query, List<String> domains) {
logAccess(AuditEvent.builder()
.userId(userId)
.action("search")
.requestParams(Map.of("query", query, "domains", domains))
.build());
}
}
5. MCP Security Configuration
For MCP server specifically:
# application-mcp.properties
quarkus.http.port=8765
# OIDC for MCP
quarkus.oidc.enabled=true
quarkus.http.auth.basic=false
# CORS for Claude Desktop
quarkus.http.cors=true
quarkus.http.cors.origins=*
# Rate limiting
app.rate-limit.enabled=true
app.rate-limit.default-tier=authenticated
6. Security Layers by Interface
| Interface | Auth | Rate Limit | Audit |
|---|---|---|---|
| MCP Server | OIDC (required) | Per-token | Full |
| Docs Server | Optional | Per-IP | Read-only |
| Chat API | OIDC (required) | Per-user | Full |
| CLI | API key | None | Commands only |
Implementation Status
| Component | File | Status |
|---|---|---|
| DomainAccessService | DomainAccessService.java |
Implemented |
| Rate Limiter | RateLimiter.java |
Implemented |
| MCP Rate Limiting | McpKnowledgeTools.java |
Wired |
| Audit Service | KnowledgeAuditService.java |
Not implemented |
| OIDC Config | application.properties |
Not configured |
| Audit Table | V1__...schema.sql |
Schema created |
RateLimiter Implementation
The RateLimiter.java provides in-memory token bucket rate limiting:
// Usage
@Inject RateLimiter rateLimiter;
// Check and consume
if (rateLimiter.tryConsume(clientId, RateLimiter.Tier.AUTHENTICATED)) {
// Request allowed
} else {
// Rate limited - return 429
}
// Check with details
RateLimiter.RateLimitResult result = rateLimiter.check(clientId, tier);
if (!result.allowed()) {
// Include Retry-After header: result.retryAfterSeconds()
}
Configuration:
app.rate-limit.enabled=true
app.rate-limit.default-tier=AUTHENTICATED
MCP Rate Limiting Integration
Rate limiting is wired into McpKnowledgeTools.java for key search operations:
// Rate-limited MCP tools (ADR-073)
- searchKnowledge // Main search tool
- askSupport // Support question answering
- searchDomain // Multi-domain federated search
// Usage in MCP tool methods:
ToolResponse rateLimitError = checkRateLimit("searchKnowledge");
if (rateLimitError != null) {
return rateLimitError; // Returns 429-style error to AI client
}
Client Identification:
- Uses Vert.x context to extract client info
- Prefers Authorization header hash for authenticated clients
- Falls back to IP address for anonymous clients
- Default:
mcp-anonymouswhen context unavailable
Consequences
Benefits
- Security - Protected access to sensitive domains
- Compliance - Audit trail for access
- Fairness - Rate limiting prevents abuse
- Flexibility - Per-domain access control
Drawbacks
- Complexity - Additional infrastructure (OIDC provider)
- Latency - Token validation adds overhead
- Operations - Need to manage tokens, roles
Next Steps
- ✅ ~~Implement RateLimiter~~ (in-memory token bucket)
- ✅ ~~Wire rate limiting to MCP tools~~ (searchKnowledge, askSupport, searchDomain)
- Configure OIDC provider (Keycloak or Auth0)
- Implement KnowledgeAuditService
- Add @RolesAllowed annotations to endpoints
- Configure MCP server auth
References
- Source:
docs/rag-complex-for-quarkus/mcp-security-architecture.md - Implementation:
src/main/java/org/idempiere/cli/rag/security/ - Quarkus OIDC: https://quarkus.io/guides/security-oidc
- Parent: ADR-068