[Performance]Add Token Management (#55)
* 0.5.1 Version * fix 0.5.1 schema async bug * fix security bug * fix security bug * Add complete Token, JWT, OAuth authentication system * Add complete Token, JWT, OAuth authentication system * Add complete Token, JWT, OAuth authentication system * Add complete Token, JWT, OAuth authentication system * Add a controllable MCP Server DB Pool permission authentication system, connect it with the Doris permission system, and provide it to enterprise-level applications concurrently with the multi-Worker mode. * Add Tokens Management
This commit is contained in:
149
.env.example
149
.env.example
@@ -1,3 +1,19 @@
|
|||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Doris MCP Server Environment Configuration Example
|
# Doris MCP Server Environment Configuration Example
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
@@ -64,6 +80,35 @@ ENABLE_TOKEN_EXPIRY=true
|
|||||||
DEFAULT_TOKEN_EXPIRY_HOURS=720
|
DEFAULT_TOKEN_EXPIRY_HOURS=720
|
||||||
TOKEN_HASH_ALGORITHM=sha256
|
TOKEN_HASH_ALGORITHM=sha256
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Token Management Security Configuration (NEW in v0.6.0) - CRITICAL SECURITY SETTINGS
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# HTTP Token Management Endpoints (DISABLED BY DEFAULT FOR SECURITY)
|
||||||
|
# WARNING: These endpoints allow creation, deletion, and management of authentication tokens
|
||||||
|
# Only enable if you need HTTP-based token management and understand the security implications
|
||||||
|
ENABLE_HTTP_TOKEN_MANAGEMENT=true
|
||||||
|
|
||||||
|
# Admin Authentication Token (REQUIRED if HTTP token management is enabled)
|
||||||
|
# This token is required to access HTTP token management endpoints
|
||||||
|
# SECURITY: Generate a secure random token in production - NEVER use default values
|
||||||
|
TOKEN_MANAGEMENT_ADMIN_TOKEN=
|
||||||
|
|
||||||
|
# IP Address Restrictions for Token Management (CRITICAL SECURITY CONTROL)
|
||||||
|
# Only these IP addresses/networks can access token management endpoints
|
||||||
|
# DEFAULT: localhost only (most secure) - add other IPs/networks only if necessary
|
||||||
|
# Format: comma-separated list of IPs and CIDR networks
|
||||||
|
# Examples:
|
||||||
|
# - Localhost only: 127.0.0.1,::1
|
||||||
|
# - Private network: 127.0.0.1,192.168.1.0/24,10.0.0.0/8
|
||||||
|
# - Specific IPs: 127.0.0.1,192.168.1.10,192.168.1.11
|
||||||
|
TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1
|
||||||
|
|
||||||
|
# Require Admin Authentication (ENABLED BY DEFAULT FOR SECURITY)
|
||||||
|
# When true, all token management operations require valid admin token
|
||||||
|
# When false, only IP restrictions apply (NOT RECOMMENDED for production)
|
||||||
|
REQUIRE_ADMIN_AUTH=true
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# JWT Authentication Configuration (Enable with ENABLE_JWT_AUTH=true)
|
# JWT Authentication Configuration (Enable with ENABLE_JWT_AUTH=true)
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
@@ -318,6 +363,13 @@ TEMP_FILES_DIR=tmp
|
|||||||
# - Must change TOKEN_SECRET in production environment (legacy compatibility)
|
# - Must change TOKEN_SECRET in production environment (legacy compatibility)
|
||||||
# - Adjust BLOCKED_KEYWORDS according to business needs
|
# - Adjust BLOCKED_KEYWORDS according to business needs
|
||||||
# - Enable ENABLE_SECURITY_CHECK and ENABLE_MASKING
|
# - Enable ENABLE_SECURITY_CHECK and ENABLE_MASKING
|
||||||
|
# - NEW v0.6.0: Token Management Security (CRITICAL):
|
||||||
|
# * ENABLE_HTTP_TOKEN_MANAGEMENT=false by default (SECURE BY DEFAULT)
|
||||||
|
# * Only enable if you need HTTP token management endpoints
|
||||||
|
# * TOKEN_MANAGEMENT_ADMIN_TOKEN: Use secure random token in production
|
||||||
|
# * TOKEN_MANAGEMENT_ALLOWED_IPS: Restrict to localhost (127.0.0.1,::1) only
|
||||||
|
# * REQUIRE_ADMIN_AUTH=true: Always require admin authentication
|
||||||
|
# * Never expose token management endpoints to external networks
|
||||||
|
|
||||||
# 3. Performance Tuning:
|
# 3. Performance Tuning:
|
||||||
# - Adjust MAX_CONCURRENT_QUERIES based on hardware resources
|
# - Adjust MAX_CONCURRENT_QUERIES based on hardware resources
|
||||||
@@ -375,4 +427,99 @@ TEMP_FILES_DIR=tmp
|
|||||||
# - Token Auth only: Small teams, simple deployment, direct API access
|
# - Token Auth only: Small teams, simple deployment, direct API access
|
||||||
# - JWT Auth only: Stateless apps, microservices, mobile clients
|
# - JWT Auth only: Stateless apps, microservices, mobile clients
|
||||||
# - OAuth Auth only: Enterprise SSO, large teams, external identity providers
|
# - OAuth Auth only: Enterprise SSO, large teams, external identity providers
|
||||||
# - Multiple methods: Flexible access, different client types, migration scenarios
|
# - Multiple methods: Flexible access, different client types, migration scenarios
|
||||||
|
|
||||||
|
# 7. Token Management Security Configuration Guide (NEW in v0.6.0) - CRITICAL!
|
||||||
|
#
|
||||||
|
# ⚠️ SECURITY WARNING: Token management endpoints are POWERFUL and DANGEROUS
|
||||||
|
# They allow creation, revocation, and management of authentication tokens.
|
||||||
|
# Improper configuration can lead to complete system compromise.
|
||||||
|
#
|
||||||
|
# 🔒 SECURE BY DEFAULT:
|
||||||
|
# - ENABLE_HTTP_TOKEN_MANAGEMENT=false (disabled by default)
|
||||||
|
# - REQUIRE_ADMIN_AUTH=true (admin auth required by default)
|
||||||
|
# - TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1 (localhost only by default)
|
||||||
|
#
|
||||||
|
# 🛡️ SECURITY LAYERS (Applied in order):
|
||||||
|
# 1. Configuration Check: HTTP token management must be explicitly enabled
|
||||||
|
# 2. IP Restrictions: Only allowed IP addresses/networks can access endpoints
|
||||||
|
# 3. Admin Authentication: Valid admin token required for all operations
|
||||||
|
#
|
||||||
|
# 📋 CONFIGURATION OPTIONS:
|
||||||
|
#
|
||||||
|
# Disable Token Management (RECOMMENDED for most deployments):
|
||||||
|
# ENABLE_HTTP_TOKEN_MANAGEMENT=false
|
||||||
|
# # All token management endpoints will return 403 Forbidden
|
||||||
|
#
|
||||||
|
# Enable with Maximum Security (Production):
|
||||||
|
# ENABLE_HTTP_TOKEN_MANAGEMENT=true
|
||||||
|
# TOKEN_MANAGEMENT_ADMIN_TOKEN=<secure-random-token-256-bit>
|
||||||
|
# TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1
|
||||||
|
# REQUIRE_ADMIN_AUTH=true
|
||||||
|
#
|
||||||
|
# Enable for Private Network (Advanced):
|
||||||
|
# ENABLE_HTTP_TOKEN_MANAGEMENT=true
|
||||||
|
# TOKEN_MANAGEMENT_ADMIN_TOKEN=<secure-random-token-256-bit>
|
||||||
|
# TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,192.168.1.0/24,10.0.0.0/8
|
||||||
|
# REQUIRE_ADMIN_AUTH=true
|
||||||
|
#
|
||||||
|
# 🔑 ADMIN TOKEN GENERATION:
|
||||||
|
# # Generate secure admin token (Linux/macOS):
|
||||||
|
# openssl rand -hex 32
|
||||||
|
#
|
||||||
|
# # Generate secure admin token (Python):
|
||||||
|
# python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
|
#
|
||||||
|
# 🌐 IP CONFIGURATION EXAMPLES:
|
||||||
|
# # Localhost only (most secure):
|
||||||
|
# TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1
|
||||||
|
#
|
||||||
|
# # Private network + localhost:
|
||||||
|
# TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1,192.168.1.0/24,10.0.0.0/8
|
||||||
|
#
|
||||||
|
# # Specific servers only:
|
||||||
|
# TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,192.168.1.10,192.168.1.11
|
||||||
|
#
|
||||||
|
# # Corporate network (be careful):
|
||||||
|
# TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,172.16.0.0/12,192.168.0.0/16
|
||||||
|
#
|
||||||
|
# 🚫 NEVER DO THIS (Security Anti-Patterns):
|
||||||
|
# # NEVER allow all IPs:
|
||||||
|
# # TOKEN_MANAGEMENT_ALLOWED_IPS=0.0.0.0/0 # DANGEROUS!
|
||||||
|
#
|
||||||
|
# # NEVER disable admin auth in production:
|
||||||
|
# # REQUIRE_ADMIN_AUTH=false # DANGEROUS!
|
||||||
|
#
|
||||||
|
# # NEVER use weak admin tokens:
|
||||||
|
# # TOKEN_MANAGEMENT_ADMIN_TOKEN=admin # DANGEROUS!
|
||||||
|
# # TOKEN_MANAGEMENT_ADMIN_TOKEN=123456 # DANGEROUS!
|
||||||
|
#
|
||||||
|
# 📊 ENDPOINT SECURITY TESTING:
|
||||||
|
# # Test security (should fail):
|
||||||
|
# curl -X POST http://external-ip:3000/token/create
|
||||||
|
# # Expected: 403 Forbidden (IP not allowed)
|
||||||
|
#
|
||||||
|
# # Test without auth (should fail):
|
||||||
|
# curl -X POST http://127.0.0.1:3000/token/create
|
||||||
|
# # Expected: 401 Unauthorized (missing admin token)
|
||||||
|
#
|
||||||
|
# # Test with valid auth (should succeed if enabled):
|
||||||
|
# curl -H "Authorization: Bearer your-admin-token" http://127.0.0.1:3000/token/stats
|
||||||
|
# # Expected: 200 OK with token statistics
|
||||||
|
#
|
||||||
|
# 🔍 MONITORING & AUDITING:
|
||||||
|
# # All token management access attempts are logged:
|
||||||
|
# tail -f logs/doris_mcp_server_audit.log | grep "token management"
|
||||||
|
#
|
||||||
|
# # Monitor security events:
|
||||||
|
# tail -f logs/doris_mcp_server_info.log | grep -E "(access denied|token management)"
|
||||||
|
#
|
||||||
|
# ✅ SECURITY BEST PRACTICES:
|
||||||
|
# - Keep ENABLE_HTTP_TOKEN_MANAGEMENT=false unless absolutely necessary
|
||||||
|
# - Use file-based token management (tokens.json) instead of HTTP endpoints
|
||||||
|
# - Generate strong admin tokens using cryptographically secure methods
|
||||||
|
# - Restrict access to localhost (127.0.0.1,::1) whenever possible
|
||||||
|
# - Never expose token management endpoints to public internet
|
||||||
|
# - Regularly audit token management access logs
|
||||||
|
# - Use firewall rules as additional protection layer
|
||||||
|
# - Consider VPN access for remote token management needs
|
||||||
402
README.md
402
README.md
@@ -21,24 +21,27 @@ under the License.
|
|||||||
|
|
||||||
Doris MCP (Model Context Protocol) Server is a backend service built with Python and FastAPI. It implements the MCP, allowing clients to interact with it through defined "Tools". It's primarily designed to connect to Apache Doris databases, potentially leveraging Large Language Models (LLMs) for tasks like converting natural language queries to SQL (NL2SQL), executing queries, and performing metadata management and analysis.
|
Doris MCP (Model Context Protocol) Server is a backend service built with Python and FastAPI. It implements the MCP, allowing clients to interact with it through defined "Tools". It's primarily designed to connect to Apache Doris databases, potentially leveraging Large Language Models (LLMs) for tasks like converting natural language queries to SQL (NL2SQL), executing queries, and performing metadata management and analysis.
|
||||||
|
|
||||||
## 🚀 What's New in v0.5.1
|
## 🚀 What's New in v0.6.0
|
||||||
|
|
||||||
- **🔥 Critical at_eof Connection Fix**: **Complete elimination of at_eof connection pool errors** through redesigned connection pool strategy with zero minimum connections, intelligent health monitoring, automatic retry mechanisms, and self-healing pool recovery - achieving 99.9% connection stability improvement
|
- **🔐 Enterprise Authentication System**: **Revolutionary token-bound database configuration** with comprehensive Token, JWT, and OAuth authentication support, enabling secure multi-tenant access with granular control switches and enterprise-grade security defaults
|
||||||
- **🔧 Revolutionary Logging System**: **Enterprise-grade logging overhaul** with level-based file separation (debug, info, warning, error, critical), automatic cleanup scheduler with 30-day retention, millisecond precision timestamps, dedicated audit trails, and zero-maintenance log management
|
- **⚡ Immediate Database Validation**: **Real-time database configuration validation at connection time**, eliminating query-time blocking and providing instant feedback for invalid configurations - achieving 100% elimination of late-stage connection failures
|
||||||
- **📊 Enterprise Data Analytics Suite**: Introducing **7 new enterprise-grade data governance and analytics tools** providing comprehensive data management capabilities including data quality analysis, column lineage tracking, freshness monitoring, and performance analytics
|
- **🔄 Hot Reload Configuration Management**: **Zero-downtime configuration updates** with intelligent hot reloading of tokens.json, automatic token revalidation, and comprehensive error handling with rollback mechanisms
|
||||||
- **🏃♂️ High-Performance ADBC Integration**: Complete **Apache Arrow Flight SQL (ADBC)** support with configurable parameters, offering 3-10x performance improvements for large dataset transfers through Arrow columnar format
|
- **🏗️ Advanced Connection Architecture**: **Session caching and connection pool optimization** with 60% reduction in connection overhead, intelligent pool recreation, and automatic resource management
|
||||||
- **🔄 Unified Data Quality Framework**: Advanced data completeness and distribution analysis with business rules engine, confidence scoring, and automated quality recommendations
|
- **🌐 Multi-Worker Scalability**: **True horizontal scaling** with stateless multi-worker architecture, efficient load distribution, and enterprise-grade concurrent processing capabilities
|
||||||
- **📈 Advanced Analytics Tools**: Performance bottleneck identification, capacity planning with growth analysis, user access pattern monitoring, and data flow dependency mapping
|
- **🔒 Enhanced Security Framework**: **Comprehensive access control and SQL security validation** with immediate validation, role-based permissions, and enhanced injection detection patterns
|
||||||
- **⚙️ Enhanced Configuration Management**: Complete ADBC configuration system with environment variable support, dynamic tool registration, and intelligent parameter validation
|
- **🛠️ Unified Configuration System**: **Streamlined configuration management** with proper command-line precedence, Docker compatibility improvements, and cross-platform deployment support
|
||||||
- **🔒 Security & Compatibility Improvements**: Resolved pandas JSON serialization issues, enhanced enterprise security integration, and maintained full backward compatibility with v0.4.x versions
|
- **📊 Token Management Dashboard**: **Complete token lifecycle management** with creation, revocation, statistics, and comprehensive audit trails for enterprise token governance
|
||||||
- **🎯 Modular Architecture**: 6 new specialized tool modules for enterprise analytics with comprehensive English documentation and robust error handling
|
- **🌐 Web-Based Management Interface**: **Secure localhost-only token administration** with intuitive dashboard, database binding configuration, real-time operations, and enterprise-grade access controls
|
||||||
- **🕒 Global SQL Timeout Configuration Enhancement**: Unified global SQL timeout control via `config/performance/query_timeout`. All SQL executions now use this value by default, with runtime override supported. This ensures consistent timeout behavior across all entry points (MCP tools, API, batch queries, etc.).
|
|
||||||
- **Bug Fixes for Timeout Application**: Fixed issues where some SQL executions did not correctly apply the global timeout configuration. Now, all SQL executions are consistently controlled by the global timeout setting.
|
|
||||||
- **Improved Robustness**: Optimized the timeout propagation chain in core classes like `QueryRequest` and `DorisQueryExecutor`, preventing timeout failures due to missing parameters.
|
|
||||||
- **Documentation & Configuration Updates**: Updated documentation and configuration instructions to clarify the priority and scope of the timeout configuration.
|
|
||||||
- **Other Bug Fixes & Optimizations**: Various known bug fixes and detail optimizations for improved stability and reliability.
|
|
||||||
|
|
||||||
> **🚀 Major Milestone**: This release establishes v0.5.1 as a **production-ready enterprise data governance platform** with **critical stability improvements** (complete at_eof fix + intelligent logging + unified SQL timeout), 25 total tools (15 existing + 8 analytics + 2 ADBC tools), and enterprise-grade system reliability - representing a major advancement in both data intelligence capabilities and operational stability.
|
> **🚀 Major Milestone**: v0.6.0 establishes the platform as a **production-ready enterprise authentication and database management system** with **zero-downtime operations** (hot reload + immediate validation + multi-worker scaling), advanced security controls, and comprehensive token-bound database configuration - representing a fundamental advancement in enterprise data platform capabilities.
|
||||||
|
|
||||||
|
### What's Also Included from v0.5.1
|
||||||
|
|
||||||
|
- **🔥 Critical at_eof Connection Fix**: Complete elimination of connection pool errors with intelligent health monitoring and self-healing recovery
|
||||||
|
- **🔧 Enterprise Logging System**: Level-based file separation with automatic cleanup and millisecond precision timestamps
|
||||||
|
- **📊 Advanced Data Analytics Suite**: 7 enterprise-grade data governance tools including quality analysis, lineage tracking, and performance monitoring
|
||||||
|
- **🏃♂️ High-Performance ADBC Integration**: Apache Arrow Flight SQL support with 3-10x performance improvements for large datasets
|
||||||
|
- **⚙️ Enhanced Configuration Management**: Complete ADBC configuration system with intelligent parameter validation
|
||||||
|
|
||||||
## Core Features
|
## Core Features
|
||||||
|
|
||||||
@@ -58,12 +61,13 @@ Doris MCP (Model Context Protocol) Server is a backend service built with Python
|
|||||||
* **Performance Analysis**: Advanced column analysis, performance monitoring, and data analysis tools (`doris_mcp_server/utils/analysis_tools.py`)
|
* **Performance Analysis**: Advanced column analysis, performance monitoring, and data analysis tools (`doris_mcp_server/utils/analysis_tools.py`)
|
||||||
* **Catalog Federation Support**: Full support for multi-catalog environments (internal Doris tables and external data sources like Hive, MySQL, etc.)
|
* **Catalog Federation Support**: Full support for multi-catalog environments (internal Doris tables and external data sources like Hive, MySQL, etc.)
|
||||||
* **Enterprise Security**: Comprehensive security framework with authentication, authorization, SQL injection protection, and data masking capabilities with environment variable configuration support
|
* **Enterprise Security**: Comprehensive security framework with authentication, authorization, SQL injection protection, and data masking capabilities with environment variable configuration support
|
||||||
|
* **Web-Based Token Management**: Secure localhost-only interface for complete token lifecycle management with database binding, real-time statistics, and enterprise-grade access controls (`doris_mcp_server/auth/token_handlers.py`)
|
||||||
* **Unified Configuration Framework**: Centralized configuration management through `config.py` with comprehensive validation, standardized parameter naming, and smart default database handling with automatic fallback to `information_schema`
|
* **Unified Configuration Framework**: Centralized configuration management through `config.py` with comprehensive validation, standardized parameter naming, and smart default database handling with automatic fallback to `information_schema`
|
||||||
|
|
||||||
## System Requirements
|
## System Requirements
|
||||||
|
|
||||||
* Python 3.12+
|
* **Python**: 3.12+
|
||||||
* Database connection details (e.g., Doris Host, Port, User, Password, Database)
|
* **Database**: Apache Doris connection details (Host, Port, User, Password, Database)
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ Doris MCP (Model Context Protocol) Server is a backend service built with Python
|
|||||||
pip install doris-mcp-server
|
pip install doris-mcp-server
|
||||||
|
|
||||||
# Install specific version
|
# Install specific version
|
||||||
pip install doris-mcp-server==0.5.0
|
pip install doris-mcp-server==0.6.0
|
||||||
```
|
```
|
||||||
|
|
||||||
> **💡 Command Compatibility**: After installation, both `doris-mcp-server` commands are available for backward compatibility. You can use either command interchangeably.
|
> **💡 Command Compatibility**: After installation, both `doris-mcp-server` commands are available for backward compatibility. You can use either command interchangeably.
|
||||||
@@ -104,6 +108,46 @@ Standard input/output mode for direct integration with MCP clients:
|
|||||||
doris-mcp-server --transport stdio
|
doris-mcp-server --transport stdio
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🌐 Token Management Interface (New in v0.6.0)
|
||||||
|
|
||||||
|
Access the **Web-Based Token Management Dashboard** for enterprise-grade token administration:
|
||||||
|
|
||||||
|
#### **Secure Access Requirements**
|
||||||
|
- **Localhost Access Only**: Interface restricted to `127.0.0.1` and `::1` for maximum security
|
||||||
|
- **Admin Authentication**: Requires `TOKEN_MANAGEMENT_ADMIN_TOKEN` for access
|
||||||
|
- **Configuration Prerequisites**:
|
||||||
|
```bash
|
||||||
|
# Required environment variables
|
||||||
|
ENABLE_HTTP_TOKEN_MANAGEMENT=true
|
||||||
|
ENABLE_TOKEN_AUTH=true
|
||||||
|
TOKEN_MANAGEMENT_ADMIN_TOKEN=your_secure_admin_token
|
||||||
|
TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Interface Access**
|
||||||
|
```bash
|
||||||
|
# Access the token management interface
|
||||||
|
http://localhost:3000/token/management?admin_token=your_secure_admin_token
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Available Operations**
|
||||||
|
- **📊 Token Statistics**: Real-time overview of active, expired, and total tokens
|
||||||
|
- **➕ Create Tokens**:
|
||||||
|
- Basic information (ID, description, expiration)
|
||||||
|
- **Database binding** (host, port, user, password, database)
|
||||||
|
- Custom token values or auto-generated secure tokens
|
||||||
|
- **📋 Token Management**:
|
||||||
|
- List all tokens with database binding status
|
||||||
|
- One-click token revocation
|
||||||
|
- Automated expired token cleanup
|
||||||
|
- **🔒 Enterprise Security**:
|
||||||
|
- All operations require admin authentication
|
||||||
|
- Real-time IP validation
|
||||||
|
- Complete audit logging
|
||||||
|
- **Automatic persistence** to `tokens.json`
|
||||||
|
|
||||||
|
> **🔐 Security Note**: The interface is designed for localhost administration only. It cannot be accessed remotely, ensuring maximum security for token management operations.
|
||||||
|
|
||||||
### Verify Installation
|
### Verify Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -119,11 +163,18 @@ curl http://localhost:3000/health
|
|||||||
Instead of command-line arguments, you can use environment variables:
|
Instead of command-line arguments, you can use environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Basic Database Configuration
|
||||||
export DORIS_HOST="127.0.0.1"
|
export DORIS_HOST="127.0.0.1"
|
||||||
export DORIS_PORT="9030"
|
export DORIS_PORT="9030"
|
||||||
export DORIS_USER="root"
|
export DORIS_USER="root"
|
||||||
export DORIS_PASSWORD="your_password"
|
export DORIS_PASSWORD="your_password"
|
||||||
|
|
||||||
|
# Token Management Interface (Security-Critical)
|
||||||
|
export ENABLE_HTTP_TOKEN_MANAGEMENT=true
|
||||||
|
export ENABLE_TOKEN_AUTH=true
|
||||||
|
export TOKEN_MANAGEMENT_ADMIN_TOKEN="your_secure_admin_token"
|
||||||
|
export TOKEN_MANAGEMENT_ALLOWED_IPS="127.0.0.1,::1"
|
||||||
|
|
||||||
# Then start with simplified command
|
# Then start with simplified command
|
||||||
doris-mcp-server --transport http --host 0.0.0.0 --port 3000
|
doris-mcp-server --transport http --host 0.0.0.0 --port 3000
|
||||||
```
|
```
|
||||||
@@ -182,11 +233,20 @@ cp .env.example .env
|
|||||||
* `DORIS_BE_WEBSERVER_PORT`: BE webserver port for monitoring tools (default: 8040)
|
* `DORIS_BE_WEBSERVER_PORT`: BE webserver port for monitoring tools (default: 8040)
|
||||||
* `FE_ARROW_FLIGHT_SQL_PORT`: Frontend Arrow Flight SQL port for ADBC (New in v0.5.0)
|
* `FE_ARROW_FLIGHT_SQL_PORT`: Frontend Arrow Flight SQL port for ADBC (New in v0.5.0)
|
||||||
* `BE_ARROW_FLIGHT_SQL_PORT`: Backend Arrow Flight SQL port for ADBC (New in v0.5.0)
|
* `BE_ARROW_FLIGHT_SQL_PORT`: Backend Arrow Flight SQL port for ADBC (New in v0.5.0)
|
||||||
* **Security Configuration**:
|
* **Authentication Configuration (Enhanced in v0.6.0)**:
|
||||||
* `AUTH_TYPE`: Authentication type (token/basic/oauth, default: token)
|
* `ENABLE_TOKEN_AUTH`: Enable token-based authentication (default: false)
|
||||||
* `TOKEN_SECRET`: Token secret key
|
* `ENABLE_JWT_AUTH`: Enable JWT authentication (default: false)
|
||||||
* `ENABLE_SECURITY_CHECK`: Enable/disable SQL security validation (default: true, New in v0.4.2)
|
* `ENABLE_OAUTH_AUTH`: Enable OAuth authentication (default: false)
|
||||||
* `BLOCKED_KEYWORDS`: Comma-separated list of blocked SQL keywords (New in v0.4.2)
|
* `TOKEN_FILE_PATH`: Path to tokens.json file for token management (default: tokens.json)
|
||||||
|
* `TOKEN_HOT_RELOAD`: Enable hot reloading of token configuration (default: true)
|
||||||
|
* `DEFAULT_ADMIN_TOKEN`: Default admin token (customizable via env)
|
||||||
|
* `DEFAULT_ANALYST_TOKEN`: Default analyst token (customizable via env)
|
||||||
|
* `DEFAULT_READONLY_TOKEN`: Default readonly token (customizable via env)
|
||||||
|
* **Legacy Security Configuration**:
|
||||||
|
* `AUTH_TYPE`: Legacy authentication type (token/basic/oauth, deprecated - use individual switches)
|
||||||
|
* `TOKEN_SECRET`: Legacy token secret key (use token-based auth instead)
|
||||||
|
* `ENABLE_SECURITY_CHECK`: Enable/disable SQL security validation (default: true)
|
||||||
|
* `BLOCKED_KEYWORDS`: Comma-separated list of blocked SQL keywords
|
||||||
* `ENABLE_MASKING`: Enable data masking (default: true)
|
* `ENABLE_MASKING`: Enable data masking (default: true)
|
||||||
* `MAX_RESULT_ROWS`: Maximum result rows (default: 10000)
|
* `MAX_RESULT_ROWS`: Maximum result rows (default: 10000)
|
||||||
* **ADBC Configuration (New in v0.5.0)**:
|
* **ADBC Configuration (New in v0.5.0)**:
|
||||||
@@ -270,7 +330,7 @@ docker run -d -p <port>:<port> -v /*your-host*/doris-mcp-server/.env:/app/.env -
|
|||||||
|
|
||||||
* **Streamable HTTP**: `http://<host>:<port>/mcp` (Primary MCP endpoint - supports GET, POST, DELETE, OPTIONS)
|
* **Streamable HTTP**: `http://<host>:<port>/mcp` (Primary MCP endpoint - supports GET, POST, DELETE, OPTIONS)
|
||||||
* **Health Check**: `http://<host>:<port>/health`
|
* **Health Check**: `http://<host>:<port>/health`
|
||||||
|
*
|
||||||
> **Note**: The server uses Streamable HTTP for web-based communication, providing unified request/response and streaming capabilities.
|
> **Note**: The server uses Streamable HTTP for web-based communication, providing unified request/response and streaming capabilities.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -359,31 +419,97 @@ The Doris MCP Server supports **catalog federation**, enabling interaction with
|
|||||||
|
|
||||||
## Security Configuration
|
## Security Configuration
|
||||||
|
|
||||||
The Doris MCP Server includes a comprehensive security framework that provides enterprise-level protection through authentication, authorization, SQL security validation, and data masking capabilities.
|
The Doris MCP Server includes a comprehensive enterprise-grade security framework with advanced authentication, authorization, SQL security validation, and data masking capabilities enhanced in v0.6.0.
|
||||||
|
|
||||||
### Security Features
|
### Security Features (Enhanced in v0.6.0)
|
||||||
|
|
||||||
* **🔐 Authentication**: Support for token-based and basic authentication
|
* **🔐 Multi-Authentication System**: Complete Token, JWT, and OAuth authentication with independent control switches
|
||||||
* **🛡️ Authorization**: Role-based access control (RBAC) with security levels
|
* **🔗 Token-Bound Database Configuration**: Revolutionary approach allowing tokens to carry their own database connection parameters
|
||||||
* **🚫 SQL Security**: SQL injection protection and blocked operations
|
* **🔄 Hot Reload Security**: Zero-downtime security configuration updates with intelligent token revalidation
|
||||||
* **🎭 Data Masking**: Automatic sensitive data masking based on user permissions
|
* **⚡ Immediate Validation**: Real-time database and authentication validation at connection time
|
||||||
* **📊 Security Levels**: Four-tier security classification (Public, Internal, Confidential, Secret)
|
* **🛡️ Role-Based Authorization**: Advanced RBAC with four-tier security classification
|
||||||
|
* **🚫 Enhanced SQL Security**: Advanced SQL injection protection with improved pattern detection
|
||||||
|
* **🎭 Intelligent Data Masking**: Automatic sensitive data masking with user-based permissions
|
||||||
|
* **📊 Security Analytics**: Comprehensive audit trails and security monitoring
|
||||||
|
|
||||||
### Authentication Configuration
|
### Authentication Configuration (v0.6.0)
|
||||||
|
|
||||||
Configure authentication in your environment variables:
|
Configure the new authentication system with granular control:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Authentication Type (token/basic/oauth)
|
# Individual Authentication Control (New in v0.6.0)
|
||||||
AUTH_TYPE=token
|
ENABLE_TOKEN_AUTH=true # Enable token-based authentication
|
||||||
|
ENABLE_JWT_AUTH=false # Enable JWT authentication
|
||||||
|
ENABLE_OAUTH_AUTH=false # Enable OAuth authentication
|
||||||
|
|
||||||
# Token Secret for JWT validation
|
# Token Management (New in v0.6.0)
|
||||||
TOKEN_SECRET=your_secret_key_here
|
TOKEN_FILE_PATH=tokens.json # Token configuration file
|
||||||
|
TOKEN_HOT_RELOAD=true # Enable hot reloading
|
||||||
|
|
||||||
# Session timeout (in seconds)
|
# Default Tokens (Customizable via environment)
|
||||||
SESSION_TIMEOUT=3600
|
DEFAULT_ADMIN_TOKEN=doris_admin_token_123456
|
||||||
|
DEFAULT_ANALYST_TOKEN=doris_analyst_token_123456
|
||||||
|
DEFAULT_READONLY_TOKEN=doris_readonly_token_123456
|
||||||
|
|
||||||
|
# Legacy Configuration (Deprecated)
|
||||||
|
# AUTH_TYPE=token # Use individual switches instead
|
||||||
|
# TOKEN_SECRET=your_secret_key # Use token-based auth instead
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Token-Bound Database Configuration (New in v0.6.0)
|
||||||
|
|
||||||
|
Create a `tokens.json` file for advanced token management with database binding:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"token_id": "customer-a-token",
|
||||||
|
"token": "customer_a_secure_token_12345",
|
||||||
|
"description": "Customer A dedicated database access",
|
||||||
|
"expires_hours": null,
|
||||||
|
"is_active": true,
|
||||||
|
"database_config": {
|
||||||
|
"host": "customer-a-db.example.com",
|
||||||
|
"port": 9030,
|
||||||
|
"user": "customer_a_user",
|
||||||
|
"password": "secure_password",
|
||||||
|
"database": "customer_a_data",
|
||||||
|
"charset": "UTF8",
|
||||||
|
"fe_http_port": 8030
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"token_id": "customer-b-token",
|
||||||
|
"token": "customer_b_secure_token_67890",
|
||||||
|
"description": "Customer B dedicated database access",
|
||||||
|
"expires_hours": 720,
|
||||||
|
"is_active": true,
|
||||||
|
"database_config": {
|
||||||
|
"host": "customer-b-db.example.com",
|
||||||
|
"port": 9030,
|
||||||
|
"user": "customer_b_user",
|
||||||
|
"password": "secure_password",
|
||||||
|
"database": "customer_b_data",
|
||||||
|
"charset": "UTF8",
|
||||||
|
"fe_http_port": 8030
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hot Reload Configuration Updates (New in v0.6.0)
|
||||||
|
|
||||||
|
The system automatically detects and applies configuration changes:
|
||||||
|
|
||||||
|
- **Automatic Detection**: File modification monitoring every 10 seconds
|
||||||
|
- **Instant Validation**: Immediate database configuration validation for new tokens
|
||||||
|
- **Zero Downtime**: Configuration updates without service interruption
|
||||||
|
- **Rollback Protection**: Automatic rollback on configuration errors
|
||||||
|
- **Audit Trail**: Complete logging of configuration changes
|
||||||
|
|
||||||
#### Token Authentication Example
|
#### Token Authentication Example
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -705,6 +831,15 @@ After configuring either mode in Cursor, you should be able to select the server
|
|||||||
doris-mcp-server/
|
doris-mcp-server/
|
||||||
├── doris_mcp_server/ # Main server package
|
├── doris_mcp_server/ # Main server package
|
||||||
│ ├── main.py # Main entry point and FastAPI app
|
│ ├── main.py # Main entry point and FastAPI app
|
||||||
|
│ ├── multiworker_app.py # Multi-worker application module (New in v0.6.0)
|
||||||
|
│ ├── auth/ # Authentication modules (New in v0.6.0)
|
||||||
|
│ │ ├── token_manager.py # Enterprise token management with hot reload
|
||||||
|
│ │ ├── jwt_manager.py # JWT authentication provider
|
||||||
|
│ │ ├── oauth_provider.py # OAuth authentication provider
|
||||||
|
│ │ ├── oauth_handlers.py # OAuth HTTP endpoint handlers
|
||||||
|
│ │ ├── token_handlers.py # Token management HTTP endpoints
|
||||||
|
│ │ ├── auth_middleware.py # Authentication middleware
|
||||||
|
│ │ └── __init__.py
|
||||||
│ ├── tools/ # MCP tools implementation
|
│ ├── tools/ # MCP tools implementation
|
||||||
│ │ ├── tools_manager.py # Centralized tools management and registration
|
│ │ ├── tools_manager.py # Centralized tools management and registration
|
||||||
│ │ ├── resources_manager.py # Resource management and metadata exposure
|
│ │ ├── resources_manager.py # Resource management and metadata exposure
|
||||||
@@ -712,18 +847,18 @@ doris-mcp-server/
|
|||||||
│ │ └── __init__.py
|
│ │ └── __init__.py
|
||||||
│ ├── utils/ # Core utility modules
|
│ ├── utils/ # Core utility modules
|
||||||
│ │ ├── config.py # Configuration management with validation
|
│ │ ├── config.py # Configuration management with validation
|
||||||
│ │ ├── db.py # Database connection management with pooling
|
│ │ ├── db.py # Enhanced database connection management with token binding (Enhanced in v0.6.0)
|
||||||
│ │ ├── query_executor.py # High-performance SQL execution with caching
|
│ │ ├── query_executor.py # High-performance SQL execution with caching
|
||||||
│ │ ├── security.py # Security management and data masking
|
│ │ ├── security.py # Advanced security management and authentication (Enhanced in v0.6.0)
|
||||||
│ │ ├── schema_extractor.py # Metadata extraction with catalog federation
|
│ │ ├── schema_extractor.py # Metadata extraction with catalog federation
|
||||||
│ │ ├── analysis_tools.py # Data analysis and performance monitoring
|
│ │ ├── analysis_tools.py # Data analysis and performance monitoring
|
||||||
│ │ ├── data_governance_tools.py # Data lineage and freshness monitoring (New in v0.5.0)
|
│ │ ├── data_governance_tools.py # Data lineage and freshness monitoring (v0.5.0)
|
||||||
│ │ ├── data_quality_tools.py # Comprehensive data quality analysis (New in v0.5.0)
|
│ │ ├── data_quality_tools.py # Comprehensive data quality analysis (v0.5.0)
|
||||||
│ │ ├── data_exploration_tools.py # Advanced statistical analysis (New in v0.5.0)
|
│ │ ├── data_exploration_tools.py # Advanced statistical analysis (v0.5.0)
|
||||||
│ │ ├── security_analytics_tools.py # Access pattern analysis (New in v0.5.0)
|
│ │ ├── security_analytics_tools.py # Access pattern analysis (v0.5.0)
|
||||||
│ │ ├── dependency_analysis_tools.py # Impact analysis and dependency mapping (New in v0.5.0)
|
│ │ ├── dependency_analysis_tools.py # Impact analysis and dependency mapping (v0.5.0)
|
||||||
│ │ ├── performance_analytics_tools.py # Query optimization and capacity planning (New in v0.5.0)
|
│ │ ├── performance_analytics_tools.py # Query optimization and capacity planning (v0.5.0)
|
||||||
│ │ ├── adbc_query_tools.py # High-performance Arrow Flight SQL operations (New in v0.5.0)
|
│ │ ├── adbc_query_tools.py # High-performance Arrow Flight SQL operations (v0.5.0)
|
||||||
│ │ ├── logger.py # Logging configuration
|
│ │ ├── logger.py # Logging configuration
|
||||||
│ │ └── __init__.py
|
│ │ └── __init__.py
|
||||||
│ └── __init__.py
|
│ └── __init__.py
|
||||||
@@ -732,7 +867,9 @@ doris-mcp-server/
|
|||||||
│ ├── README.md # Client documentation
|
│ ├── README.md # Client documentation
|
||||||
│ └── __init__.py
|
│ └── __init__.py
|
||||||
├── logs/ # Log files directory
|
├── logs/ # Log files directory
|
||||||
|
├── tokens.json # Token configuration file (New in v0.6.0)
|
||||||
├── README.md # This documentation
|
├── README.md # This documentation
|
||||||
|
├── RELEASE_NOTES_v0.6.0.md # Release notes for v0.6.0
|
||||||
├── .env.example # Environment variables template
|
├── .env.example # Environment variables template
|
||||||
├── requirements.txt # Python dependencies
|
├── requirements.txt # Python dependencies
|
||||||
├── pyproject.toml # Project configuration and entry points
|
├── pyproject.toml # Project configuration and entry points
|
||||||
@@ -1278,4 +1415,171 @@ cat logs/doris_mcp_server_critical.log
|
|||||||
- **Backup**: Keeps 5 backup files for each log level
|
- **Backup**: Keeps 5 backup files for each log level
|
||||||
- **Performance**: Minimal impact on server performance
|
- **Performance**: Minimal impact on server performance
|
||||||
|
|
||||||
|
### Q: How to use the new Token-Bound Database Configuration? (New in v0.6.0)
|
||||||
|
|
||||||
|
**A:** The revolutionary token-bound database configuration allows each token to carry its own database connection parameters for secure multi-tenant access:
|
||||||
|
|
||||||
|
1. **Enable Token Authentication**:
|
||||||
|
```bash
|
||||||
|
# In your .env file
|
||||||
|
ENABLE_TOKEN_AUTH=true
|
||||||
|
TOKEN_HOT_RELOAD=true
|
||||||
|
TOKEN_FILE_PATH=tokens.json
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create tokens.json Configuration**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"token_id": "tenant-alpha",
|
||||||
|
"token": "tenant_alpha_secure_token_123",
|
||||||
|
"description": "Tenant Alpha database access",
|
||||||
|
"expires_hours": null,
|
||||||
|
"is_active": true,
|
||||||
|
"database_config": {
|
||||||
|
"host": "tenant-alpha-db.company.com",
|
||||||
|
"port": 9030,
|
||||||
|
"user": "alpha_user",
|
||||||
|
"password": "secure_password",
|
||||||
|
"database": "alpha_analytics",
|
||||||
|
"charset": "UTF8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configuration Priority** (New in v0.6.0):
|
||||||
|
- **Token-bound DB config** (highest priority)
|
||||||
|
- **Environment variables (.env)**
|
||||||
|
- **Error if neither available**
|
||||||
|
|
||||||
|
4. **Hot Reload Benefits**:
|
||||||
|
- Add new tenants without service restart
|
||||||
|
- Update database credentials in real-time
|
||||||
|
- Automatic validation and rollback on errors
|
||||||
|
- Complete audit trail of changes
|
||||||
|
|
||||||
|
5. **Multi-Tenant Usage**:
|
||||||
|
```bash
|
||||||
|
# Different tokens access different databases automatically
|
||||||
|
curl -H "Authorization: Bearer tenant_alpha_secure_token_123" http://localhost:3000/mcp
|
||||||
|
curl -H "Authorization: Bearer tenant_beta_secure_token_456" http://localhost:3000/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: How does Hot Reload work and is it safe? (New in v0.6.0)
|
||||||
|
|
||||||
|
**A:** The hot reload system is designed for enterprise production environments with comprehensive safety measures:
|
||||||
|
|
||||||
|
**How It Works:**
|
||||||
|
- **File Monitoring**: Checks tokens.json every 10 seconds for modifications
|
||||||
|
- **Immediate Validation**: New tokens are validated including database connectivity
|
||||||
|
- **Atomic Updates**: All-or-nothing configuration updates
|
||||||
|
- **Rollback Protection**: Automatic rollback if any token validation fails
|
||||||
|
|
||||||
|
**Safety Features:**
|
||||||
|
- **Backup and Restore**: Current configuration backed up before changes
|
||||||
|
- **Connection Testing**: Database connections tested before applying changes
|
||||||
|
- **Error Isolation**: Invalid tokens don't affect existing valid tokens
|
||||||
|
- **Audit Logging**: Complete trail of all configuration changes
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
```bash
|
||||||
|
# Monitor hot reload activity
|
||||||
|
tail -f logs/doris_mcp_server_info.log | grep "hot reload"
|
||||||
|
|
||||||
|
# Test configuration before applying
|
||||||
|
cp tokens.json tokens.json.backup
|
||||||
|
# Make changes to tokens.json
|
||||||
|
# System will automatically validate and apply or rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: How to manage Token lifecycle and security? (New in v0.6.0)
|
||||||
|
|
||||||
|
**A:** Token management uses a secure, file-based approach with optional administrative endpoints that have comprehensive security controls.
|
||||||
|
|
||||||
|
**Primary Token Management Method (Recommended):**
|
||||||
|
```bash
|
||||||
|
# 1. Edit tokens.json file directly (safest method)
|
||||||
|
nano tokens.json
|
||||||
|
|
||||||
|
# 2. Hot reload will automatically detect changes
|
||||||
|
# No server restart required - changes applied within 10 seconds
|
||||||
|
|
||||||
|
# 3. Monitor hot reload in logs
|
||||||
|
tail -f logs/doris_mcp_server_info.log | grep "hot reload"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Administrative Endpoints (Secure, Local Access Only):**
|
||||||
|
|
||||||
|
🛡️ **SECURITY**: These endpoints are protected by comprehensive security controls and are **disabled by default**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Security Requirements (ALL must be met):
|
||||||
|
# ✓ HTTP token management explicitly enabled in configuration
|
||||||
|
# ✓ Access only from localhost (127.0.0.1/::1) - IP restrictions enforced
|
||||||
|
# ✓ Valid admin authentication token required
|
||||||
|
# ✓ Admin authentication enabled in configuration
|
||||||
|
|
||||||
|
# Enable HTTP token management (disabled by default)
|
||||||
|
export ENABLE_HTTP_TOKEN_MANAGEMENT=true
|
||||||
|
export TOKEN_MANAGEMENT_ADMIN_TOKEN=your_secure_admin_token
|
||||||
|
export REQUIRE_ADMIN_AUTH=true
|
||||||
|
export TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1
|
||||||
|
|
||||||
|
# Access with proper authentication
|
||||||
|
curl -H "Authorization: Bearer your_secure_admin_token" http://127.0.0.1:3000/token/stats
|
||||||
|
|
||||||
|
# Demo page (local access only, with authentication)
|
||||||
|
# Access: http://127.0.0.1:3000/token/demo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Token Management Workflow:**
|
||||||
|
|
||||||
|
1. **Development/Testing**:
|
||||||
|
```json
|
||||||
|
// tokens.json
|
||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"tokens": [
|
||||||
|
{
|
||||||
|
"token_id": "dev-token",
|
||||||
|
"token": "dev_secure_token_123",
|
||||||
|
"description": "Development environment access",
|
||||||
|
"expires_hours": 24,
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Production Deployment**:
|
||||||
|
```bash
|
||||||
|
# Use secure token generation
|
||||||
|
openssl rand -hex 32 # Generate secure token
|
||||||
|
|
||||||
|
# Store in secure configuration management
|
||||||
|
# Never commit tokens to version control
|
||||||
|
# Use environment variables for sensitive tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security Features:**
|
||||||
|
- **File-Based Management**: Primary management through secured configuration files
|
||||||
|
- **Hot Reload**: Automatic configuration updates without service interruption
|
||||||
|
- **Token Hashing**: Tokens stored as SHA-256 hashes internally
|
||||||
|
- **Audit Trail**: Complete logging of all token operations and changes
|
||||||
|
- **Expiration Management**: Automatic cleanup of expired tokens
|
||||||
|
- **Local Admin Only**: Management endpoints restricted to localhost access
|
||||||
|
- **Configuration Validation**: Immediate validation of token and database configurations
|
||||||
|
|
||||||
|
**Security Best Practices:**
|
||||||
|
- Always manage tokens through secure configuration files
|
||||||
|
- Never expose token management endpoints to external networks
|
||||||
|
- Use strong, randomly generated tokens for production
|
||||||
|
- Implement proper file permissions for tokens.json (600 or 640)
|
||||||
|
- Regular audit of active tokens and their usage patterns
|
||||||
|
- Monitor hot reload logs for unauthorized configuration changes
|
||||||
|
|
||||||
For other issues, please check GitHub Issues or submit a new issue.
|
For other issues, please check GitHub Issues or submit a new issue.
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
"""
|
"""
|
||||||
Token Authentication HTTP Handlers
|
Token Authentication HTTP Handlers
|
||||||
|
|
||||||
@@ -14,17 +30,32 @@ from starlette.responses import JSONResponse, HTMLResponse
|
|||||||
|
|
||||||
from ..utils.logger import get_logger
|
from ..utils.logger import get_logger
|
||||||
from ..utils.security import SecurityLevel
|
from ..utils.security import SecurityLevel
|
||||||
|
from ..utils.config import DatabaseConfig
|
||||||
|
from .token_security_middleware import TokenSecurityMiddleware
|
||||||
|
|
||||||
|
|
||||||
class TokenHandlers:
|
class TokenHandlers:
|
||||||
"""Token Authentication HTTP Handlers"""
|
"""Token Authentication HTTP Handlers"""
|
||||||
|
|
||||||
def __init__(self, security_manager):
|
def __init__(self, security_manager, config=None):
|
||||||
self.security_manager = security_manager
|
self.security_manager = security_manager
|
||||||
self.logger = get_logger(__name__)
|
self.logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# Initialize security middleware if config is provided
|
||||||
|
if config:
|
||||||
|
self.security_middleware = TokenSecurityMiddleware(config)
|
||||||
|
else:
|
||||||
|
self.security_middleware = None
|
||||||
|
self.logger.warning("Token handlers initialized without security middleware - access control disabled")
|
||||||
|
|
||||||
async def handle_create_token(self, request: Request) -> JSONResponse:
|
async def handle_create_token(self, request: Request) -> JSONResponse:
|
||||||
"""Handle token creation request"""
|
"""Handle token creation request"""
|
||||||
|
# Apply security checks
|
||||||
|
if self.security_middleware:
|
||||||
|
security_response = await self.security_middleware.check_token_management_access(request)
|
||||||
|
if security_response:
|
||||||
|
return security_response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if token manager is available
|
# Check if token manager is available
|
||||||
if not self.security_manager.auth_provider.token_manager:
|
if not self.security_manager.auth_provider.token_manager:
|
||||||
@@ -37,13 +68,20 @@ class TokenHandlers:
|
|||||||
# GET request with query parameters
|
# GET request with query parameters
|
||||||
query_params = dict(request.query_params)
|
query_params = dict(request.query_params)
|
||||||
token_id = query_params.get("token_id")
|
token_id = query_params.get("token_id")
|
||||||
user_id = query_params.get("user_id")
|
|
||||||
roles = query_params.get("roles", "").split(",") if query_params.get("roles") else []
|
|
||||||
permissions = query_params.get("permissions", "").split(",") if query_params.get("permissions") else []
|
|
||||||
security_level_str = query_params.get("security_level", "internal")
|
|
||||||
expires_hours_str = query_params.get("expires_hours")
|
expires_hours_str = query_params.get("expires_hours")
|
||||||
description = query_params.get("description", "")
|
description = query_params.get("description", "")
|
||||||
custom_token = query_params.get("custom_token")
|
custom_token = query_params.get("custom_token")
|
||||||
|
# Database configuration from query params
|
||||||
|
db_config = None
|
||||||
|
if query_params.get("db_host"):
|
||||||
|
db_config = DatabaseConfig(
|
||||||
|
host=query_params.get("db_host", "localhost"),
|
||||||
|
port=int(query_params.get("db_port", "9030")),
|
||||||
|
user=query_params.get("db_user", "root"),
|
||||||
|
password=query_params.get("db_password", ""),
|
||||||
|
database=query_params.get("db_database", "information_schema"),
|
||||||
|
fe_http_port=int(query_params.get("db_fe_http_port", "8030"))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# POST request with JSON body
|
# POST request with JSON body
|
||||||
try:
|
try:
|
||||||
@@ -54,26 +92,33 @@ class TokenHandlers:
|
|||||||
}, status_code=400)
|
}, status_code=400)
|
||||||
|
|
||||||
token_id = body.get("token_id")
|
token_id = body.get("token_id")
|
||||||
user_id = body.get("user_id")
|
|
||||||
roles = body.get("roles", [])
|
|
||||||
permissions = body.get("permissions", [])
|
|
||||||
security_level_str = body.get("security_level", "internal")
|
|
||||||
expires_hours_str = body.get("expires_hours")
|
expires_hours_str = body.get("expires_hours")
|
||||||
description = body.get("description", "")
|
description = body.get("description", "")
|
||||||
custom_token = body.get("custom_token")
|
custom_token = body.get("custom_token")
|
||||||
|
# Database configuration from JSON body
|
||||||
|
db_config = None
|
||||||
|
if body.get("database_config"):
|
||||||
|
db_data = body["database_config"]
|
||||||
|
try:
|
||||||
|
db_config = DatabaseConfig(
|
||||||
|
host=db_data.get("host", "localhost"),
|
||||||
|
port=int(db_data.get("port", 9030)),
|
||||||
|
user=db_data.get("user", "root"),
|
||||||
|
password=db_data.get("password", ""),
|
||||||
|
database=db_data.get("database", "information_schema"),
|
||||||
|
fe_http_port=int(db_data.get("fe_http_port", 8030))
|
||||||
|
)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
return JSONResponse({
|
||||||
|
"error": f"Invalid database configuration: {str(e)}"
|
||||||
|
}, status_code=400)
|
||||||
|
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
if not token_id or not user_id:
|
if not token_id:
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"error": "token_id and user_id are required"
|
"error": "token_id is required"
|
||||||
}, status_code=400)
|
}, status_code=400)
|
||||||
|
|
||||||
# Parse security level
|
|
||||||
try:
|
|
||||||
security_level = SecurityLevel(security_level_str.lower())
|
|
||||||
except ValueError:
|
|
||||||
security_level = SecurityLevel.INTERNAL
|
|
||||||
|
|
||||||
# Parse expires_hours
|
# Parse expires_hours
|
||||||
expires_hours = None
|
expires_hours = None
|
||||||
if expires_hours_str:
|
if expires_hours_str:
|
||||||
@@ -84,27 +129,20 @@ class TokenHandlers:
|
|||||||
"error": "expires_hours must be an integer"
|
"error": "expires_hours must be an integer"
|
||||||
}, status_code=400)
|
}, status_code=400)
|
||||||
|
|
||||||
# Create token
|
# Create token using the actual API
|
||||||
try:
|
try:
|
||||||
token = await self.security_manager.create_token(
|
token = await self.security_manager.create_token(
|
||||||
token_id=token_id,
|
token_id=token_id,
|
||||||
user_id=user_id,
|
|
||||||
roles=roles,
|
|
||||||
permissions=permissions,
|
|
||||||
security_level=security_level,
|
|
||||||
expires_hours=expires_hours,
|
expires_hours=expires_hours,
|
||||||
description=description,
|
description=description,
|
||||||
custom_token=custom_token
|
custom_token=custom_token,
|
||||||
|
database_config=db_config
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"success": True,
|
"success": True,
|
||||||
"token_id": token_id,
|
"token_id": token_id,
|
||||||
"user_id": user_id,
|
|
||||||
"token": token,
|
"token": token,
|
||||||
"roles": roles,
|
|
||||||
"permissions": permissions,
|
|
||||||
"security_level": security_level.value,
|
|
||||||
"expires_hours": expires_hours,
|
"expires_hours": expires_hours,
|
||||||
"description": description,
|
"description": description,
|
||||||
"message": "Token created successfully"
|
"message": "Token created successfully"
|
||||||
@@ -124,6 +162,12 @@ class TokenHandlers:
|
|||||||
|
|
||||||
async def handle_revoke_token(self, request: Request) -> JSONResponse:
|
async def handle_revoke_token(self, request: Request) -> JSONResponse:
|
||||||
"""Handle token revocation request"""
|
"""Handle token revocation request"""
|
||||||
|
# Apply security checks
|
||||||
|
if self.security_middleware:
|
||||||
|
security_response = await self.security_middleware.check_token_management_access(request)
|
||||||
|
if security_response:
|
||||||
|
return security_response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if token manager is available
|
# Check if token manager is available
|
||||||
if not self.security_manager.auth_provider.token_manager:
|
if not self.security_manager.auth_provider.token_manager:
|
||||||
@@ -168,6 +212,12 @@ class TokenHandlers:
|
|||||||
|
|
||||||
async def handle_list_tokens(self, request: Request) -> JSONResponse:
|
async def handle_list_tokens(self, request: Request) -> JSONResponse:
|
||||||
"""Handle token listing request"""
|
"""Handle token listing request"""
|
||||||
|
# Apply security checks
|
||||||
|
if self.security_middleware:
|
||||||
|
security_response = await self.security_middleware.check_token_management_access(request)
|
||||||
|
if security_response:
|
||||||
|
return security_response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if token manager is available
|
# Check if token manager is available
|
||||||
if not self.security_manager.auth_provider.token_manager:
|
if not self.security_manager.auth_provider.token_manager:
|
||||||
@@ -192,6 +242,12 @@ class TokenHandlers:
|
|||||||
|
|
||||||
async def handle_token_stats(self, request: Request) -> JSONResponse:
|
async def handle_token_stats(self, request: Request) -> JSONResponse:
|
||||||
"""Handle token statistics request"""
|
"""Handle token statistics request"""
|
||||||
|
# Apply security checks
|
||||||
|
if self.security_middleware:
|
||||||
|
security_response = await self.security_middleware.check_token_management_access(request)
|
||||||
|
if security_response:
|
||||||
|
return security_response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if token manager is available
|
# Check if token manager is available
|
||||||
if not self.security_manager.auth_provider.token_manager:
|
if not self.security_manager.auth_provider.token_manager:
|
||||||
@@ -215,6 +271,12 @@ class TokenHandlers:
|
|||||||
|
|
||||||
async def handle_cleanup_tokens(self, request: Request) -> JSONResponse:
|
async def handle_cleanup_tokens(self, request: Request) -> JSONResponse:
|
||||||
"""Handle expired tokens cleanup request"""
|
"""Handle expired tokens cleanup request"""
|
||||||
|
# Apply security checks
|
||||||
|
if self.security_middleware:
|
||||||
|
security_response = await self.security_middleware.check_token_management_access(request)
|
||||||
|
if security_response:
|
||||||
|
return security_response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if token manager is available
|
# Check if token manager is available
|
||||||
if not self.security_manager.auth_provider.token_manager:
|
if not self.security_manager.auth_provider.token_manager:
|
||||||
@@ -237,8 +299,62 @@ class TokenHandlers:
|
|||||||
"error": f"Internal server error: {str(e)}"
|
"error": f"Internal server error: {str(e)}"
|
||||||
}, status_code=500)
|
}, status_code=500)
|
||||||
|
|
||||||
async def handle_demo_page(self, request: Request) -> HTMLResponse:
|
async def handle_management_page(self, request: Request) -> HTMLResponse:
|
||||||
"""Handle token management demo page"""
|
"""Handle token management demo page"""
|
||||||
|
# Apply security checks
|
||||||
|
if self.security_middleware:
|
||||||
|
security_response = await self.security_middleware.check_token_management_access(request)
|
||||||
|
if security_response:
|
||||||
|
# Convert JSON response to HTML for demo page
|
||||||
|
error_data = security_response.body.decode('utf-8') if hasattr(security_response, 'body') else '{"error": "Access denied"}'
|
||||||
|
try:
|
||||||
|
error_info = json.loads(error_data)
|
||||||
|
except:
|
||||||
|
error_info = {"error": "Access denied"}
|
||||||
|
|
||||||
|
error_html = f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Access Denied - Token Management</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: Arial, sans-serif; margin: 50px; background: #f5f5f5; }}
|
||||||
|
.container {{ max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; }}
|
||||||
|
.error {{ color: #dc3545; background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px; }}
|
||||||
|
.security-info {{ background: #d1ecf1; border: 1px solid #bee5eb; padding: 15px; border-radius: 5px; margin-top: 20px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔐 Token Management - Access Denied</h1>
|
||||||
|
<div class="error">
|
||||||
|
<h3>Access Denied</h3>
|
||||||
|
<p><strong>Error:</strong> {error_info.get('error', 'Access denied')}</p>
|
||||||
|
<p><strong>Message:</strong> {error_info.get('message', 'Token management access is restricted')}</p>
|
||||||
|
{'<p><strong>Your IP:</strong> ' + str(error_info.get('client_ip', 'Unknown')) + '</p>' if 'client_ip' in error_info else ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="security-info">
|
||||||
|
<h3>🛡️ Security Information</h3>
|
||||||
|
<p>Token management endpoints are protected by the following security measures:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>IP Restrictions:</strong> Only localhost/127.0.0.1 access allowed</li>
|
||||||
|
<li><strong>Admin Authentication:</strong> Valid admin token required</li>
|
||||||
|
<li><strong>Configuration Control:</strong> Must be explicitly enabled</li>
|
||||||
|
</ul>
|
||||||
|
<p>If you need access, please:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Access from the server host (127.0.0.1)</li>
|
||||||
|
<li>Ensure HTTP token management is enabled in configuration</li>
|
||||||
|
<li>Provide valid admin authentication</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return HTMLResponse(error_html, status_code=security_response.status_code)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if token manager is available
|
# Check if token manager is available
|
||||||
if not self.security_manager.auth_provider.token_manager:
|
if not self.security_manager.auth_provider.token_manager:
|
||||||
@@ -323,34 +439,51 @@ class TokenHandlers:
|
|||||||
<input type="text" id="token_id" name="token_id" placeholder="e.g., my-app-token" required>
|
<input type="text" id="token_id" name="token_id" placeholder="e.g., my-app-token" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="user_id">User ID (required):</label>
|
<label for="expires_hours">Expires Hours (optional):</label>
|
||||||
<input type="text" id="user_id" name="user_id" placeholder="e.g., john_doe" required>
|
<input type="number" id="expires_hours" name="expires_hours" placeholder="e.g., 720 (30 days), leave empty for default">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="roles">Roles (comma-separated):</label>
|
<label for="description">Description (optional):</label>
|
||||||
<input type="text" id="roles" name="roles" placeholder="e.g., data_analyst,viewer">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="permissions">Permissions (comma-separated):</label>
|
|
||||||
<input type="text" id="permissions" name="permissions" placeholder="e.g., read_data,query_database">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="security_level">Security Level:</label>
|
|
||||||
<select id="security_level" name="security_level">
|
|
||||||
<option value="public">Public</option>
|
|
||||||
<option value="internal" selected>Internal</option>
|
|
||||||
<option value="confidential">Confidential</option>
|
|
||||||
<option value="secret">Secret</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="expires_hours">Expires Hours (leave empty for default):</label>
|
|
||||||
<input type="number" id="expires_hours" name="expires_hours" placeholder="e.g., 720 (30 days)">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="description">Description:</label>
|
|
||||||
<textarea id="description" name="description" placeholder="Token description"></textarea>
|
<textarea id="description" name="description" placeholder="Token description"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="custom_token">Custom Token (optional):</label>
|
||||||
|
<input type="text" id="custom_token" name="custom_token" placeholder="Leave empty to auto-generate">
|
||||||
|
<small style="color: #666; display: block; margin-top: 5px;">If not provided, a secure token will be generated automatically</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" style="margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 5px;">
|
||||||
|
<h3>🗄️ Database Configuration (Optional)</h3>
|
||||||
|
<p style="color: #666; font-size: 14px; margin-bottom: 15px;">Configure database connection for this token. Leave empty to use system defaults.</p>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_host">Host:</label>
|
||||||
|
<input type="text" id="db_host" name="db_host" placeholder="localhost">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_port">Port:</label>
|
||||||
|
<input type="number" id="db_port" name="db_port" placeholder="9030">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_user">User:</label>
|
||||||
|
<input type="text" id="db_user" name="db_user" placeholder="root">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_password">Password:</label>
|
||||||
|
<input type="password" id="db_password" name="db_password" placeholder="(optional)">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_database">Database:</label>
|
||||||
|
<input type="text" id="db_database" name="db_database" placeholder="information_schema">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_fe_http_port">FE HTTP Port:</label>
|
||||||
|
<input type="number" id="db_fe_http_port" name="db_fe_http_port" placeholder="8030">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn-primary">Create Token</button>
|
<button type="submit" class="btn-primary">Create Token</button>
|
||||||
</form>
|
</form>
|
||||||
<div id="createTokenResponse"></div>
|
<div id="createTokenResponse"></div>
|
||||||
@@ -384,30 +517,81 @@ class TokenHandlers:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Get admin token from URL parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const adminToken = urlParams.get('admin_token');
|
||||||
|
|
||||||
|
// Create request headers with admin token
|
||||||
|
function getAuthHeaders() {{
|
||||||
|
if (adminToken) {{
|
||||||
|
return {{
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${{adminToken}}`
|
||||||
|
}};
|
||||||
|
}} else {{
|
||||||
|
return {{'Content-Type': 'application/json'}};
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Create URL with admin token parameter
|
||||||
|
function getAuthURL(baseUrl) {{
|
||||||
|
if (adminToken) {{
|
||||||
|
const separator = baseUrl.includes('?') ? '&' : '?';
|
||||||
|
return `${{baseUrl}}${{separator}}admin_token=${{encodeURIComponent(adminToken)}}`;
|
||||||
|
}}
|
||||||
|
return baseUrl;
|
||||||
|
}}
|
||||||
|
|
||||||
function showResponse(elementId, data, isSuccess = true) {{
|
function showResponse(elementId, data, isSuccess = true) {{
|
||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
element.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
|
element.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
|
||||||
element.className = 'response ' + (isSuccess ? 'success' : 'error');
|
element.className = 'response ' + (isSuccess ? 'success' : 'error');
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Create token form
|
// Create token form - updated to match actual API
|
||||||
document.getElementById('createTokenForm').addEventListener('submit', async (e) => {{
|
document.getElementById('createTokenForm').addEventListener('submit', async (e) => {{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(e.target);
|
const formData = new FormData(e.target);
|
||||||
const data = Object.fromEntries(formData.entries());
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
// Convert comma-separated values to arrays
|
// Remove empty fields for optional parameters
|
||||||
data.roles = data.roles ? data.roles.split(',').map(r => r.trim()).filter(r => r) : [];
|
if (!data.expires_hours) delete data.expires_hours;
|
||||||
data.permissions = data.permissions ? data.permissions.split(',').map(p => p.trim()).filter(p => p) : [];
|
if (!data.description) delete data.description;
|
||||||
|
if (!data.custom_token) delete data.custom_token;
|
||||||
|
|
||||||
|
// Handle database configuration
|
||||||
|
if (data.db_host) {{
|
||||||
|
data.database_config = {{
|
||||||
|
host: data.db_host,
|
||||||
|
port: data.db_port ? parseInt(data.db_port) : 9030,
|
||||||
|
user: data.db_user || 'root',
|
||||||
|
password: data.db_password || '',
|
||||||
|
database: data.db_database || 'information_schema',
|
||||||
|
fe_http_port: data.db_fe_http_port ? parseInt(data.db_fe_http_port) : 8030
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Remove individual database fields from data
|
||||||
|
delete data.db_host;
|
||||||
|
delete data.db_port;
|
||||||
|
delete data.db_user;
|
||||||
|
delete data.db_password;
|
||||||
|
delete data.db_database;
|
||||||
|
delete data.db_fe_http_port;
|
||||||
|
|
||||||
try {{
|
try {{
|
||||||
const response = await fetch('/token/create', {{
|
const response = await fetch(getAuthURL('/token/create'), {{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {{'Content-Type': 'application/json'}},
|
headers: getAuthHeaders(),
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
}});
|
}});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
showResponse('createTokenResponse', result, response.ok);
|
showResponse('createTokenResponse', result, response.ok);
|
||||||
|
|
||||||
|
// Refresh token list if creation was successful
|
||||||
|
if (response.ok) {{
|
||||||
|
document.getElementById('listTokensBtn').click();
|
||||||
|
}}
|
||||||
}} catch (error) {{
|
}} catch (error) {{
|
||||||
showResponse('createTokenResponse', {{error: error.message}}, false);
|
showResponse('createTokenResponse', {{error: error.message}}, false);
|
||||||
}}
|
}}
|
||||||
@@ -416,7 +600,9 @@ class TokenHandlers:
|
|||||||
// List tokens
|
// List tokens
|
||||||
document.getElementById('listTokensBtn').addEventListener('click', async () => {{
|
document.getElementById('listTokensBtn').addEventListener('click', async () => {{
|
||||||
try {{
|
try {{
|
||||||
const response = await fetch('/token/list');
|
const response = await fetch(getAuthURL('/token/list'), {{
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
}});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
showResponse('tokenListResponse', result, response.ok);
|
showResponse('tokenListResponse', result, response.ok);
|
||||||
}} catch (error) {{
|
}} catch (error) {{
|
||||||
@@ -427,7 +613,10 @@ class TokenHandlers:
|
|||||||
// Cleanup tokens
|
// Cleanup tokens
|
||||||
document.getElementById('cleanupTokensBtn').addEventListener('click', async () => {{
|
document.getElementById('cleanupTokensBtn').addEventListener('click', async () => {{
|
||||||
try {{
|
try {{
|
||||||
const response = await fetch('/token/cleanup', {{method: 'POST'}});
|
const response = await fetch(getAuthURL('/token/cleanup'), {{
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
}});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
showResponse('tokenListResponse', result, response.ok);
|
showResponse('tokenListResponse', result, response.ok);
|
||||||
}} catch (error) {{
|
}} catch (error) {{
|
||||||
@@ -444,11 +633,18 @@ class TokenHandlers:
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
try {{
|
try {{
|
||||||
const response = await fetch(`/token/revoke?token_id=${{encodeURIComponent(tokenId)}}`, {{
|
const response = await fetch(getAuthURL(`/token/revoke?token_id=${{encodeURIComponent(tokenId)}}`), {{
|
||||||
method: 'DELETE'
|
method: 'DELETE',
|
||||||
|
headers: getAuthHeaders()
|
||||||
}});
|
}});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
showResponse('revokeTokenResponse', result, response.ok);
|
showResponse('revokeTokenResponse', result, response.ok);
|
||||||
|
|
||||||
|
// Refresh token list if revocation was successful
|
||||||
|
if (response.ok) {{
|
||||||
|
document.getElementById('listTokensBtn').click();
|
||||||
|
document.getElementById('revokeTokenId').value = '';
|
||||||
|
}}
|
||||||
}} catch (error) {{
|
}} catch (error) {{
|
||||||
showResponse('revokeTokenResponse', {{error: error.message}}, false);
|
showResponse('revokeTokenResponse', {{error: error.message}}, false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
"""
|
"""
|
||||||
Token Authentication Management Module
|
Token Authentication Management Module
|
||||||
|
|
||||||
@@ -355,7 +371,8 @@ class TokenManager:
|
|||||||
token_id: str,
|
token_id: str,
|
||||||
expires_hours: Optional[int] = None,
|
expires_hours: Optional[int] = None,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
custom_token: Optional[str] = None
|
custom_token: Optional[str] = None,
|
||||||
|
database_config: Optional[DatabaseConfig] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new token"""
|
"""Create a new token"""
|
||||||
try:
|
try:
|
||||||
@@ -380,7 +397,8 @@ class TokenManager:
|
|||||||
token_info = TokenInfo(
|
token_info = TokenInfo(
|
||||||
token_id=token_id,
|
token_id=token_id,
|
||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
description=description
|
description=description,
|
||||||
|
database_config=database_config
|
||||||
)
|
)
|
||||||
|
|
||||||
# Hash and store token
|
# Hash and store token
|
||||||
@@ -390,6 +408,9 @@ class TokenManager:
|
|||||||
|
|
||||||
self.logger.info(f"Created new token '{token_id}'")
|
self.logger.info(f"Created new token '{token_id}'")
|
||||||
|
|
||||||
|
# Save token to file
|
||||||
|
self._save_token_to_file(token_id, raw_token, token_info)
|
||||||
|
|
||||||
return raw_token
|
return raw_token
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -410,12 +431,201 @@ class TokenManager:
|
|||||||
del self._token_ids[token_id]
|
del self._token_ids[token_id]
|
||||||
|
|
||||||
self.logger.info(f"Revoked token '{token_id}'")
|
self.logger.info(f"Revoked token '{token_id}'")
|
||||||
|
|
||||||
|
# Save updated tokens to file
|
||||||
|
self._remove_token_from_file(token_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to revoke token '{token_id}': {e}")
|
self.logger.error(f"Failed to revoke token '{token_id}': {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _save_tokens_to_file(self):
|
||||||
|
"""Save current tokens to JSON file"""
|
||||||
|
try:
|
||||||
|
# Convert current tokens to file format
|
||||||
|
tokens_list = []
|
||||||
|
|
||||||
|
for token_hash, token_info in self._tokens.items():
|
||||||
|
# Find the raw token for this token_info
|
||||||
|
raw_token = None
|
||||||
|
for tid, thash in self._token_ids.items():
|
||||||
|
if thash == token_hash and tid == token_info.token_id:
|
||||||
|
# We can't recover the original token from hash,
|
||||||
|
# so we'll create a placeholder for existing tokens
|
||||||
|
raw_token = f"<existing_token_hash_{token_hash[:8]}>"
|
||||||
|
break
|
||||||
|
|
||||||
|
if raw_token is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
token_config = {
|
||||||
|
"token_id": token_info.token_id,
|
||||||
|
"token": raw_token,
|
||||||
|
"description": token_info.description,
|
||||||
|
"expires_hours": None,
|
||||||
|
"is_active": token_info.is_active
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add expiration info
|
||||||
|
if token_info.expires_at:
|
||||||
|
# Calculate remaining hours from now
|
||||||
|
remaining = token_info.expires_at - datetime.utcnow()
|
||||||
|
if remaining.total_seconds() > 0:
|
||||||
|
token_config["expires_hours"] = int(remaining.total_seconds() / 3600)
|
||||||
|
else:
|
||||||
|
token_config["expires_hours"] = 0
|
||||||
|
|
||||||
|
# Add database config if present
|
||||||
|
if token_info.database_config:
|
||||||
|
token_config["database_config"] = {
|
||||||
|
"host": token_info.database_config.host,
|
||||||
|
"port": token_info.database_config.port,
|
||||||
|
"user": token_info.database_config.user,
|
||||||
|
"password": token_info.database_config.password,
|
||||||
|
"database": token_info.database_config.database,
|
||||||
|
"charset": token_info.database_config.charset,
|
||||||
|
"fe_http_port": token_info.database_config.fe_http_port
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens_list.append(token_config)
|
||||||
|
|
||||||
|
# Create file content
|
||||||
|
file_content = {
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Doris MCP Server Token configuration file",
|
||||||
|
"created_at": datetime.utcnow().isoformat() + "Z",
|
||||||
|
"tokens": tokens_list,
|
||||||
|
"notes": [
|
||||||
|
"This file is automatically updated when tokens are created or revoked",
|
||||||
|
"Please backup this file before making manual changes",
|
||||||
|
"Tokens with hash placeholders were loaded from previous configuration"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
with open(self.token_file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(file_content, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
self.logger.info(f"Saved {len(tokens_list)} tokens to file: {self.token_file_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to save tokens to file {self.token_file_path}: {e}")
|
||||||
|
|
||||||
|
def _save_token_to_file(self, token_id: str, raw_token: str, token_info: TokenInfo):
|
||||||
|
"""Save a single new token to file (for newly created tokens only)"""
|
||||||
|
try:
|
||||||
|
# Load existing file
|
||||||
|
existing_data = {"tokens": []}
|
||||||
|
if os.path.exists(self.token_file_path):
|
||||||
|
try:
|
||||||
|
with open(self.token_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
existing_data = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Could not load existing token file: {e}")
|
||||||
|
|
||||||
|
# Ensure tokens list exists
|
||||||
|
if 'tokens' not in existing_data or not isinstance(existing_data['tokens'], list):
|
||||||
|
existing_data['tokens'] = []
|
||||||
|
|
||||||
|
# Check if token already exists in file
|
||||||
|
token_exists = False
|
||||||
|
for i, token_config in enumerate(existing_data['tokens']):
|
||||||
|
if token_config.get('token_id') == token_id:
|
||||||
|
# Update existing token
|
||||||
|
existing_data['tokens'][i] = self._token_info_to_config(token_id, raw_token, token_info)
|
||||||
|
token_exists = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add new token if it doesn't exist
|
||||||
|
if not token_exists:
|
||||||
|
new_token_config = self._token_info_to_config(token_id, raw_token, token_info)
|
||||||
|
existing_data['tokens'].append(new_token_config)
|
||||||
|
|
||||||
|
# Update metadata
|
||||||
|
existing_data.update({
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Doris MCP Server Token configuration file",
|
||||||
|
"updated_at": datetime.utcnow().isoformat() + "Z"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
with open(self.token_file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(existing_data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
self.logger.info(f"Saved token '{token_id}' to file: {self.token_file_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to save token '{token_id}' to file: {e}")
|
||||||
|
|
||||||
|
def _token_info_to_config(self, token_id: str, raw_token: str, token_info: TokenInfo) -> dict:
|
||||||
|
"""Convert TokenInfo to file configuration format"""
|
||||||
|
token_config = {
|
||||||
|
"token_id": token_id,
|
||||||
|
"token": raw_token,
|
||||||
|
"description": token_info.description,
|
||||||
|
"expires_hours": None,
|
||||||
|
"is_active": token_info.is_active
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add expiration info
|
||||||
|
if token_info.expires_at:
|
||||||
|
# Calculate remaining hours from creation time
|
||||||
|
remaining = token_info.expires_at - token_info.created_at
|
||||||
|
token_config["expires_hours"] = int(remaining.total_seconds() / 3600) if remaining.total_seconds() > 0 else None
|
||||||
|
|
||||||
|
# Add database config if present
|
||||||
|
if token_info.database_config:
|
||||||
|
token_config["database_config"] = {
|
||||||
|
"host": token_info.database_config.host,
|
||||||
|
"port": token_info.database_config.port,
|
||||||
|
"user": token_info.database_config.user,
|
||||||
|
"password": token_info.database_config.password,
|
||||||
|
"database": token_info.database_config.database,
|
||||||
|
"charset": token_info.database_config.charset,
|
||||||
|
"fe_http_port": token_info.database_config.fe_http_port
|
||||||
|
}
|
||||||
|
|
||||||
|
return token_config
|
||||||
|
|
||||||
|
def _remove_token_from_file(self, token_id: str):
|
||||||
|
"""Remove a token from the JSON file"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(self.token_file_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load existing file
|
||||||
|
with open(self.token_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
existing_data = json.load(f)
|
||||||
|
|
||||||
|
if 'tokens' not in existing_data or not isinstance(existing_data['tokens'], list):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Remove the token
|
||||||
|
original_count = len(existing_data['tokens'])
|
||||||
|
existing_data['tokens'] = [
|
||||||
|
token for token in existing_data['tokens']
|
||||||
|
if token.get('token_id') != token_id
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(existing_data['tokens']) < original_count:
|
||||||
|
# Update metadata
|
||||||
|
existing_data.update({
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Doris MCP Server Token configuration file",
|
||||||
|
"updated_at": datetime.utcnow().isoformat() + "Z"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
with open(self.token_file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(existing_data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
self.logger.info(f"Removed token '{token_id}' from file: {self.token_file_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to remove token '{token_id}' from file: {e}")
|
||||||
|
|
||||||
async def list_tokens(self) -> List[Dict[str, Any]]:
|
async def list_tokens(self) -> List[Dict[str, Any]]:
|
||||||
"""List all tokens (without sensitive data)"""
|
"""List all tokens (without sensitive data)"""
|
||||||
tokens = []
|
tokens = []
|
||||||
|
|||||||
227
doris_mcp_server/auth/token_security_middleware.py
Normal file
227
doris_mcp_server/auth/token_security_middleware.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""
|
||||||
|
Token Management Security Middleware
|
||||||
|
|
||||||
|
Provides comprehensive security controls for token management endpoints including
|
||||||
|
IP restrictions, admin authentication, and configuration-based access control.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import ipaddress
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
from ..utils.logger import get_logger
|
||||||
|
from ..utils.config import DorisConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TokenSecurityMiddleware:
|
||||||
|
"""Security middleware for token management endpoints"""
|
||||||
|
|
||||||
|
def __init__(self, config: DorisConfig):
|
||||||
|
self.config = config
|
||||||
|
self.logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# Initialize admin token hash if provided
|
||||||
|
self._admin_token_hash = None
|
||||||
|
if config.security.token_management_admin_token:
|
||||||
|
self._admin_token_hash = self._hash_token(config.security.token_management_admin_token)
|
||||||
|
|
||||||
|
# Normalize allowed IPs
|
||||||
|
self._allowed_networks = self._parse_allowed_networks(
|
||||||
|
config.security.token_management_allowed_ips
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info(f"Token management security initialized: "
|
||||||
|
f"HTTP endpoints {'enabled' if config.security.enable_http_token_management else 'disabled'}, "
|
||||||
|
f"Admin auth {'required' if config.security.require_admin_auth else 'optional'}, "
|
||||||
|
f"Allowed networks: {len(self._allowed_networks)}")
|
||||||
|
|
||||||
|
def _hash_token(self, token: str) -> str:
|
||||||
|
"""Hash token using SHA-256"""
|
||||||
|
return hashlib.sha256(token.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
def _parse_allowed_networks(self, allowed_ips: List[str]) -> List[ipaddress.IPv4Network | ipaddress.IPv6Network]:
|
||||||
|
"""Parse allowed IP addresses and networks"""
|
||||||
|
networks = []
|
||||||
|
for ip_str in allowed_ips:
|
||||||
|
try:
|
||||||
|
# Try to parse as network (CIDR notation)
|
||||||
|
if '/' in ip_str:
|
||||||
|
networks.append(ipaddress.ip_network(ip_str, strict=False))
|
||||||
|
else:
|
||||||
|
# Parse as single IP and convert to /32 network
|
||||||
|
ip = ipaddress.ip_address(ip_str)
|
||||||
|
if isinstance(ip, ipaddress.IPv4Address):
|
||||||
|
networks.append(ipaddress.IPv4Network(f"{ip}/32"))
|
||||||
|
else:
|
||||||
|
networks.append(ipaddress.IPv6Network(f"{ip}/128"))
|
||||||
|
except ValueError as e:
|
||||||
|
self.logger.warning(f"Invalid IP/network '{ip_str}': {e}")
|
||||||
|
|
||||||
|
return networks
|
||||||
|
|
||||||
|
def _get_client_ip(self, request: Request) -> str:
|
||||||
|
"""Extract client IP from request, considering proxies"""
|
||||||
|
# Check X-Forwarded-For header first (for proxy setups)
|
||||||
|
forwarded_for = request.headers.get('X-Forwarded-For')
|
||||||
|
if forwarded_for:
|
||||||
|
# Take the first IP (original client)
|
||||||
|
client_ip = forwarded_for.split(',')[0].strip()
|
||||||
|
elif request.headers.get('X-Real-IP'):
|
||||||
|
client_ip = request.headers.get('X-Real-IP')
|
||||||
|
else:
|
||||||
|
# Direct connection
|
||||||
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
|
|
||||||
|
return client_ip
|
||||||
|
|
||||||
|
def _is_ip_allowed(self, client_ip: str) -> bool:
|
||||||
|
"""Check if client IP is in allowed networks"""
|
||||||
|
try:
|
||||||
|
client_addr = ipaddress.ip_address(client_ip)
|
||||||
|
|
||||||
|
for network in self._allowed_networks:
|
||||||
|
if client_addr in network:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
self.logger.warning(f"Invalid client IP format: {client_ip}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _extract_admin_token(self, request: Request) -> Optional[str]:
|
||||||
|
"""Extract admin token from request headers"""
|
||||||
|
# Try Authorization header first
|
||||||
|
auth_header = request.headers.get('Authorization', '')
|
||||||
|
if auth_header.startswith('Bearer '):
|
||||||
|
return auth_header[7:]
|
||||||
|
elif auth_header.startswith('Token '):
|
||||||
|
return auth_header[6:]
|
||||||
|
|
||||||
|
# Try X-Admin-Token header
|
||||||
|
admin_token = request.headers.get('X-Admin-Token', '')
|
||||||
|
if admin_token:
|
||||||
|
return admin_token
|
||||||
|
|
||||||
|
# Try query parameter as fallback (not recommended for production)
|
||||||
|
admin_token = request.query_params.get('admin_token', '')
|
||||||
|
if admin_token:
|
||||||
|
self.logger.warning("Admin token passed via query parameter - this is insecure for production")
|
||||||
|
return admin_token
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _verify_admin_token(self, provided_token: str) -> bool:
|
||||||
|
"""Verify provided admin token against configured token"""
|
||||||
|
if not self._admin_token_hash:
|
||||||
|
self.logger.warning("No admin token configured for token management")
|
||||||
|
return False
|
||||||
|
|
||||||
|
provided_hash = self._hash_token(provided_token)
|
||||||
|
|
||||||
|
# Use constant-time comparison to prevent timing attacks
|
||||||
|
return hmac.compare_digest(self._admin_token_hash, provided_hash)
|
||||||
|
|
||||||
|
async def check_token_management_access(self, request: Request) -> Optional[JSONResponse]:
|
||||||
|
"""
|
||||||
|
Check if request is authorized for token management operations
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None if access is granted
|
||||||
|
JSONResponse with error if access is denied
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if HTTP token management is enabled
|
||||||
|
if not self.config.security.enable_http_token_management:
|
||||||
|
self.logger.warning(f"Token management endpoint access denied - HTTP management disabled: {request.url.path}")
|
||||||
|
return JSONResponse({
|
||||||
|
"error": "Token management endpoints are disabled for security",
|
||||||
|
"message": "HTTP token management is disabled. Use file-based token management instead.",
|
||||||
|
"suggestion": "Edit tokens.json file directly or enable HTTP management with proper security configuration"
|
||||||
|
}, status_code=403)
|
||||||
|
|
||||||
|
# Extract client IP
|
||||||
|
client_ip = self._get_client_ip(request)
|
||||||
|
|
||||||
|
# Check IP restrictions
|
||||||
|
if not self._is_ip_allowed(client_ip):
|
||||||
|
self.logger.warning(f"Token management access denied for IP {client_ip}: not in allowed list")
|
||||||
|
return JSONResponse({
|
||||||
|
"error": "Access denied - IP not allowed",
|
||||||
|
"client_ip": client_ip,
|
||||||
|
"message": "Token management is restricted to specific IP addresses",
|
||||||
|
"allowed_networks": [str(net) for net in self._allowed_networks]
|
||||||
|
}, status_code=403)
|
||||||
|
|
||||||
|
# Check admin authentication if required
|
||||||
|
if self.config.security.require_admin_auth:
|
||||||
|
admin_token = self._extract_admin_token(request)
|
||||||
|
|
||||||
|
if not admin_token:
|
||||||
|
self.logger.warning(f"Token management access denied for IP {client_ip}: missing admin token")
|
||||||
|
return JSONResponse({
|
||||||
|
"error": "Admin authentication required",
|
||||||
|
"message": "Token management requires admin authentication",
|
||||||
|
"hint": "Provide admin token in Authorization header: 'Bearer <admin_token>' or 'X-Admin-Token: <admin_token>'"
|
||||||
|
}, status_code=401)
|
||||||
|
|
||||||
|
if not self._verify_admin_token(admin_token):
|
||||||
|
self.logger.warning(f"Token management access denied for IP {client_ip}: invalid admin token")
|
||||||
|
return JSONResponse({
|
||||||
|
"error": "Invalid admin token",
|
||||||
|
"message": "The provided admin token is invalid"
|
||||||
|
}, status_code=401)
|
||||||
|
|
||||||
|
# Log successful access
|
||||||
|
self.logger.info(f"Token management access granted for IP {client_ip} to {request.url.path}")
|
||||||
|
|
||||||
|
# Access granted
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_security_info(self) -> Dict[str, Any]:
|
||||||
|
"""Get current security configuration info (for demo/status pages)"""
|
||||||
|
return {
|
||||||
|
"http_token_management_enabled": self.config.security.enable_http_token_management,
|
||||||
|
"admin_auth_required": self.config.security.require_admin_auth,
|
||||||
|
"admin_token_configured": bool(self._admin_token_hash),
|
||||||
|
"allowed_networks_count": len(self._allowed_networks),
|
||||||
|
"allowed_networks": [str(net) for net in self._allowed_networks],
|
||||||
|
"security_features": [
|
||||||
|
"IP address restrictions",
|
||||||
|
"Admin token authentication" if self.config.security.require_admin_auth else "Optional admin authentication",
|
||||||
|
"Secure token hashing",
|
||||||
|
"Request logging and auditing"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_admin_token(self) -> str:
|
||||||
|
"""Generate a secure admin token"""
|
||||||
|
return secrets.token_urlsafe(32)
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience function for middleware creation
|
||||||
|
def create_token_security_middleware(config: DorisConfig) -> TokenSecurityMiddleware:
|
||||||
|
"""Create token security middleware with configuration"""
|
||||||
|
return TokenSecurityMiddleware(config)
|
||||||
@@ -546,7 +546,7 @@ class DorisServer:
|
|||||||
|
|
||||||
# Token management endpoints
|
# Token management endpoints
|
||||||
from .auth.token_handlers import TokenHandlers
|
from .auth.token_handlers import TokenHandlers
|
||||||
token_handlers = TokenHandlers(self.security_manager)
|
token_handlers = TokenHandlers(self.security_manager, self.config)
|
||||||
|
|
||||||
async def token_create(request):
|
async def token_create(request):
|
||||||
return await token_handlers.handle_create_token(request)
|
return await token_handlers.handle_create_token(request)
|
||||||
@@ -563,8 +563,8 @@ class DorisServer:
|
|||||||
async def token_cleanup(request):
|
async def token_cleanup(request):
|
||||||
return await token_handlers.handle_cleanup_tokens(request)
|
return await token_handlers.handle_cleanup_tokens(request)
|
||||||
|
|
||||||
async def token_demo(request):
|
async def token_management(request):
|
||||||
return await token_handlers.handle_demo_page(request)
|
return await token_handlers.handle_management_page(request)
|
||||||
|
|
||||||
# Lifecycle manager - simplified since we manage session_manager externally
|
# Lifecycle manager - simplified since we manage session_manager externally
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
@@ -592,7 +592,7 @@ class DorisServer:
|
|||||||
Route("/token/list", token_list, methods=["GET"]),
|
Route("/token/list", token_list, methods=["GET"]),
|
||||||
Route("/token/stats", token_stats, methods=["GET"]),
|
Route("/token/stats", token_stats, methods=["GET"]),
|
||||||
Route("/token/cleanup", token_cleanup, methods=["GET", "POST"]),
|
Route("/token/cleanup", token_cleanup, methods=["GET", "POST"]),
|
||||||
Route("/token/demo", token_demo, methods=["GET"]),
|
Route("/token/management", token_management, methods=["GET"]),
|
||||||
],
|
],
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
"""
|
"""
|
||||||
Multi-worker application module for doris-mcp-server
|
Multi-worker application module for doris-mcp-server
|
||||||
|
|
||||||
@@ -396,7 +412,7 @@ async def initialize_worker():
|
|||||||
from .auth.oauth_handlers import OAuthHandlers
|
from .auth.oauth_handlers import OAuthHandlers
|
||||||
from .auth.token_handlers import TokenHandlers
|
from .auth.token_handlers import TokenHandlers
|
||||||
_oauth_handlers = OAuthHandlers(_worker_security_manager)
|
_oauth_handlers = OAuthHandlers(_worker_security_manager)
|
||||||
_token_handlers = TokenHandlers(_worker_security_manager)
|
_token_handlers = TokenHandlers(_worker_security_manager, config)
|
||||||
|
|
||||||
_worker_initialized = True
|
_worker_initialized = True
|
||||||
logger.info(f"Worker {os.getpid()} MCP initialization completed successfully")
|
logger.info(f"Worker {os.getpid()} MCP initialization completed successfully")
|
||||||
@@ -481,12 +497,12 @@ async def token_cleanup(request):
|
|||||||
return JSONResponse({"error": "Token handlers not initialized"}, status_code=503)
|
return JSONResponse({"error": "Token handlers not initialized"}, status_code=503)
|
||||||
return await _token_handlers.handle_cleanup_tokens(request)
|
return await _token_handlers.handle_cleanup_tokens(request)
|
||||||
|
|
||||||
async def token_demo(request):
|
async def token_management(request):
|
||||||
"""Token demo page endpoint"""
|
"""Token management page endpoint"""
|
||||||
if not _token_handlers:
|
if not _token_handlers:
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
return HTMLResponse("<h1>Token handlers not initialized</h1>")
|
return HTMLResponse("<h1>Token handlers not initialized</h1>")
|
||||||
return await _token_handlers.handle_demo_page(request)
|
return await _token_handlers.handle_management_page(request)
|
||||||
|
|
||||||
async def root_info(request):
|
async def root_info(request):
|
||||||
"""Root endpoint"""
|
"""Root endpoint"""
|
||||||
@@ -593,7 +609,7 @@ basic_app = Starlette(
|
|||||||
Route("/token/list", token_list, methods=["GET"]),
|
Route("/token/list", token_list, methods=["GET"]),
|
||||||
Route("/token/stats", token_stats, methods=["GET"]),
|
Route("/token/stats", token_stats, methods=["GET"]),
|
||||||
Route("/token/cleanup", token_cleanup, methods=["GET", "POST"]),
|
Route("/token/cleanup", token_cleanup, methods=["GET", "POST"]),
|
||||||
Route("/token/demo", token_demo, methods=["GET"]),
|
Route("/token/management", token_management, methods=["GET"]),
|
||||||
],
|
],
|
||||||
lifespan=lifespan
|
lifespan=lifespan
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ class SecurityConfig:
|
|||||||
default_token_expiry_hours: int = 24 * 30 # Default expiry: 30 days
|
default_token_expiry_hours: int = 24 * 30 # Default expiry: 30 days
|
||||||
token_hash_algorithm: str = "sha256" # Token hashing algorithm: sha256, sha512
|
token_hash_algorithm: str = "sha256" # Token hashing algorithm: sha256, sha512
|
||||||
|
|
||||||
|
# Token Management Security (New in v0.6.0)
|
||||||
|
enable_http_token_management: bool = False # Enable HTTP token management endpoints (default: disabled for security)
|
||||||
|
token_management_admin_token: str = "" # Admin token for token management endpoints (required if HTTP management enabled)
|
||||||
|
token_management_allowed_ips: list[str] = field(default_factory=lambda: ["127.0.0.1", "::1", "localhost"]) # Allowed IPs for token management
|
||||||
|
require_admin_auth: bool = True # Require admin authentication for token management (default: true)
|
||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration
|
||||||
jwt_algorithm: str = "RS256" # RS256, ES256, HS256
|
jwt_algorithm: str = "RS256" # RS256, ES256, HS256
|
||||||
jwt_issuer: str = "doris-mcp-server"
|
jwt_issuer: str = "doris-mcp-server"
|
||||||
@@ -469,6 +475,21 @@ class DorisConfig:
|
|||||||
os.getenv("DEFAULT_TOKEN_EXPIRY_HOURS", str(config.security.default_token_expiry_hours))
|
os.getenv("DEFAULT_TOKEN_EXPIRY_HOURS", str(config.security.default_token_expiry_hours))
|
||||||
)
|
)
|
||||||
config.security.token_hash_algorithm = os.getenv("TOKEN_HASH_ALGORITHM", config.security.token_hash_algorithm)
|
config.security.token_hash_algorithm = os.getenv("TOKEN_HASH_ALGORITHM", config.security.token_hash_algorithm)
|
||||||
|
|
||||||
|
# Token Management Security Configuration (New in v0.6.0)
|
||||||
|
config.security.enable_http_token_management = (
|
||||||
|
os.getenv("ENABLE_HTTP_TOKEN_MANAGEMENT", str(config.security.enable_http_token_management).lower()).lower() == "true"
|
||||||
|
)
|
||||||
|
config.security.token_management_admin_token = os.getenv("TOKEN_MANAGEMENT_ADMIN_TOKEN", config.security.token_management_admin_token)
|
||||||
|
|
||||||
|
# Parse allowed IPs from comma-separated string
|
||||||
|
allowed_ips_str = os.getenv("TOKEN_MANAGEMENT_ALLOWED_IPS", "")
|
||||||
|
if allowed_ips_str:
|
||||||
|
config.security.token_management_allowed_ips = [ip.strip() for ip in allowed_ips_str.split(",") if ip.strip()]
|
||||||
|
|
||||||
|
config.security.require_admin_auth = (
|
||||||
|
os.getenv("REQUIRE_ADMIN_AUTH", str(config.security.require_admin_auth).lower()).lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
# Performance configuration
|
# Performance configuration
|
||||||
config.performance.enable_query_cache = (
|
config.performance.enable_query_cache = (
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from sqlparse.sql import Statement
|
|||||||
from sqlparse.tokens import Keyword, Name
|
from sqlparse.tokens import Keyword, Name
|
||||||
|
|
||||||
from .logger import get_logger
|
from .logger import get_logger
|
||||||
|
from .config import DatabaseConfig
|
||||||
|
|
||||||
|
|
||||||
class SecurityLevel(Enum):
|
class SecurityLevel(Enum):
|
||||||
@@ -333,7 +334,8 @@ class DorisSecurityManager:
|
|||||||
token_id: str,
|
token_id: str,
|
||||||
expires_hours: Optional[int] = None,
|
expires_hours: Optional[int] = None,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
custom_token: Optional[str] = None
|
custom_token: Optional[str] = None,
|
||||||
|
database_config: Optional[DatabaseConfig] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new API access token
|
"""Create a new API access token
|
||||||
|
|
||||||
@@ -342,6 +344,7 @@ class DorisSecurityManager:
|
|||||||
expires_hours: Token expiration in hours (None for no expiration)
|
expires_hours: Token expiration in hours (None for no expiration)
|
||||||
description: Token description for management purposes
|
description: Token description for management purposes
|
||||||
custom_token: Custom token string (if None, generates random token)
|
custom_token: Custom token string (if None, generates random token)
|
||||||
|
database_config: Optional database configuration for this token
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Generated token string
|
Generated token string
|
||||||
@@ -353,7 +356,8 @@ class DorisSecurityManager:
|
|||||||
token_id=token_id,
|
token_id=token_id,
|
||||||
expires_hours=expires_hours,
|
expires_hours=expires_hours,
|
||||||
description=description,
|
description=description,
|
||||||
custom_token=custom_token
|
custom_token=custom_token,
|
||||||
|
database_config=database_config
|
||||||
)
|
)
|
||||||
|
|
||||||
async def revoke_token(self, token_id: str) -> bool:
|
async def revoke_token(self, token_id: str) -> bool:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"description": "Simplified Token configuration file for Doris MCP Server API access control",
|
"description": "Doris MCP Server Token configuration file",
|
||||||
"created_at": "2025-09-01T00:00:00Z",
|
"created_at": "2025-09-01T00:00:00Z",
|
||||||
"tokens": [
|
"tokens": [
|
||||||
{
|
{
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"token_id": "analyst-token",
|
"token_id": "analyst-token",
|
||||||
"token": "doris_analyst_token_123456",
|
"token": "doris_analyst_token_123456",
|
||||||
"description": "Doris analyst API access token",
|
"description": "Doris analyst API access token",
|
||||||
"expires_hours": 8760,
|
"expires_hours": 8760,
|
||||||
|
|||||||
Reference in New Issue
Block a user