Compare commits
	
		
			1 Commits
		
	
	
		
			secure-clu
			...
			metadata-a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 377af163f0 | 
							
								
								
									
										34
									
								
								auth/auth.go
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								auth/auth.go
									
									
									
									
									
								
							@@ -198,6 +198,40 @@ func (s *AuthService) CheckResourcePermission(authCtx *AuthContext, resourceKey
 | 
			
		||||
	return CheckPermission(metadata.Permissions, operation, isOwner, isGroupMember)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetResourceMetadata retrieves metadata for a resource
 | 
			
		||||
func (s *AuthService) GetResourceMetadata(resourceKey string) (*types.ResourceMetadata, error) {
 | 
			
		||||
	var metadata types.ResourceMetadata
 | 
			
		||||
 | 
			
		||||
	err := s.db.View(func(txn *badger.Txn) error {
 | 
			
		||||
		item, err := txn.Get([]byte(ResourceMetadataKey(resourceKey)))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return item.Value(func(val []byte) error {
 | 
			
		||||
			return json.Unmarshal(val, &metadata)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &metadata, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetResourceMetadata stores metadata for a resource
 | 
			
		||||
func (s *AuthService) SetResourceMetadata(resourceKey string, metadata *types.ResourceMetadata) error {
 | 
			
		||||
	metadataBytes, err := json.Marshal(metadata)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to marshal metadata: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s.db.Update(func(txn *badger.Txn) error {
 | 
			
		||||
		return txn.Set([]byte(ResourceMetadataKey(resourceKey)), metadataBytes)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAuthContext retrieves auth context from request context
 | 
			
		||||
func GetAuthContext(ctx context.Context) *AuthContext {
 | 
			
		||||
	if authCtx, ok := ctx.Value("auth").(*AuthContext); ok {
 | 
			
		||||
 
 | 
			
		||||
@@ -444,6 +444,95 @@ EOF
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Test 6: Resource Metadata Management (Issue #12)
 | 
			
		||||
test_metadata_management() {
 | 
			
		||||
    test_start "Resource Metadata Management test (Issue #12)"
 | 
			
		||||
 | 
			
		||||
    # Create metadata test config
 | 
			
		||||
    cat > metadata_test.yaml <<EOF
 | 
			
		||||
node_id: "metadata-test"
 | 
			
		||||
bind_address: "127.0.0.1"
 | 
			
		||||
port: 8096
 | 
			
		||||
data_dir: "./metadata_test_data"
 | 
			
		||||
seed_nodes: []
 | 
			
		||||
log_level: "error"
 | 
			
		||||
auth_enabled: true
 | 
			
		||||
allow_anonymous_read: false
 | 
			
		||||
allow_anonymous_write: false
 | 
			
		||||
EOF
 | 
			
		||||
 | 
			
		||||
    # Start node
 | 
			
		||||
    $BINARY metadata_test.yaml >metadata_test.log 2>&1 &
 | 
			
		||||
    local pid=$!
 | 
			
		||||
 | 
			
		||||
    if wait_for_service 8096; then
 | 
			
		||||
        sleep 2  # Allow root account creation
 | 
			
		||||
 | 
			
		||||
        # Extract the token from logs
 | 
			
		||||
        local token=$(grep "Token:" metadata_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
 | 
			
		||||
 | 
			
		||||
        # First, create a KV resource
 | 
			
		||||
        curl -s -X PUT http://localhost:8096/kv/test/resource -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"data":"test"}' >/dev/null
 | 
			
		||||
        sleep 1
 | 
			
		||||
 | 
			
		||||
        # Test 1: Get metadata should fail for non-existent metadata (initially no metadata exists)
 | 
			
		||||
        local get_response=$(curl -s -w "\n%{http_code}" -X GET http://localhost:8096/kv/test/resource/metadata -H "Authorization: Bearer $token")
 | 
			
		||||
        local get_body=$(echo "$get_response" | head -n -1)
 | 
			
		||||
        local get_code=$(echo "$get_response" | tail -n 1)
 | 
			
		||||
 | 
			
		||||
        if [ "$get_code" = "404" ]; then
 | 
			
		||||
            log_success "GET metadata returns 404 for non-existent metadata"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "GET metadata should return 404 for non-existent metadata, got code: $get_code, body: $get_body"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Test 2: Update metadata should create new metadata
 | 
			
		||||
        local update_response=$(curl -s -X PUT http://localhost:8096/kv/test/resource/metadata -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"owner_uuid":"test-owner-123","permissions":3840}')
 | 
			
		||||
        if echo "$update_response" | grep -q "owner_uuid"; then
 | 
			
		||||
            log_success "PUT metadata creates metadata successfully"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "PUT metadata should create metadata, got: $update_response"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Test 3: Get metadata should now return the created metadata
 | 
			
		||||
        local get_response2=$(curl -s -X GET http://localhost:8096/kv/test/resource/metadata -H "Authorization: Bearer $token")
 | 
			
		||||
        if echo "$get_response2" | grep -q "test-owner-123" && echo "$get_response2" | grep -q "3840"; then
 | 
			
		||||
            log_success "GET metadata returns created metadata"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "GET metadata should return created metadata, got: $get_response2"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Test 4: Update metadata should modify existing metadata
 | 
			
		||||
        local update_response2=$(curl -s -X PUT http://localhost:8096/kv/test/resource/metadata -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"owner_uuid":"new-owner-456"}')
 | 
			
		||||
        if echo "$update_response2" | grep -q "new-owner-456"; then
 | 
			
		||||
            log_success "PUT metadata updates existing metadata"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "PUT metadata should update metadata, got: $update_response2"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Test 5: Metadata endpoints should require authentication
 | 
			
		||||
        local no_auth=$(curl -s -w "\n%{http_code}" -X GET http://localhost:8096/kv/test/resource/metadata)
 | 
			
		||||
        local no_auth_code=$(echo "$no_auth" | tail -n 1)
 | 
			
		||||
        if [ "$no_auth_code" = "401" ]; then
 | 
			
		||||
            log_success "Metadata endpoints properly require authentication"
 | 
			
		||||
        else
 | 
			
		||||
            log_error "Metadata endpoints should require authentication, got code: $no_auth_code"
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        kill $pid 2>/dev/null || true
 | 
			
		||||
        sleep 2
 | 
			
		||||
    else
 | 
			
		||||
        log_error "Metadata test node failed to start"
 | 
			
		||||
        kill $pid 2>/dev/null || true
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Main test execution
 | 
			
		||||
main() {
 | 
			
		||||
    echo "=================================================="
 | 
			
		||||
@@ -462,7 +551,8 @@ main() {
 | 
			
		||||
    test_cluster_formation
 | 
			
		||||
    test_conflict_resolution
 | 
			
		||||
    test_authentication_middleware
 | 
			
		||||
    
 | 
			
		||||
    test_metadata_management
 | 
			
		||||
 | 
			
		||||
    # Results
 | 
			
		||||
    echo "=================================================="
 | 
			
		||||
    echo "              Test Results"
 | 
			
		||||
 
 | 
			
		||||
@@ -213,6 +213,104 @@ func (s *Server) deleteKVHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	s.logger.WithField("path", path).Info("Value deleted")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getResourceMetadataHandler retrieves metadata for a KV resource
 | 
			
		||||
func (s *Server) getResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	path := vars["path"]
 | 
			
		||||
 | 
			
		||||
	// Get metadata from storage
 | 
			
		||||
	metadata, err := s.authService.GetResourceMetadata(path)
 | 
			
		||||
	if err == badger.ErrKeyNotFound {
 | 
			
		||||
		http.Error(w, "Not Found: No metadata exists for this resource", http.StatusNotFound)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to get resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := types.GetResourceMetadataResponse{
 | 
			
		||||
		OwnerUUID:   metadata.OwnerUUID,
 | 
			
		||||
		GroupUUID:   metadata.GroupUUID,
 | 
			
		||||
		Permissions: metadata.Permissions,
 | 
			
		||||
		TTL:         metadata.TTL,
 | 
			
		||||
		CreatedAt:   metadata.CreatedAt,
 | 
			
		||||
		UpdatedAt:   metadata.UpdatedAt,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	json.NewEncoder(w).Encode(response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// updateResourceMetadataHandler updates metadata for a KV resource
 | 
			
		||||
func (s *Server) updateResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	path := vars["path"]
 | 
			
		||||
 | 
			
		||||
	// Parse request body
 | 
			
		||||
	var req types.UpdateResourceMetadataRequest
 | 
			
		||||
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 | 
			
		||||
		http.Error(w, "Bad Request: Invalid JSON", http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get existing metadata or create new one
 | 
			
		||||
	metadata, err := s.authService.GetResourceMetadata(path)
 | 
			
		||||
	if err == badger.ErrKeyNotFound {
 | 
			
		||||
		// Create new metadata with defaults
 | 
			
		||||
		metadata = &types.ResourceMetadata{
 | 
			
		||||
			OwnerUUID:   "",
 | 
			
		||||
			GroupUUID:   "",
 | 
			
		||||
			Permissions: types.DefaultPermissions,
 | 
			
		||||
			TTL:         "",
 | 
			
		||||
			CreatedAt:   time.Now().Unix(),
 | 
			
		||||
			UpdatedAt:   time.Now().Unix(),
 | 
			
		||||
		}
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to get resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update only provided fields
 | 
			
		||||
	if req.OwnerUUID != nil {
 | 
			
		||||
		metadata.OwnerUUID = *req.OwnerUUID
 | 
			
		||||
	}
 | 
			
		||||
	if req.GroupUUID != nil {
 | 
			
		||||
		metadata.GroupUUID = *req.GroupUUID
 | 
			
		||||
	}
 | 
			
		||||
	if req.Permissions != nil {
 | 
			
		||||
		metadata.Permissions = *req.Permissions
 | 
			
		||||
	}
 | 
			
		||||
	metadata.UpdatedAt = time.Now().Unix()
 | 
			
		||||
 | 
			
		||||
	// Store updated metadata
 | 
			
		||||
	if err := s.authService.SetResourceMetadata(path, metadata); err != nil {
 | 
			
		||||
		s.logger.WithError(err).WithField("path", path).Error("Failed to update resource metadata")
 | 
			
		||||
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := types.GetResourceMetadataResponse{
 | 
			
		||||
		OwnerUUID:   metadata.OwnerUUID,
 | 
			
		||||
		GroupUUID:   metadata.GroupUUID,
 | 
			
		||||
		Permissions: metadata.Permissions,
 | 
			
		||||
		TTL:         metadata.TTL,
 | 
			
		||||
		CreatedAt:   metadata.CreatedAt,
 | 
			
		||||
		UpdatedAt:   metadata.UpdatedAt,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	json.NewEncoder(w).Encode(response)
 | 
			
		||||
 | 
			
		||||
	s.logger.WithFields(logrus.Fields{
 | 
			
		||||
		"path":       path,
 | 
			
		||||
		"owner_uuid": metadata.OwnerUUID,
 | 
			
		||||
		"group_uuid": metadata.GroupUUID,
 | 
			
		||||
	}).Info("Resource metadata updated")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// isClusterMember checks if request is from a cluster member
 | 
			
		||||
func (s *Server) isClusterMember(remoteAddr string) bool {
 | 
			
		||||
	host, _, err := net.SplitHostPort(remoteAddr)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,18 @@ func (s *Server) setupRoutes() *mux.Router {
 | 
			
		||||
	// Health endpoint (always available)
 | 
			
		||||
	router.HandleFunc("/health", s.healthHandler).Methods("GET")
 | 
			
		||||
 | 
			
		||||
	// Resource Metadata Management endpoints (Issue #12) - Must come BEFORE general KV routes
 | 
			
		||||
	// These need to be registered first to prevent /kv/{path:.+} from matching metadata paths
 | 
			
		||||
	if s.config.AuthEnabled {
 | 
			
		||||
		router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
 | 
			
		||||
			[]string{"admin:users:read"}, nil, "",
 | 
			
		||||
		)(s.getResourceMetadataHandler)).Methods("GET")
 | 
			
		||||
 | 
			
		||||
		router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
 | 
			
		||||
			[]string{"admin:users:update"}, nil, "",
 | 
			
		||||
		)(s.updateResourceMetadataHandler)).Methods("PUT")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -131,6 +131,22 @@ type CreateTokenResponse struct {
 | 
			
		||||
	ExpiresAt int64  `json:"expires_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Resource Metadata Management API structures (Issue #12)
 | 
			
		||||
type GetResourceMetadataResponse struct {
 | 
			
		||||
	OwnerUUID   string `json:"owner_uuid"`
 | 
			
		||||
	GroupUUID   string `json:"group_uuid"`
 | 
			
		||||
	Permissions int    `json:"permissions"`
 | 
			
		||||
	TTL         string `json:"ttl"`
 | 
			
		||||
	CreatedAt   int64  `json:"created_at"`
 | 
			
		||||
	UpdatedAt   int64  `json:"updated_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UpdateResourceMetadataRequest struct {
 | 
			
		||||
	OwnerUUID   *string `json:"owner_uuid,omitempty"`
 | 
			
		||||
	GroupUUID   *string `json:"group_uuid,omitempty"`
 | 
			
		||||
	Permissions *int    `json:"permissions,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Cluster and member management types
 | 
			
		||||
type Member struct {
 | 
			
		||||
	ID              string `json:"id"`
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user