Compare commits
7 Commits
aae9022bb2
...
secure-clu
| Author | SHA1 | Date | |
|---|---|---|---|
| 852275945c | |||
| c7dcebb894 | |||
| 2431d3cfb0 | |||
| b4f57b3604 | |||
| e6d87d025f | |||
| 3aff0ab5ef | |||
| 8d6a280441 |
13
CLAUDE.md
13
CLAUDE.md
@@ -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.
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -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 |
|
||||||
|
|||||||
27
auth/auth.go
27
auth/auth.go
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,3 +205,26 @@ func GetAuthContext(ctx context.Context) *AuthContext {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasUsers checks if any users exist in the database
|
||||||
|
func (s *AuthService) HasUsers() (bool, error) {
|
||||||
|
var hasUsers bool
|
||||||
|
|
||||||
|
err := s.db.View(func(txn *badger.Txn) error {
|
||||||
|
opts := badger.DefaultIteratorOptions
|
||||||
|
opts.PrefetchValues = false // We only need to check if keys exist
|
||||||
|
iterator := txn.NewIterator(opts)
|
||||||
|
defer iterator.Close()
|
||||||
|
|
||||||
|
// Look for any key starting with "user:"
|
||||||
|
prefix := []byte("user:")
|
||||||
|
for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() {
|
||||||
|
hasUsers = true
|
||||||
|
return nil // Found at least one user, can exit early
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return hasUsers, err
|
||||||
|
}
|
||||||
|
|||||||
77
auth/cluster.go
Normal file
77
auth/cluster.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClusterAuthService handles authentication for inter-cluster communication
|
||||||
|
type ClusterAuthService struct {
|
||||||
|
clusterSecret string
|
||||||
|
logger *logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClusterAuthService creates a new cluster authentication service
|
||||||
|
func NewClusterAuthService(clusterSecret string, logger *logrus.Logger) *ClusterAuthService {
|
||||||
|
return &ClusterAuthService{
|
||||||
|
clusterSecret: clusterSecret,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware validates cluster authentication headers
|
||||||
|
func (s *ClusterAuthService) Middleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Extract authentication headers
|
||||||
|
clusterSecret := r.Header.Get("X-Cluster-Secret")
|
||||||
|
nodeID := r.Header.Get("X-Node-ID")
|
||||||
|
|
||||||
|
// Log authentication attempt
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"node_id": nodeID,
|
||||||
|
"remote_addr": r.RemoteAddr,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
"method": r.Method,
|
||||||
|
}).Debug("Cluster authentication attempt")
|
||||||
|
|
||||||
|
// Validate cluster secret
|
||||||
|
if clusterSecret == "" {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"node_id": nodeID,
|
||||||
|
"remote_addr": r.RemoteAddr,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
}).Warn("Missing X-Cluster-Secret header")
|
||||||
|
http.Error(w, "Unauthorized: Missing cluster secret", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if clusterSecret != s.clusterSecret {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"node_id": nodeID,
|
||||||
|
"remote_addr": r.RemoteAddr,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
}).Warn("Invalid cluster secret")
|
||||||
|
http.Error(w, "Unauthorized: Invalid cluster secret", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate node ID is present
|
||||||
|
if nodeID == "" {
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"remote_addr": r.RemoteAddr,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
}).Warn("Missing X-Node-ID header")
|
||||||
|
http.Error(w, "Unauthorized: Missing node ID", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication successful
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"node_id": nodeID,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
}).Debug("Cluster authentication successful")
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -82,10 +82,19 @@ func (s *BootstrapService) attemptJoin(seedAddr string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
client := NewAuthenticatedHTTPClient(s.config, 10*time.Second)
|
||||||
url := fmt.Sprintf("http://%s/members/join", seedAddr)
|
protocol := GetProtocol(s.config)
|
||||||
|
url := fmt.Sprintf("%s://%s/members/join", protocol, seedAddr)
|
||||||
|
|
||||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Error("Failed to create join request")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
AddClusterAuthHeaders(req, s.config)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.WithFields(logrus.Fields{
|
s.logger.WithFields(logrus.Fields{
|
||||||
"seed": seedAddr,
|
"seed": seedAddr,
|
||||||
|
|||||||
@@ -181,11 +181,20 @@ func (s *GossipService) gossipWithPeer(peer *types.Member) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send HTTP request to peer
|
// Send HTTP request to peer with cluster authentication
|
||||||
client := &http.Client{Timeout: 5 * time.Second}
|
client := NewAuthenticatedHTTPClient(s.config, 5*time.Second)
|
||||||
url := fmt.Sprintf("http://%s/members/gossip", peer.Address)
|
protocol := GetProtocol(s.config)
|
||||||
|
url := fmt.Sprintf("%s://%s/members/gossip", protocol, peer.Address)
|
||||||
|
|
||||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Error("Failed to create gossip request")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
AddClusterAuthHeaders(req, s.config)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.WithFields(logrus.Fields{
|
s.logger.WithFields(logrus.Fields{
|
||||||
"peer": peer.Address,
|
"peer": peer.Address,
|
||||||
|
|||||||
43
cluster/http_client.go
Normal file
43
cluster/http_client.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"kvs/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewAuthenticatedHTTPClient creates an HTTP client configured for cluster authentication
|
||||||
|
func NewAuthenticatedHTTPClient(config *types.Config, timeout time.Duration) *http.Client {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure TLS if enabled
|
||||||
|
if config.ClusterTLSEnabled {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: config.ClusterTLSSkipVerify,
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddClusterAuthHeaders adds authentication headers to an HTTP request
|
||||||
|
func AddClusterAuthHeaders(req *http.Request, config *types.Config) {
|
||||||
|
req.Header.Set("X-Cluster-Secret", config.ClusterSecret)
|
||||||
|
req.Header.Set("X-Node-ID", config.NodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProtocol returns the appropriate protocol (http or https) based on TLS configuration
|
||||||
|
func GetProtocol(config *types.Config) string {
|
||||||
|
if config.ClusterTLSEnabled {
|
||||||
|
return "https"
|
||||||
|
}
|
||||||
|
return "http"
|
||||||
|
}
|
||||||
@@ -186,10 +186,17 @@ func (s *SyncService) performMerkleSync() {
|
|||||||
|
|
||||||
// requestMerkleRoot requests the Merkle root from a peer
|
// requestMerkleRoot requests the Merkle root from a peer
|
||||||
func (s *SyncService) requestMerkleRoot(peerAddress string) (*types.MerkleRootResponse, error) {
|
func (s *SyncService) requestMerkleRoot(peerAddress string) (*types.MerkleRootResponse, error) {
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
client := NewAuthenticatedHTTPClient(s.config, 10*time.Second)
|
||||||
url := fmt.Sprintf("http://%s/merkle_tree/root", peerAddress)
|
protocol := GetProtocol(s.config)
|
||||||
|
url := fmt.Sprintf("%s://%s/merkle_tree/root", protocol, peerAddress)
|
||||||
|
|
||||||
resp, err := client.Get(url)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
AddClusterAuthHeaders(req, s.config)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -294,10 +301,17 @@ func (s *SyncService) handleLeafLevelDiff(peerAddress string, keys []string, loc
|
|||||||
|
|
||||||
// fetchSingleKVFromPeer fetches a single KV pair from a peer
|
// fetchSingleKVFromPeer fetches a single KV pair from a peer
|
||||||
func (s *SyncService) fetchSingleKVFromPeer(peerAddress, path string) (*types.StoredValue, error) {
|
func (s *SyncService) fetchSingleKVFromPeer(peerAddress, path string) (*types.StoredValue, error) {
|
||||||
client := &http.Client{Timeout: 5 * time.Second}
|
client := NewAuthenticatedHTTPClient(s.config, 5*time.Second)
|
||||||
url := fmt.Sprintf("http://%s/kv/%s", peerAddress, path)
|
protocol := GetProtocol(s.config)
|
||||||
|
url := fmt.Sprintf("%s://%s/kv/%s", protocol, peerAddress, path)
|
||||||
|
|
||||||
resp, err := client.Get(url)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
AddClusterAuthHeaders(req, s.config)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -461,16 +475,24 @@ func (s *SyncService) resolveConflict(key string, local, remote *types.StoredVal
|
|||||||
}
|
}
|
||||||
|
|
||||||
// requestMerkleDiff requests children hashes or keys for a given node/range from a peer
|
// requestMerkleDiff requests children hashes or keys for a given node/range from a peer
|
||||||
func (s *SyncService) requestMerkleDiff(peerAddress string, req types.MerkleTreeDiffRequest) (*types.MerkleTreeDiffResponse, error) {
|
func (s *SyncService) requestMerkleDiff(peerAddress string, reqData types.MerkleTreeDiffRequest) (*types.MerkleTreeDiffResponse, error) {
|
||||||
jsonData, err := json.Marshal(req)
|
jsonData, err := json.Marshal(reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
client := NewAuthenticatedHTTPClient(s.config, 10*time.Second)
|
||||||
url := fmt.Sprintf("http://%s/merkle_tree/diff", peerAddress)
|
protocol := GetProtocol(s.config)
|
||||||
|
url := fmt.Sprintf("%s://%s/merkle_tree/diff", protocol, peerAddress)
|
||||||
|
|
||||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
AddClusterAuthHeaders(req, s.config)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -525,20 +547,28 @@ func (s *SyncService) handleChildrenDiff(peerAddress string, children []types.Me
|
|||||||
|
|
||||||
// fetchAndStoreRange fetches a range of KV pairs from a peer and stores them locally
|
// fetchAndStoreRange fetches a range of KV pairs from a peer and stores them locally
|
||||||
func (s *SyncService) fetchAndStoreRange(peerAddress string, startKey, endKey string) error {
|
func (s *SyncService) fetchAndStoreRange(peerAddress string, startKey, endKey string) error {
|
||||||
req := types.KVRangeRequest{
|
reqData := types.KVRangeRequest{
|
||||||
StartKey: startKey,
|
StartKey: startKey,
|
||||||
EndKey: endKey,
|
EndKey: endKey,
|
||||||
Limit: 0, // No limit
|
Limit: 0, // No limit
|
||||||
}
|
}
|
||||||
jsonData, err := json.Marshal(req)
|
jsonData, err := json.Marshal(reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: 30 * time.Second} // Longer timeout for range fetches
|
client := NewAuthenticatedHTTPClient(s.config, 30*time.Second) // Longer timeout for range fetches
|
||||||
url := fmt.Sprintf("http://%s/kv_range", peerAddress)
|
protocol := GetProtocol(s.config)
|
||||||
|
url := fmt.Sprintf("%s://%s/kv_range", protocol, peerAddress)
|
||||||
|
|
||||||
resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
AddClusterAuthHeaders(req, s.config)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"kvs/types"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"kvs/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
@@ -55,9 +57,33 @@ 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,
|
||||||
|
|
||||||
|
// Default cluster authentication settings (Issue #13)
|
||||||
|
ClusterSecret: generateClusterSecret(),
|
||||||
|
ClusterTLSEnabled: false,
|
||||||
|
ClusterTLSCertFile: "",
|
||||||
|
ClusterTLSKeyFile: "",
|
||||||
|
ClusterTLSSkipVerify: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateClusterSecret generates a cryptographically secure random cluster secret
|
||||||
|
func generateClusterSecret() string {
|
||||||
|
// Generate 32 bytes (256 bits) of random data
|
||||||
|
randomBytes := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(randomBytes); err != nil {
|
||||||
|
// Fallback to a warning - this should never happen in practice
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: Failed to generate secure cluster secret: %v\n", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Encode as base64 for easy configuration file storage
|
||||||
|
return base64.StdEncoding.EncodeToString(randomBytes)
|
||||||
|
}
|
||||||
|
|
||||||
// Load configuration from file or create default
|
// Load configuration from file or create default
|
||||||
func Load(configPath string) (*types.Config, error) {
|
func Load(configPath string) (*types.Config, error) {
|
||||||
config := Default()
|
config := Default()
|
||||||
@@ -90,5 +116,13 @@ func Load(configPath string) (*types.Config, error) {
|
|||||||
return nil, fmt.Errorf("failed to parse config file: %v", err)
|
return nil, fmt.Errorf("failed to parse config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate cluster secret if not provided and clustering is enabled (Issue #13)
|
||||||
|
if config.ClusteringEnabled && config.ClusterSecret == "" {
|
||||||
|
config.ClusterSecret = generateClusterSecret()
|
||||||
|
fmt.Printf("Warning: No cluster_secret configured. Generated a random secret.\n")
|
||||||
|
fmt.Printf(" To share this secret with other nodes, add it to your config:\n")
|
||||||
|
fmt.Printf(" cluster_secret: %s\n", config.ClusterSecret)
|
||||||
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -123,6 +125,9 @@ EOF
|
|||||||
test_cluster_formation() {
|
test_cluster_formation() {
|
||||||
test_start "2-node cluster formation and Merkle Tree replication"
|
test_start "2-node cluster formation and Merkle Tree replication"
|
||||||
|
|
||||||
|
# Shared cluster secret for authentication (Issue #13)
|
||||||
|
local CLUSTER_SECRET="test-cluster-secret-12345678901234567890"
|
||||||
|
|
||||||
# Node 1 config
|
# Node 1 config
|
||||||
cat > cluster1.yaml <<EOF
|
cat > cluster1.yaml <<EOF
|
||||||
node_id: "cluster-1"
|
node_id: "cluster-1"
|
||||||
@@ -134,6 +139,9 @@ 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
|
||||||
|
cluster_secret: "$CLUSTER_SECRET"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Node 2 config
|
# Node 2 config
|
||||||
@@ -147,6 +155,9 @@ 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
|
||||||
|
cluster_secret: "$CLUSTER_SECRET"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start nodes
|
# Start nodes
|
||||||
@@ -233,6 +244,9 @@ test_conflict_resolution() {
|
|||||||
if go run test_conflict.go "$TEST_DIR/conflict1_data" "$TEST_DIR/conflict2_data"; then
|
if go run test_conflict.go "$TEST_DIR/conflict1_data" "$TEST_DIR/conflict2_data"; then
|
||||||
cd "$TEST_DIR"
|
cd "$TEST_DIR"
|
||||||
|
|
||||||
|
# Shared cluster secret for authentication (Issue #13)
|
||||||
|
local CLUSTER_SECRET="conflict-cluster-secret-1234567890123"
|
||||||
|
|
||||||
# Create configs
|
# Create configs
|
||||||
cat > conflict1.yaml <<EOF
|
cat > conflict1.yaml <<EOF
|
||||||
node_id: "conflict-1"
|
node_id: "conflict-1"
|
||||||
@@ -242,6 +256,9 @@ 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
|
||||||
|
cluster_secret: "$CLUSTER_SECRET"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat > conflict2.yaml <<EOF
|
cat > conflict2.yaml <<EOF
|
||||||
@@ -252,6 +269,9 @@ 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
|
||||||
|
cluster_secret: "$CLUSTER_SECRET"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start nodes
|
# Start nodes
|
||||||
@@ -351,6 +371,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 +461,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 "=================================================="
|
||||||
|
|||||||
65
issues/2.md
Normal file
65
issues/2.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Issue #2: Update README.md
|
||||||
|
|
||||||
|
**Status:** ✅ **COMPLETED** *(updated during this session)*
|
||||||
|
**Author:** MrKalzu
|
||||||
|
**Created:** 2025-09-12 22:01:34 +03:00
|
||||||
|
**Repository:** https://git.rauhala.info/ryyst/kalzu-value-store/issues/2
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
"It feels like the readme has lot of expired info after the latest update."
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The project's README file contained outdated information that needed to be revised following recent updates and refactoring.
|
||||||
|
|
||||||
|
## Resolution Status
|
||||||
|
|
||||||
|
**✅ COMPLETED** - The README.md has been comprehensively updated to reflect the current state of the codebase.
|
||||||
|
|
||||||
|
## Updates Made
|
||||||
|
|
||||||
|
### Architecture & Features
|
||||||
|
- ✅ Updated key features to include Merkle Tree sync, JWT authentication, and modular architecture
|
||||||
|
- ✅ Revised architecture diagram to show modular components
|
||||||
|
- ✅ Added authentication and authorization sections
|
||||||
|
- ✅ Updated conflict resolution description
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- ✅ Added comprehensive configuration options including feature toggles
|
||||||
|
- ✅ Updated default values to match actual implementation
|
||||||
|
- ✅ Added feature toggle documentation (auth, clustering, compression, etc.)
|
||||||
|
- ✅ Included backup and tamper logging configuration
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
- ✅ Added JWT authentication examples
|
||||||
|
- ✅ Updated API endpoints with proper authorization headers
|
||||||
|
- ✅ Added authentication endpoints documentation
|
||||||
|
- ✅ Included Merkle tree and sync endpoints
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
- ✅ Completely updated project structure to reflect modular architecture
|
||||||
|
- ✅ Documented all packages (auth/, cluster/, storage/, server/, etc.)
|
||||||
|
- ✅ Updated file organization to match current codebase
|
||||||
|
|
||||||
|
### Development & Testing
|
||||||
|
- ✅ Updated build and test commands
|
||||||
|
- ✅ Added integration test suite documentation
|
||||||
|
- ✅ Updated conflict resolution testing procedures
|
||||||
|
- ✅ Added code quality tools documentation
|
||||||
|
|
||||||
|
### Performance & Limitations
|
||||||
|
- ✅ Updated performance characteristics with Merkle sync improvements
|
||||||
|
- ✅ Revised limitations to reflect implemented features
|
||||||
|
- ✅ Added realistic timing expectations
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
The README.md now accurately reflects:
|
||||||
|
- Current modular architecture
|
||||||
|
- All implemented features and capabilities
|
||||||
|
- Proper configuration options
|
||||||
|
- Updated development workflow
|
||||||
|
- Comprehensive API documentation
|
||||||
|
|
||||||
|
**This issue has been resolved.**
|
||||||
71
issues/3.md
Normal file
71
issues/3.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Issue #3: Implement Autogenerated Root Account for Initial Setup
|
||||||
|
|
||||||
|
**Status:** ✅ **COMPLETED**
|
||||||
|
**Author:** MrKalzu
|
||||||
|
**Created:** 2025-09-12 22:17:12 +03:00
|
||||||
|
**Repository:** https://git.rauhala.info/ryyst/kalzu-value-store/issues/3
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The KVS server lacks a mechanism to create an initial administrative user when starting with an empty database and no seed nodes. This makes it impossible to interact with authentication-protected endpoints during initial setup.
|
||||||
|
|
||||||
|
## Current Challenge
|
||||||
|
|
||||||
|
- Empty database + no seed nodes = no way to authenticate
|
||||||
|
- No existing users means no way to create API tokens
|
||||||
|
- Authentication-protected endpoints become inaccessible
|
||||||
|
- Manual database seeding required for initial setup
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
### 1. Detection Logic
|
||||||
|
- Detect empty database condition
|
||||||
|
- Verify no seed nodes are configured
|
||||||
|
- Only trigger on initial startup with empty state
|
||||||
|
|
||||||
|
### 2. Root Account Generation
|
||||||
|
Create a default "root" user with:
|
||||||
|
- **Server-generated UUID**
|
||||||
|
- **Hashed nickname** (e.g., "root")
|
||||||
|
- **Assigned to default "admin" group**
|
||||||
|
- **Full administrative privileges**
|
||||||
|
|
||||||
|
### 3. API Token Creation
|
||||||
|
- Generate API token with administrative scopes
|
||||||
|
- Include all necessary permissions for initial setup
|
||||||
|
- Set reasonable expiration time
|
||||||
|
|
||||||
|
### 4. Secure Token Distribution
|
||||||
|
- **Securely log the token to console** (one-time display)
|
||||||
|
- **Persist user and token in BadgerDB**
|
||||||
|
- **Clear token from memory after logging**
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Relevant Code Sections
|
||||||
|
- `NewServer` function - Add initialization logic
|
||||||
|
- `User`, `Group`, `APIToken` structs - Use existing data structures
|
||||||
|
- Hashing and storage key functions - Leverage existing auth system
|
||||||
|
|
||||||
|
### Proposed Changes (from MrKalzu's comment)
|
||||||
|
- **Added `HasUsers() (bool, error)`** to `auth/auth.go`
|
||||||
|
- **Added "Initial root account setup for empty DB with no seeds"** to `server/server.go`
|
||||||
|
- **Diff file attached** with implementation details
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Token should be displayed only once during startup
|
||||||
|
- Token should have reasonable expiration
|
||||||
|
- Root account should be clearly identified in logs
|
||||||
|
- Consider forcing password change on first use (future enhancement)
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- Enables zero-configuration initial setup
|
||||||
|
- Provides secure bootstrap process
|
||||||
|
- Eliminates manual database seeding
|
||||||
|
- Supports automated deployment scenarios
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This issue blocks **Issue #4** (securing administrative endpoints), as it provides the mechanism for initial administrative access.
|
||||||
59
issues/4.md
Normal file
59
issues/4.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Issue #4: Secure User and Group Management Endpoints with Authentication Middleware
|
||||||
|
|
||||||
|
**Status:** Open
|
||||||
|
**Author:** MrKalzu
|
||||||
|
**Created:** 2025-09-12
|
||||||
|
**Assignee:** ryyst
|
||||||
|
**Repository:** https://git.rauhala.info/ryyst/kalzu-value-store/issues/4
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
**Security Vulnerability:** User, group, and token management API endpoints are currently exposed without authentication, creating a significant security risk.
|
||||||
|
|
||||||
|
## Current Problem
|
||||||
|
|
||||||
|
The following administrative endpoints are accessible without authentication:
|
||||||
|
- User management endpoints (`createUserHandler`, `getUserHandler`, etc.)
|
||||||
|
- Group management endpoints
|
||||||
|
- Token management endpoints
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
### 1. Define Granular Administrative Scopes
|
||||||
|
|
||||||
|
Create specific administrative scopes for fine-grained access control:
|
||||||
|
- `admin:users:create` - Create new users
|
||||||
|
- `admin:users:read` - View user information
|
||||||
|
- `admin:users:update` - Modify user data
|
||||||
|
- `admin:users:delete` - Remove users
|
||||||
|
- `admin:groups:create` - Create new groups
|
||||||
|
- `admin:groups:read` - View group information
|
||||||
|
- `admin:groups:update` - Modify group membership
|
||||||
|
- `admin:groups:delete` - Remove groups
|
||||||
|
- `admin:tokens:create` - Generate API tokens
|
||||||
|
- `admin:tokens:revoke` - Revoke API tokens
|
||||||
|
|
||||||
|
### 2. Apply Authentication Middleware
|
||||||
|
|
||||||
|
Wrap all administrative handlers with `authMiddleware` and specific scope requirements:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Example implementation
|
||||||
|
router.Handle("/auth/users", authMiddleware("admin:users:create")(createUserHandler))
|
||||||
|
router.Handle("/auth/users/{id}", authMiddleware("admin:users:read")(getUserHandler))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Depends on Issue #3**: Requires implementation of autogenerated root account for initial setup
|
||||||
|
|
||||||
|
## Security Benefits
|
||||||
|
|
||||||
|
- Prevents unauthorized administrative access
|
||||||
|
- Implements principle of least privilege
|
||||||
|
- Provides audit trail for administrative operations
|
||||||
|
- Protects against privilege escalation attacks
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
**High Priority** - This addresses a critical security vulnerability that could allow unauthorized access to administrative functions.
|
||||||
47
issues/5.md
Normal file
47
issues/5.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Issue #5: Add Configuration for Anonymous Read and Write Access to KV Endpoints
|
||||||
|
|
||||||
|
**Status:** Open
|
||||||
|
**Author:** MrKalzu
|
||||||
|
**Created:** 2025-09-12
|
||||||
|
**Repository:** https://git.rauhala.info/ryyst/kalzu-value-store/issues/5
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Currently, KV endpoints are publicly accessible without authentication. This issue proposes adding granular control over public access to key-value store functionality.
|
||||||
|
|
||||||
|
## Proposed Configuration Parameters
|
||||||
|
|
||||||
|
Add two new configuration parameters to the `Config` struct:
|
||||||
|
|
||||||
|
1. **`AllowAnonymousRead`** (boolean, default `false`)
|
||||||
|
- Controls whether unauthenticated users can read data
|
||||||
|
|
||||||
|
2. **`AllowAnonymousWrite`** (boolean, default `false`)
|
||||||
|
- Controls whether unauthenticated users can write data
|
||||||
|
|
||||||
|
## Proposed Implementation Changes
|
||||||
|
|
||||||
|
### Modify `setupRoutes` Function
|
||||||
|
- Conditionally apply authentication middleware based on configuration flags
|
||||||
|
|
||||||
|
### Specific Handler Changes
|
||||||
|
- **`getKVHandler`**: Apply auth middleware with "read" scope if `AllowAnonymousRead` is `false`
|
||||||
|
- **`putKVHandler`**: Apply auth middleware with "write" scope if `AllowAnonymousWrite` is `false`
|
||||||
|
- **`deleteKVHandler`**: Always require authentication (no anonymous delete)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Provide granular control over public access to key-value store functionality while maintaining security for sensitive operations.
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Public read-only deployments**: Allow anonymous reading for public data
|
||||||
|
- **Public write scenarios**: Allow anonymous data submission (like forms or logs)
|
||||||
|
- **Secure deployments**: Require authentication for all operations
|
||||||
|
- **Mixed access patterns**: Different permissions for read vs write operations
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Delete operations should always require authentication
|
||||||
|
- Consider rate limiting for anonymous access
|
||||||
|
- Audit logging should track anonymous operations differently
|
||||||
46
issues/6.md
Normal file
46
issues/6.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Issue #6: Configuration Options to Disable Optional Functionalities
|
||||||
|
|
||||||
|
**Status:** ✅ **COMPLETED**
|
||||||
|
**Author:** MrKalzu
|
||||||
|
**Created:** 2025-09-12
|
||||||
|
**Repository:** https://git.rauhala.info/ryyst/kalzu-value-store/issues/6
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Proposes adding configuration options to disable advanced features in the KVS (Key-Value Store) server to allow more flexible and lightweight deployment scenarios.
|
||||||
|
|
||||||
|
## Suggested Disablement Options
|
||||||
|
|
||||||
|
1. **Authentication System** - Disable JWT authentication entirely
|
||||||
|
2. **Tamper-Evident Logging** - Disable cryptographic audit trails
|
||||||
|
3. **Clustering** - Disable gossip protocol and distributed features
|
||||||
|
4. **Rate Limiting** - Disable per-client rate limiting
|
||||||
|
5. **Revision History** - Disable automatic versioning
|
||||||
|
|
||||||
|
## Proposed Implementation
|
||||||
|
|
||||||
|
- Add boolean flags to the Config struct for each feature
|
||||||
|
- Modify server initialization and request handling to respect these flags
|
||||||
|
- Allow conditional compilation/execution of features based on configuration
|
||||||
|
|
||||||
|
## Pros of Proposed Changes
|
||||||
|
|
||||||
|
- Reduce unnecessary overhead for simple deployments
|
||||||
|
- Simplify setup for different deployment needs
|
||||||
|
- Improve performance for specific use cases
|
||||||
|
- Lower resource consumption
|
||||||
|
|
||||||
|
## Cons of Proposed Changes
|
||||||
|
|
||||||
|
- Potential security risks if features are disabled inappropriately
|
||||||
|
- Loss of advanced functionality like audit trails or data recovery
|
||||||
|
- Increased complexity in codebase with conditional feature logic
|
||||||
|
|
||||||
|
## Already Implemented Features
|
||||||
|
|
||||||
|
- Backup System (configurable)
|
||||||
|
- Compression (configurable)
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
The issue suggests modifying relevant code sections to conditionally enable/disable these features based on configuration, similar to how backup and compression are currently handled.
|
||||||
1
main.go
1
main.go
@@ -11,7 +11,6 @@ import (
|
|||||||
"kvs/server"
|
"kvs/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
configPath := "./config.yaml"
|
configPath := "./config.yaml"
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import (
|
|||||||
"kvs/utils"
|
"kvs/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// healthHandler returns server health status
|
// healthHandler returns server health status
|
||||||
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
mode := s.getMode()
|
mode := s.getMode()
|
||||||
@@ -1271,3 +1269,29 @@ func (s *Server) getRevisionHistory(key string) ([]map[string]interface{}, error
|
|||||||
func (s *Server) getSpecificRevision(key string, revision int) (*types.StoredValue, error) {
|
func (s *Server) getSpecificRevision(key string, revision int) (*types.StoredValue, error) {
|
||||||
return s.revisionService.GetSpecificRevision(key, revision)
|
return s.revisionService.GetSpecificRevision(key, revision)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clusterBootstrapHandler provides the cluster secret to authenticated administrators (Issue #13)
|
||||||
|
func (s *Server) clusterBootstrapHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Ensure clustering is enabled
|
||||||
|
if !s.config.ClusteringEnabled {
|
||||||
|
http.Error(w, "Clustering is disabled", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure cluster secret is configured
|
||||||
|
if s.config.ClusterSecret == "" {
|
||||||
|
s.logger.Error("Cluster secret is not configured")
|
||||||
|
http.Error(w, "Cluster secret is not configured", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the cluster secret for secure bootstrap
|
||||||
|
response := map[string]string{
|
||||||
|
"cluster_secret": s.config.ClusterSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.WithField("remote_addr", r.RemoteAddr).Info("Cluster secret retrieved for bootstrap")
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|||||||
122
server/routes.go
122
server/routes.go
@@ -1,6 +1,8 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -8,46 +10,122 @@ import (
|
|||||||
func (s *Server) setupRoutes() *mux.Router {
|
func (s *Server) setupRoutes() *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
// Health endpoint
|
// Health endpoint (always available)
|
||||||
router.HandleFunc("/health", s.healthHandler).Methods("GET")
|
router.HandleFunc("/health", s.healthHandler).Methods("GET")
|
||||||
|
|
||||||
// KV endpoints
|
// KV endpoints (with conditional authentication based on anonymous access settings)
|
||||||
|
// GET endpoint - require auth if anonymous read is disabled
|
||||||
|
if s.config.AuthEnabled && !s.config.AllowAnonymousRead {
|
||||||
|
router.Handle("/kv/{path:.+}", s.authService.Middleware(
|
||||||
|
[]string{"read"}, nil, "",
|
||||||
|
)(s.getKVHandler)).Methods("GET")
|
||||||
|
} else {
|
||||||
router.HandleFunc("/kv/{path:.+}", s.getKVHandler).Methods("GET")
|
router.HandleFunc("/kv/{path:.+}", s.getKVHandler).Methods("GET")
|
||||||
router.HandleFunc("/kv/{path:.+}", s.putKVHandler).Methods("PUT")
|
}
|
||||||
router.HandleFunc("/kv/{path:.+}", s.deleteKVHandler).Methods("DELETE")
|
|
||||||
|
|
||||||
// Member endpoints
|
// 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)
|
||||||
|
if s.config.ClusteringEnabled {
|
||||||
|
// GET /members/ is unprotected for monitoring/inspection
|
||||||
router.HandleFunc("/members/", s.getMembersHandler).Methods("GET")
|
router.HandleFunc("/members/", s.getMembersHandler).Methods("GET")
|
||||||
|
|
||||||
|
// Apply cluster authentication middleware to all cluster communication endpoints
|
||||||
|
if s.clusterAuthService != nil {
|
||||||
|
router.Handle("/members/join", s.clusterAuthService.Middleware(http.HandlerFunc(s.joinMemberHandler))).Methods("POST")
|
||||||
|
router.Handle("/members/leave", s.clusterAuthService.Middleware(http.HandlerFunc(s.leaveMemberHandler))).Methods("DELETE")
|
||||||
|
router.Handle("/members/gossip", s.clusterAuthService.Middleware(http.HandlerFunc(s.gossipHandler))).Methods("POST")
|
||||||
|
router.Handle("/members/pairs_by_time", s.clusterAuthService.Middleware(http.HandlerFunc(s.pairsByTimeHandler))).Methods("POST")
|
||||||
|
|
||||||
|
// Merkle Tree endpoints (clustering feature)
|
||||||
|
router.Handle("/merkle_tree/root", s.clusterAuthService.Middleware(http.HandlerFunc(s.getMerkleRootHandler))).Methods("GET")
|
||||||
|
router.Handle("/merkle_tree/diff", s.clusterAuthService.Middleware(http.HandlerFunc(s.getMerkleDiffHandler))).Methods("POST")
|
||||||
|
router.Handle("/kv_range", s.clusterAuthService.Middleware(http.HandlerFunc(s.getKVRangeHandler))).Methods("POST")
|
||||||
|
} else {
|
||||||
|
// Fallback to unprotected endpoints (for backwards compatibility)
|
||||||
router.HandleFunc("/members/join", s.joinMemberHandler).Methods("POST")
|
router.HandleFunc("/members/join", s.joinMemberHandler).Methods("POST")
|
||||||
router.HandleFunc("/members/leave", s.leaveMemberHandler).Methods("DELETE")
|
router.HandleFunc("/members/leave", s.leaveMemberHandler).Methods("DELETE")
|
||||||
router.HandleFunc("/members/gossip", s.gossipHandler).Methods("POST")
|
router.HandleFunc("/members/gossip", s.gossipHandler).Methods("POST")
|
||||||
router.HandleFunc("/members/pairs_by_time", s.pairsByTimeHandler).Methods("POST") // Still available for clients
|
router.HandleFunc("/members/pairs_by_time", s.pairsByTimeHandler).Methods("POST")
|
||||||
|
|
||||||
// Merkle Tree endpoints
|
// Merkle Tree endpoints (clustering feature)
|
||||||
router.HandleFunc("/merkle_tree/root", s.getMerkleRootHandler).Methods("GET")
|
router.HandleFunc("/merkle_tree/root", s.getMerkleRootHandler).Methods("GET")
|
||||||
router.HandleFunc("/merkle_tree/diff", s.getMerkleDiffHandler).Methods("POST")
|
router.HandleFunc("/merkle_tree/diff", s.getMerkleDiffHandler).Methods("POST")
|
||||||
router.HandleFunc("/kv_range", s.getKVRangeHandler).Methods("POST") // New endpoint for fetching ranges
|
router.HandleFunc("/kv_range", s.getKVRangeHandler).Methods("POST")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// User Management endpoints
|
// Authentication and user management endpoints (available when auth is enabled)
|
||||||
router.HandleFunc("/api/users", s.createUserHandler).Methods("POST")
|
if s.config.AuthEnabled {
|
||||||
router.HandleFunc("/api/users/{uuid}", s.getUserHandler).Methods("GET")
|
// User Management endpoints (with authentication middleware)
|
||||||
router.HandleFunc("/api/users/{uuid}", s.updateUserHandler).Methods("PUT")
|
router.Handle("/api/users", s.authService.Middleware(
|
||||||
router.HandleFunc("/api/users/{uuid}", s.deleteUserHandler).Methods("DELETE")
|
[]string{"admin:users:create"}, nil, "",
|
||||||
|
)(s.createUserHandler)).Methods("POST")
|
||||||
|
|
||||||
// Group Management endpoints
|
router.Handle("/api/users/{uuid}", s.authService.Middleware(
|
||||||
router.HandleFunc("/api/groups", s.createGroupHandler).Methods("POST")
|
[]string{"admin:users:read"}, nil, "",
|
||||||
router.HandleFunc("/api/groups/{uuid}", s.getGroupHandler).Methods("GET")
|
)(s.getUserHandler)).Methods("GET")
|
||||||
router.HandleFunc("/api/groups/{uuid}", s.updateGroupHandler).Methods("PUT")
|
|
||||||
router.HandleFunc("/api/groups/{uuid}", s.deleteGroupHandler).Methods("DELETE")
|
|
||||||
|
|
||||||
// Token Management endpoints
|
router.Handle("/api/users/{uuid}", s.authService.Middleware(
|
||||||
router.HandleFunc("/api/tokens", s.createTokenHandler).Methods("POST")
|
[]string{"admin:users:update"}, nil, "",
|
||||||
|
)(s.updateUserHandler)).Methods("PUT")
|
||||||
|
|
||||||
// Revision History endpoints
|
router.Handle("/api/users/{uuid}", s.authService.Middleware(
|
||||||
|
[]string{"admin:users:delete"}, nil, "",
|
||||||
|
)(s.deleteUserHandler)).Methods("DELETE")
|
||||||
|
|
||||||
|
// Group Management endpoints (with authentication middleware)
|
||||||
|
router.Handle("/api/groups", s.authService.Middleware(
|
||||||
|
[]string{"admin:groups:create"}, nil, "",
|
||||||
|
)(s.createGroupHandler)).Methods("POST")
|
||||||
|
|
||||||
|
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 (with authentication middleware)
|
||||||
|
router.Handle("/api/tokens", s.authService.Middleware(
|
||||||
|
[]string{"admin:tokens:create"}, nil, "",
|
||||||
|
)(s.createTokenHandler)).Methods("POST")
|
||||||
|
|
||||||
|
// Cluster Bootstrap endpoint (Issue #13) - Protected by JWT authentication
|
||||||
|
// Allows authenticated administrators to retrieve the cluster secret for new nodes
|
||||||
|
router.Handle("/auth/cluster-bootstrap", s.authService.Middleware(
|
||||||
|
[]string{"admin:tokens:create"}, nil, "",
|
||||||
|
)(s.clusterBootstrapHandler)).Methods("GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revision History endpoints (available when revision history is enabled)
|
||||||
|
if s.config.RevisionHistoryEnabled {
|
||||||
router.HandleFunc("/api/data/{key}/history", s.getRevisionHistoryHandler).Methods("GET")
|
router.HandleFunc("/api/data/{key}/history", s.getRevisionHistoryHandler).Methods("GET")
|
||||||
router.HandleFunc("/api/data/{key}/history/{revision}", s.getSpecificRevisionHandler).Methods("GET")
|
router.HandleFunc("/api/data/{key}/history/{revision}", s.getSpecificRevisionHandler).Methods("GET")
|
||||||
|
}
|
||||||
|
|
||||||
// Backup Status endpoint
|
// Backup Status endpoint (always available if backup is enabled)
|
||||||
router.HandleFunc("/api/backup/status", s.getBackupStatusHandler).Methods("GET")
|
router.HandleFunc("/api/backup/status", s.getBackupStatusHandler).Methods("GET")
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|||||||
153
server/server.go
153
server/server.go
@@ -2,10 +2,12 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ import (
|
|||||||
"kvs/cluster"
|
"kvs/cluster"
|
||||||
"kvs/storage"
|
"kvs/storage"
|
||||||
"kvs/types"
|
"kvs/types"
|
||||||
|
"kvs/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server represents the KVS node
|
// Server represents the KVS node
|
||||||
@@ -48,6 +51,7 @@ type Server struct {
|
|||||||
|
|
||||||
// Authentication service
|
// Authentication service
|
||||||
authService *auth.AuthService
|
authService *auth.AuthService
|
||||||
|
clusterAuthService *auth.ClusterAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer initializes and returns a new Server instance
|
// NewServer initializes and returns a new Server instance
|
||||||
@@ -115,7 +119,19 @@ 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)
|
||||||
|
|
||||||
|
// Initialize cluster authentication service (Issue #13)
|
||||||
|
if config.ClusteringEnabled {
|
||||||
|
server.clusterAuthService = auth.NewClusterAuthService(config.ClusterSecret, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup initial root account if needed (Issue #3)
|
||||||
|
if config.AuthEnabled {
|
||||||
|
if err := server.setupRootAccount(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to setup root account: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize Merkle tree using cluster service
|
// Initialize Merkle tree using cluster service
|
||||||
if err := server.syncService.InitializeMerkleTree(); err != nil {
|
if err := server.syncService.InitializeMerkleTree(); err != nil {
|
||||||
@@ -182,3 +198,138 @@ func (s *Server) getBackupStatus() types.BackupStatus {
|
|||||||
|
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupRootAccount creates an initial root account if no users exist and no seed nodes are configured
|
||||||
|
func (s *Server) setupRootAccount() error {
|
||||||
|
// Only create root account if:
|
||||||
|
// 1. No users exist in the database
|
||||||
|
// 2. No seed nodes are configured (standalone mode)
|
||||||
|
hasUsers, err := s.authService.HasUsers()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check if users exist: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If users already exist or we have seed nodes, no need to create root account
|
||||||
|
if hasUsers || len(s.config.SeedNodes) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Creating initial root account for empty database with no seed nodes")
|
||||||
|
|
||||||
|
// Import required packages for user creation
|
||||||
|
// Note: We need these imports at the top of the file
|
||||||
|
return s.createRootUserAndToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
// createRootUserAndToken creates the root user, admin group, and initial token
|
||||||
|
func (s *Server) createRootUserAndToken() error {
|
||||||
|
rootNickname := "root"
|
||||||
|
adminGroupName := "admin"
|
||||||
|
|
||||||
|
// Generate UUIDs
|
||||||
|
rootUserUUID := "root-" + time.Now().Format("20060102-150405")
|
||||||
|
adminGroupUUID := "admin-" + time.Now().Format("20060102-150405")
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
// Create admin group
|
||||||
|
adminGroup := types.Group{
|
||||||
|
UUID: adminGroupUUID,
|
||||||
|
NameHash: hashGroupName(adminGroupName),
|
||||||
|
Members: []string{rootUserUUID},
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create root user
|
||||||
|
rootUser := types.User{
|
||||||
|
UUID: rootUserUUID,
|
||||||
|
NicknameHash: hashUserNickname(rootNickname),
|
||||||
|
Groups: []string{adminGroupUUID},
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store group and user in database
|
||||||
|
if err := s.storeUserAndGroup(&rootUser, &adminGroup); err != nil {
|
||||||
|
return fmt.Errorf("failed to store root user and admin group: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create API token with full administrative scopes
|
||||||
|
adminScopes := []string{
|
||||||
|
"admin:users:create", "admin:users:read", "admin:users:update", "admin:users:delete",
|
||||||
|
"admin:groups:create", "admin:groups:read", "admin:groups:update", "admin:groups:delete",
|
||||||
|
"admin:tokens:create", "admin:tokens:revoke",
|
||||||
|
"read", "write", "delete",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate token with 24 hour expiration for initial setup
|
||||||
|
tokenString, expiresAt, err := auth.GenerateJWT(rootUserUUID, adminScopes, 24)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate root token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store token in database
|
||||||
|
if err := s.storeAPIToken(tokenString, rootUserUUID, adminScopes, expiresAt); err != nil {
|
||||||
|
return fmt.Errorf("failed to store root token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the token securely (one-time display)
|
||||||
|
s.logger.WithFields(logrus.Fields{
|
||||||
|
"user_uuid": rootUserUUID,
|
||||||
|
"group_uuid": adminGroupUUID,
|
||||||
|
"expires_at": time.Unix(expiresAt, 0).Format(time.RFC3339),
|
||||||
|
"expires_in": "24 hours",
|
||||||
|
}).Warn("Root account created - SAVE THIS TOKEN:")
|
||||||
|
|
||||||
|
// Display token prominently
|
||||||
|
fmt.Printf("\n" + strings.Repeat("=", 80) + "\n")
|
||||||
|
fmt.Printf("🔐 ROOT ACCOUNT CREATED - INITIAL SETUP TOKEN\n")
|
||||||
|
fmt.Printf("===========================================\n")
|
||||||
|
fmt.Printf("User UUID: %s\n", rootUserUUID)
|
||||||
|
fmt.Printf("Group UUID: %s\n", adminGroupUUID)
|
||||||
|
fmt.Printf("Token: %s\n", tokenString)
|
||||||
|
fmt.Printf("Expires: %s (24 hours)\n", time.Unix(expiresAt, 0).Format(time.RFC3339))
|
||||||
|
fmt.Printf("\n⚠️ IMPORTANT: Save this token immediately!\n")
|
||||||
|
fmt.Printf(" This is the only time it will be displayed.\n")
|
||||||
|
fmt.Printf(" Use this token to authenticate and create additional users.\n")
|
||||||
|
fmt.Printf(strings.Repeat("=", 80) + "\n\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashUserNickname creates a hash of the user nickname (similar to handlers.go)
|
||||||
|
func hashUserNickname(nickname string) string {
|
||||||
|
return utils.HashSHA3512(nickname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashGroupName creates a hash of the group name (similar to handlers.go)
|
||||||
|
func hashGroupName(groupname string) string {
|
||||||
|
return utils.HashSHA3512(groupname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// storeUserAndGroup stores both user and group in the database
|
||||||
|
func (s *Server) storeUserAndGroup(user *types.User, group *types.Group) error {
|
||||||
|
return s.db.Update(func(txn *badger.Txn) error {
|
||||||
|
// Store user
|
||||||
|
userData, err := json.Marshal(user)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal user data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := txn.Set([]byte(auth.UserStorageKey(user.UUID)), userData); err != nil {
|
||||||
|
return fmt.Errorf("failed to store user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store group
|
||||||
|
groupData, err := json.Marshal(group)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal group data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := txn.Set([]byte(auth.GroupStorageKey(group.UUID)), groupData); err != nil {
|
||||||
|
return fmt.Errorf("failed to store group: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -273,4 +273,15 @@ 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
|
||||||
|
|
||||||
|
// Cluster authentication (Issue #13)
|
||||||
|
ClusterSecret string `yaml:"cluster_secret"` // Shared secret for cluster authentication (auto-generated if empty)
|
||||||
|
ClusterTLSEnabled bool `yaml:"cluster_tls_enabled"` // Require TLS for inter-node communication
|
||||||
|
ClusterTLSCertFile string `yaml:"cluster_tls_cert_file"` // Path to TLS certificate file
|
||||||
|
ClusterTLSKeyFile string `yaml:"cluster_tls_key_file"` // Path to TLS private key file
|
||||||
|
ClusterTLSSkipVerify bool `yaml:"cluster_tls_skip_verify"` // Skip TLS verification (insecure, for testing only)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user