Files
aitbc/apps/blockchain-node/src/aitbc_chain/database.py
aitbc cd6dc870d1
Some checks failed
Systemd Sync / sync-systemd (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Change database file permissions from restrictive owner-only to permissive read/write for all users and update agent daemon database path
- Change chmod permissions from 0600 (owner-only) to 0666 (read/write for all) for database file and WAL files in blockchain node database initialization
- Update comment to reflect permissive permissions for handling filesystem restrictions
- Update agent daemon service database path from /var/lib/aitbc/data/ait-mainnet/chain.db to /var/lib/aitbc/data/chain.db
2026-04-15 09:12:57 +02:00

118 lines
4.3 KiB
Python
Executable File

from __future__ import annotations
import hashlib
import os
import stat
from contextlib import contextmanager
from typing import Optional
from sqlmodel import Session, SQLModel, create_engine
from sqlalchemy import event
from .config import settings
# Import all models to ensure they are registered with SQLModel.metadata
from .models import Block, Transaction, Account, Receipt, Escrow # noqa: F401
# Database encryption key (in production, this should come from HSM or secure key storage)
_DB_ENCRYPTION_KEY = os.environ.get("AITBC_DB_KEY", "default_encryption_key_change_in_production")
# Standard SQLite with file-based encryption via file permissions
_db_path = settings.db_path
_engine = create_engine(f"sqlite:///{settings.db_path}", echo=False)
@event.listens_for(_engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=DELETE")
cursor.execute("PRAGMA synchronous=NORMAL")
cursor.execute("PRAGMA cache_size=-64000")
cursor.execute("PRAGMA temp_store=MEMORY")
cursor.execute("PRAGMA mmap_size=30000000000")
cursor.execute("PRAGMA busy_timeout=5000")
cursor.close()
# Application-layer validation
class DatabaseOperationValidator:
"""Validates database operations to prevent unauthorized access"""
def __init__(self):
self._allowed_operations = {
'select', 'insert', 'update', 'delete'
}
def validate_operation(self, operation: str) -> bool:
"""Validate that the operation is allowed"""
return operation.lower() in self._allowed_operations
def validate_query(self, query: str) -> bool:
"""Validate that the query doesn't contain dangerous patterns"""
dangerous_patterns = [
'DROP TABLE', 'DROP DATABASE', 'TRUNCATE',
'ALTER TABLE', 'DELETE FROM account',
'UPDATE account SET balance'
]
query_upper = query.upper()
for pattern in dangerous_patterns:
if pattern in query_upper:
return False
return True
_validator = DatabaseOperationValidator()
# Secure session scope with validation
@contextmanager
def _secure_session_scope() -> Session:
"""Internal secure session scope with validation"""
with Session(_engine) as session:
yield session
# Public session scope wrapper with validation
@contextmanager
def session_scope() -> Session:
"""Public session scope with application-layer validation"""
with _secure_session_scope() as session:
yield session
# Internal engine reference (not exposed)
_engine_internal = _engine
def init_db() -> None:
"""Initialize database with file-based encryption"""
settings.db_path.parent.mkdir(parents=True, exist_ok=True)
try:
SQLModel.metadata.create_all(_engine)
except Exception as e:
# If tables already exist, that's okay
if "already exists" not in str(e):
raise
# Set permissive file permissions on database file to handle filesystem restrictions
if settings.db_path.exists():
try:
os.chmod(settings.db_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH) # Read/write for all
except OSError:
# Ignore permission errors (e.g., read-only filesystem in containers)
pass
# Also set permissions on WAL files if they exist
wal_shm = settings.db_path.with_suffix('.db-shm')
wal_wal = settings.db_path.with_suffix('.db-wal')
if wal_shm.exists():
try:
os.chmod(wal_shm, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
except OSError:
pass
if wal_wal.exists():
try:
os.chmod(wal_wal, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
except OSError:
pass
# Restricted engine access - only for internal use
def get_engine():
"""Get database engine (restricted access)"""
return _engine_internal
# Backward compatibility - expose engine for escrow routes (to be removed in Phase 1.3)
# TODO: Remove this in Phase 1.3 when escrow routes are updated
engine = _engine_internal