3 Commits

Author SHA1 Message Date
2431d3cfb0 test: add comprehensive authentication middleware test (issue #4)
- Add Test 5 to integration_test.sh for authentication verification
- Test admin endpoints reject unauthorized requests properly
- Test admin endpoints work with valid JWT tokens
- Test KV endpoints respect anonymous access configuration
- Extract and use auto-generated root account tokens

docs: update README and CLAUDE.md for recent security features

- Document allow_anonymous_read and allow_anonymous_write config options
- Update API documentation with authentication requirements
- Add security notes about DELETE operations always requiring auth
- Update configuration table with new anonymous access settings
- Document new authentication test coverage in CLAUDE.md

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 12:34:15 +03:00
b4f57b3604 feat: add anonymous access configuration for KV endpoints (issue #5)
- Add AllowAnonymousRead and AllowAnonymousWrite config parameters
- Set both to false by default for security
- Apply conditional authentication middleware to KV endpoints:
  - GET requires auth if AllowAnonymousRead is false
  - PUT requires auth if AllowAnonymousWrite is false
  - DELETE always requires authentication (no anonymous delete)
- Update integration tests to enable anonymous access for testing
- Maintain backward compatibility when AuthEnabled is false

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 12:22:14 +03:00
e6d87d025f fix: secure admin endpoints with authentication middleware (issue #4)
- Add config parameter to AuthService constructor
- Implement proper config-based auth checks in middleware
- Wrap all admin endpoints (users, groups, tokens) with authentication
- Apply granular scopes: admin:users:*, admin:groups:*, admin:tokens:*
- Maintain backward compatibility when config is nil

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 12:15:38 +03:00
9 changed files with 185 additions and 26 deletions

View File

@@ -99,15 +99,21 @@ type StoredValue struct {
### Configuration Architecture ### Configuration Architecture
The system uses feature toggles extensively (`types/Config:271-276`): The system uses feature toggles extensively (`types/Config:271-280`):
```yaml ```yaml
auth_enabled: true # JWT authentication system auth_enabled: true # JWT authentication system
tamper_logging_enabled: true # Cryptographic audit trail tamper_logging_enabled: true # Cryptographic audit trail
clustering_enabled: true # Gossip protocol and sync clustering_enabled: true # Gossip protocol and sync
rate_limiting_enabled: true # Per-client rate limiting rate_limiting_enabled: true # Per-client rate limiting
revision_history_enabled: true # Automatic versioning revision_history_enabled: true # Automatic versioning
# Anonymous access control (Issue #5 - when auth_enabled: true)
allow_anonymous_read: false # Allow unauthenticated read access to KV endpoints
allow_anonymous_write: false # Allow unauthenticated write access to KV endpoints
``` ```
**Security Note**: DELETE operations always require authentication when `auth_enabled: true`, regardless of anonymous access settings.
### Testing Strategy ### Testing Strategy
#### Integration Test Suite (`integration_test.sh`) #### Integration Test Suite (`integration_test.sh`)
@@ -115,6 +121,11 @@ revision_history_enabled: true # Automatic versioning
- **Basic functionality** - Single-node CRUD operations - **Basic functionality** - Single-node CRUD operations
- **Cluster formation** - 2-node gossip protocol and data replication - **Cluster formation** - 2-node gossip protocol and data replication
- **Conflict resolution** - Automated conflict detection and resolution using `test_conflict.go` - **Conflict resolution** - Automated conflict detection and resolution using `test_conflict.go`
- **Authentication middleware** - Comprehensive security testing (Issue #4):
- Admin endpoints properly reject unauthenticated requests
- Admin endpoints work with valid JWT tokens
- KV endpoints respect anonymous access configuration
- Automatic root account creation and token extraction
The test suite uses sophisticated retry logic and timing to handle the eventually consistent nature of the system. The test suite uses sophisticated retry logic and timing to handle the eventually consistent nature of the system.

View File

@@ -113,6 +113,10 @@ clustering_enabled: true # Gossip protocol and sync
rate_limiting_enabled: true # Rate limiting rate_limiting_enabled: true # Rate limiting
revision_history_enabled: true # Automatic versioning revision_history_enabled: true # Automatic versioning
# Anonymous access control (when auth_enabled: true)
allow_anonymous_read: false # Allow unauthenticated read access to KV endpoints
allow_anonymous_write: false # Allow unauthenticated write access to KV endpoints
# Backup configuration # Backup configuration
backup_enabled: true # Automated backups backup_enabled: true # Automated backups
backup_schedule: "0 0 * * *" # Daily at midnight (cron format) backup_schedule: "0 0 * * *" # Daily at midnight (cron format)
@@ -134,7 +138,7 @@ backup_retention: 7 # Days to keep backups
```bash ```bash
PUT /kv/{path} PUT /kv/{path}
Content-Type: application/json Content-Type: application/json
Authorization: Bearer <jwt-token> # Required if auth_enabled Authorization: Bearer <jwt-token> # Required if auth_enabled && !allow_anonymous_write
# Basic storage # Basic storage
curl -X PUT http://localhost:8080/kv/users/john/profile \ curl -X PUT http://localhost:8080/kv/users/john/profile \
@@ -158,7 +162,7 @@ curl -X PUT http://localhost:8080/kv/cache/session/abc123 \
#### Retrieve Data #### Retrieve Data
```bash ```bash
GET /kv/{path} GET /kv/{path}
Authorization: Bearer <jwt-token> # Required if auth_enabled Authorization: Bearer <jwt-token> # Required if auth_enabled && !allow_anonymous_read
curl -H "Authorization: Bearer eyJ..." http://localhost:8080/kv/users/john/profile curl -H "Authorization: Bearer eyJ..." http://localhost:8080/kv/users/john/profile
@@ -177,7 +181,7 @@ curl -H "Authorization: Bearer eyJ..." http://localhost:8080/kv/users/john/profi
#### Delete Data #### Delete Data
```bash ```bash
DELETE /kv/{path} DELETE /kv/{path}
Authorization: Bearer <jwt-token> # Required if auth_enabled Authorization: Bearer <jwt-token> # Always required when auth_enabled (no anonymous delete)
curl -X DELETE -H "Authorization: Bearer eyJ..." http://localhost:8080/kv/users/john/profile curl -X DELETE -H "Authorization: Bearer eyJ..." http://localhost:8080/kv/users/john/profile
# Returns: 204 No Content # Returns: 204 No Content
@@ -532,6 +536,8 @@ type StoredValue struct {
| `bootstrap_max_age_hours` | Max historical data to sync | 720 hours | 30 days default | | `bootstrap_max_age_hours` | Max historical data to sync | 720 hours | 30 days default |
| **Feature Toggles** | | **Feature Toggles** |
| `auth_enabled` | JWT authentication system | true | Complete auth/authz system | | `auth_enabled` | JWT authentication system | true | Complete auth/authz system |
| `allow_anonymous_read` | Allow unauthenticated read access | false | When auth_enabled, controls KV GET endpoints |
| `allow_anonymous_write` | Allow unauthenticated write access | false | When auth_enabled, controls KV PUT endpoints |
| `clustering_enabled` | Gossip protocol and sync | true | Distributed mode | | `clustering_enabled` | Gossip protocol and sync | true | Distributed mode |
| `compression_enabled` | ZSTD compression | true | Reduces storage size | | `compression_enabled` | ZSTD compression | true | Reduces storage size |
| `rate_limiting_enabled` | Rate limiting | true | Per-client limits | | `rate_limiting_enabled` | Rate limiting | true | Per-client limits |

View File

@@ -26,13 +26,15 @@ type AuthContext struct {
type AuthService struct { type AuthService struct {
db *badger.DB db *badger.DB
logger *logrus.Logger logger *logrus.Logger
config *types.Config
} }
// NewAuthService creates a new authentication service // NewAuthService creates a new authentication service
func NewAuthService(db *badger.DB, logger *logrus.Logger) *AuthService { func NewAuthService(db *badger.DB, logger *logrus.Logger, config *types.Config) *AuthService {
return &AuthService{ return &AuthService{
db: db, db: db,
logger: logger, logger: logger,
config: config,
} }
} }

View File

@@ -138,11 +138,12 @@ func (s *RateLimitService) RateLimitMiddleware(next http.HandlerFunc) http.Handl
} }
} }
// isAuthEnabled checks if authentication is enabled (would be passed from config) // isAuthEnabled checks if authentication is enabled from config
func (s *AuthService) isAuthEnabled() bool { func (s *AuthService) isAuthEnabled() bool {
// This would normally be injected from config, but for now we'll assume enabled if s.config != nil {
// TODO: Inject config dependency return s.config.AuthEnabled
return true }
return true // Default to enabled if no config
} }
// Helper method to check rate limits (simplified version) // Helper method to check rate limits (simplified version)

View File

@@ -55,6 +55,10 @@ func Default() *types.Config {
ClusteringEnabled: true, ClusteringEnabled: true,
RateLimitingEnabled: true, RateLimitingEnabled: true,
RevisionHistoryEnabled: true, RevisionHistoryEnabled: true,
// Default anonymous access settings (both disabled by default for security)
AllowAnonymousRead: false,
AllowAnonymousWrite: false,
} }
} }

View File

@@ -91,6 +91,8 @@ port: 8090
data_dir: "./basic_data" data_dir: "./basic_data"
seed_nodes: [] seed_nodes: []
log_level: "error" log_level: "error"
allow_anonymous_read: true
allow_anonymous_write: true
EOF EOF
# Start node # Start node
@@ -134,6 +136,8 @@ log_level: "error"
gossip_interval_min: 5 gossip_interval_min: 5
gossip_interval_max: 10 gossip_interval_max: 10
sync_interval: 10 sync_interval: 10
allow_anonymous_read: true
allow_anonymous_write: true
EOF EOF
# Node 2 config # Node 2 config
@@ -147,6 +151,8 @@ log_level: "error"
gossip_interval_min: 5 gossip_interval_min: 5
gossip_interval_max: 10 gossip_interval_max: 10
sync_interval: 10 sync_interval: 10
allow_anonymous_read: true
allow_anonymous_write: true
EOF EOF
# Start nodes # Start nodes
@@ -242,6 +248,8 @@ data_dir: "./conflict1_data"
seed_nodes: [] seed_nodes: []
log_level: "info" log_level: "info"
sync_interval: 3 sync_interval: 3
allow_anonymous_read: true
allow_anonymous_write: true
EOF EOF
cat > conflict2.yaml <<EOF cat > conflict2.yaml <<EOF
@@ -252,6 +260,8 @@ data_dir: "./conflict2_data"
seed_nodes: ["127.0.0.1:8111"] seed_nodes: ["127.0.0.1:8111"]
log_level: "info" log_level: "info"
sync_interval: 3 sync_interval: 3
allow_anonymous_read: true
allow_anonymous_write: true
EOF EOF
# Start nodes # Start nodes
@@ -351,6 +361,79 @@ EOF
fi fi
} }
# Test 5: Authentication middleware (Issue #4)
test_authentication_middleware() {
test_start "Authentication middleware test (Issue #4)"
# Create auth test config
cat > auth_test.yaml <<EOF
node_id: "auth-test"
bind_address: "127.0.0.1"
port: 8095
data_dir: "./auth_test_data"
seed_nodes: []
log_level: "error"
auth_enabled: true
allow_anonymous_read: false
allow_anonymous_write: false
EOF
# Start node
$BINARY auth_test.yaml >auth_test.log 2>&1 &
local pid=$!
if wait_for_service 8095; then
sleep 2 # Allow root account creation
# Extract the token from logs
local token=$(grep "Token:" auth_test.log | sed 's/.*Token: //' | tr -d '\n\r')
if [ -z "$token" ]; then
log_error "Failed to extract authentication token from logs"
kill $pid 2>/dev/null || true
return
fi
# Test 1: Admin endpoints should fail without authentication
local no_auth_response=$(curl -s -X POST http://localhost:8095/api/users -H "Content-Type: application/json" -d '{"nickname":"test","password":"test"}')
if echo "$no_auth_response" | grep -q "Unauthorized"; then
log_success "Admin endpoints properly reject unauthenticated requests"
else
log_error "Admin endpoints should reject unauthenticated requests, got: $no_auth_response"
fi
# Test 2: Admin endpoints should work with valid authentication
local auth_response=$(curl -s -X POST http://localhost:8095/api/users -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"nickname":"authtest","password":"authtest"}')
if echo "$auth_response" | grep -q "uuid"; then
log_success "Admin endpoints work with valid authentication"
else
log_error "Admin endpoints should work with authentication, got: $auth_response"
fi
# Test 3: KV endpoints should require auth when anonymous access is disabled
local kv_no_auth=$(curl -s -X PUT http://localhost:8095/kv/test/auth -H "Content-Type: application/json" -d '{"test":"auth"}')
if echo "$kv_no_auth" | grep -q "Unauthorized"; then
log_success "KV endpoints properly require authentication when anonymous access disabled"
else
log_error "KV endpoints should require auth when anonymous access disabled, got: $kv_no_auth"
fi
# Test 4: KV endpoints should work with valid authentication
local kv_auth=$(curl -s -X PUT http://localhost:8095/kv/test/auth -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"test":"auth"}')
if echo "$kv_auth" | grep -q "uuid\|timestamp" || [ -z "$kv_auth" ]; then
log_success "KV endpoints work with valid authentication"
else
log_error "KV endpoints should work with authentication, got: $kv_auth"
fi
kill $pid 2>/dev/null || true
sleep 2
else
log_error "Auth test node failed to start"
kill $pid 2>/dev/null || true
fi
}
# Main test execution # Main test execution
main() { main() {
echo "==================================================" echo "=================================================="
@@ -368,6 +451,7 @@ main() {
test_basic_functionality test_basic_functionality
test_cluster_formation test_cluster_formation
test_conflict_resolution test_conflict_resolution
test_authentication_middleware
# Results # Results
echo "==================================================" echo "=================================================="

View File

@@ -11,10 +11,33 @@ func (s *Server) setupRoutes() *mux.Router {
// Health endpoint (always available) // Health endpoint (always available)
router.HandleFunc("/health", s.healthHandler).Methods("GET") router.HandleFunc("/health", s.healthHandler).Methods("GET")
// KV endpoints (always available - see issue #5 for anonymous access control) // KV endpoints (with conditional authentication based on anonymous access settings)
router.HandleFunc("/kv/{path:.+}", s.getKVHandler).Methods("GET") // GET endpoint - require auth if anonymous read is disabled
router.HandleFunc("/kv/{path:.+}", s.putKVHandler).Methods("PUT") if s.config.AuthEnabled && !s.config.AllowAnonymousRead {
router.HandleFunc("/kv/{path:.+}", s.deleteKVHandler).Methods("DELETE") router.Handle("/kv/{path:.+}", s.authService.Middleware(
[]string{"read"}, nil, "",
)(s.getKVHandler)).Methods("GET")
} else {
router.HandleFunc("/kv/{path:.+}", s.getKVHandler).Methods("GET")
}
// PUT endpoint - require auth if anonymous write is disabled
if s.config.AuthEnabled && !s.config.AllowAnonymousWrite {
router.Handle("/kv/{path:.+}", s.authService.Middleware(
[]string{"write"}, nil, "",
)(s.putKVHandler)).Methods("PUT")
} else {
router.HandleFunc("/kv/{path:.+}", s.putKVHandler).Methods("PUT")
}
// DELETE endpoint - always require authentication (no anonymous delete)
if s.config.AuthEnabled {
router.Handle("/kv/{path:.+}", s.authService.Middleware(
[]string{"delete"}, nil, "",
)(s.deleteKVHandler)).Methods("DELETE")
} else {
router.HandleFunc("/kv/{path:.+}", s.deleteKVHandler).Methods("DELETE")
}
// Member endpoints (available when clustering is enabled) // Member endpoints (available when clustering is enabled)
if s.config.ClusteringEnabled { if s.config.ClusteringEnabled {
@@ -32,20 +55,44 @@ func (s *Server) setupRoutes() *mux.Router {
// Authentication and user management endpoints (available when auth is enabled) // Authentication and user management endpoints (available when auth is enabled)
if s.config.AuthEnabled { if s.config.AuthEnabled {
// User Management endpoints // User Management endpoints (with authentication middleware)
router.HandleFunc("/api/users", s.createUserHandler).Methods("POST") router.Handle("/api/users", s.authService.Middleware(
router.HandleFunc("/api/users/{uuid}", s.getUserHandler).Methods("GET") []string{"admin:users:create"}, nil, "",
router.HandleFunc("/api/users/{uuid}", s.updateUserHandler).Methods("PUT") )(s.createUserHandler)).Methods("POST")
router.HandleFunc("/api/users/{uuid}", s.deleteUserHandler).Methods("DELETE")
router.Handle("/api/users/{uuid}", s.authService.Middleware(
[]string{"admin:users:read"}, nil, "",
)(s.getUserHandler)).Methods("GET")
router.Handle("/api/users/{uuid}", s.authService.Middleware(
[]string{"admin:users:update"}, nil, "",
)(s.updateUserHandler)).Methods("PUT")
router.Handle("/api/users/{uuid}", s.authService.Middleware(
[]string{"admin:users:delete"}, nil, "",
)(s.deleteUserHandler)).Methods("DELETE")
// Group Management endpoints // Group Management endpoints (with authentication middleware)
router.HandleFunc("/api/groups", s.createGroupHandler).Methods("POST") router.Handle("/api/groups", s.authService.Middleware(
router.HandleFunc("/api/groups/{uuid}", s.getGroupHandler).Methods("GET") []string{"admin:groups:create"}, nil, "",
router.HandleFunc("/api/groups/{uuid}", s.updateGroupHandler).Methods("PUT") )(s.createGroupHandler)).Methods("POST")
router.HandleFunc("/api/groups/{uuid}", s.deleteGroupHandler).Methods("DELETE")
router.Handle("/api/groups/{uuid}", s.authService.Middleware(
[]string{"admin:groups:read"}, nil, "",
)(s.getGroupHandler)).Methods("GET")
router.Handle("/api/groups/{uuid}", s.authService.Middleware(
[]string{"admin:groups:update"}, nil, "",
)(s.updateGroupHandler)).Methods("PUT")
router.Handle("/api/groups/{uuid}", s.authService.Middleware(
[]string{"admin:groups:delete"}, nil, "",
)(s.deleteGroupHandler)).Methods("DELETE")
// Token Management endpoints // Token Management endpoints (with authentication middleware)
router.HandleFunc("/api/tokens", s.createTokenHandler).Methods("POST") router.Handle("/api/tokens", s.authService.Middleware(
[]string{"admin:tokens:create"}, nil, "",
)(s.createTokenHandler)).Methods("POST")
} }
// Revision History endpoints (available when revision history is enabled) // Revision History endpoints (available when revision history is enabled)

View File

@@ -118,7 +118,7 @@ func NewServer(config *types.Config) (*Server, error) {
server.revisionService = storage.NewRevisionService(storageService) server.revisionService = storage.NewRevisionService(storageService)
// Initialize authentication service // Initialize authentication service
server.authService = auth.NewAuthService(db, logger) server.authService = auth.NewAuthService(db, logger, config)
// Setup initial root account if needed (Issue #3) // Setup initial root account if needed (Issue #3)
if config.AuthEnabled { if config.AuthEnabled {

View File

@@ -273,4 +273,8 @@ type Config struct {
ClusteringEnabled bool `yaml:"clustering_enabled"` // Enable/disable clustering/gossip ClusteringEnabled bool `yaml:"clustering_enabled"` // Enable/disable clustering/gossip
RateLimitingEnabled bool `yaml:"rate_limiting_enabled"` // Enable/disable rate limiting RateLimitingEnabled bool `yaml:"rate_limiting_enabled"` // Enable/disable rate limiting
RevisionHistoryEnabled bool `yaml:"revision_history_enabled"` // Enable/disable revision history RevisionHistoryEnabled bool `yaml:"revision_history_enabled"` // Enable/disable revision history
// Anonymous access control (Issue #5)
AllowAnonymousRead bool `yaml:"allow_anonymous_read"` // Allow unauthenticated read access to KV endpoints
AllowAnonymousWrite bool `yaml:"allow_anonymous_write"` // Allow unauthenticated write access to KV endpoints
} }