319 lines
No EOL
12 KiB
Python
319 lines
No EOL
12 KiB
Python
from security.encrypt import encrypt_data, decrypt_data, generate_key
|
|
import sqlite3
|
|
import threading
|
|
import hashlib
|
|
import logging
|
|
import base64
|
|
from typing import Optional, Dict, Any, Union
|
|
from security.encrypt import encrypt_data, decrypt_data
|
|
from security.rbac_engine import RBACEngine
|
|
|
|
class AccessDenied(Exception):
|
|
"""Raised when RBAC validation fails"""
|
|
|
|
class NotFound(Exception):
|
|
"""Raised when requested key doesn't exist"""
|
|
|
|
class EncryptionError(Exception):
|
|
"""Raised when encryption operation fails"""
|
|
|
|
class SQLiteAdapter:
|
|
"""SQLite storage adapter with RBAC and encryption support.
|
|
Integrates with TaskDispatcher for secure storage operations.
|
|
|
|
Attributes:
|
|
db_path: Path to SQLite database file
|
|
encryption_key: Key used for data encryption
|
|
rbac: RBAC engine instance (must be set before use)
|
|
_lock: Thread lock for concurrent access
|
|
logger: Logger instance for operations
|
|
"""
|
|
|
|
rbac: RBACEngine = None # Must be set by application
|
|
logger = logging.getLogger('SQLiteAdapter')
|
|
|
|
def __init__(self, db_path: str, encryption_key: str):
|
|
"""Initialize SQLite adapter with dispatcher integration.
|
|
|
|
Args:
|
|
db_path: Path to SQLite database file
|
|
encryption_key: Encryption key for data protection (32+ chars or 32 bytes)
|
|
|
|
Raises:
|
|
RuntimeError: If encryption key is invalid
|
|
"""
|
|
if not encryption_key or (isinstance(encryption_key, str) and len(encryption_key) < 32):
|
|
raise RuntimeError("Encryption key must be at least 32 characters or 32 bytes")
|
|
|
|
self.db_path = db_path
|
|
self.encryption_key = self._convert_key(encryption_key)
|
|
self._lock = threading.Lock()
|
|
self._init_db()
|
|
self.logger.info(f"Initialized SQLite adapter for {db_path}")
|
|
|
|
def _init_db(self):
|
|
"""Initialize database tables."""
|
|
with self._lock, sqlite3.connect(self.db_path) as conn:
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS storage (
|
|
key_hash TEXT PRIMARY KEY,
|
|
encrypted_value BLOB,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
created_by TEXT
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS access_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key_hash TEXT,
|
|
operation TEXT,
|
|
user_id TEXT,
|
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY(key_hash) REFERENCES storage(key_hash)
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS performance_metrics (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
operation TEXT NOT NULL,
|
|
execution_time_ms INTEGER NOT NULL,
|
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
user_id TEXT,
|
|
key_hash TEXT,
|
|
FOREIGN KEY(key_hash) REFERENCES storage(key_hash)
|
|
)
|
|
""")
|
|
|
|
def _hash_key(self, key):
|
|
"""Generate SHA-256 hash of key."""
|
|
return hashlib.sha256(key.encode()).hexdigest()
|
|
|
|
def _convert_key(self, key: Union[str, bytes]) -> bytes:
|
|
"""Convert encryption key to bytes format required by AES-256-GCM.
|
|
Supports:
|
|
- 32-byte raw keys
|
|
- 44-byte base64 encoded keys
|
|
- String keys (hashed to 32 bytes)
|
|
|
|
Args:
|
|
key: Original key (str or bytes)
|
|
|
|
Returns:
|
|
bytes: 32-byte key
|
|
|
|
Raises:
|
|
RuntimeError: If key cannot be converted to 32 bytes
|
|
"""
|
|
if isinstance(key, bytes):
|
|
if len(key) == 32:
|
|
return key
|
|
if len(key) == 44: # Base64 encoded
|
|
try:
|
|
decoded = base64.urlsafe_b64decode(key)
|
|
if len(decoded) == 32:
|
|
return decoded
|
|
raise ValueError("Decoded key must be 32 bytes")
|
|
except Exception as e:
|
|
raise RuntimeError(f"Invalid base64 key: {str(e)}")
|
|
raise RuntimeError("Bytes key must be 32 raw bytes or 44 base64 bytes")
|
|
|
|
# Convert string key to bytes using SHA-256 for consistent length
|
|
return hashlib.sha256(key.encode()).digest()[:32]
|
|
|
|
def create(self, key: str, value: Any, user_id: str, client_cert_info: Optional[Dict] = None) -> bool:
|
|
"""Create new storage entry with RBAC check.
|
|
Integrates with dispatcher's RBAC system.
|
|
|
|
Args:
|
|
key: Storage key
|
|
value: Value to store (will be encrypted)
|
|
user_id: User ID for RBAC validation
|
|
client_cert_info: Optional client certificate info for enhanced auth
|
|
|
|
Returns:
|
|
bool: True if successful, False if unauthorized
|
|
|
|
Raises:
|
|
ValueError: If key or value is invalid
|
|
"""
|
|
if not key or not isinstance(key, str):
|
|
raise ValueError("Key must be non-empty string")
|
|
|
|
if not self.rbac or not self.rbac.validate_permission(user_id, "storage", "create"):
|
|
self.logger.warning(f"Unauthorized create attempt by {user_id}")
|
|
return False
|
|
|
|
key_hash = self._hash_key(key)
|
|
try:
|
|
encrypted_value = encrypt_data(value, self.encryption_key)
|
|
except Exception as e:
|
|
self.logger.error(f"Encryption failed: {str(e)}")
|
|
raise RuntimeError("Data encryption failed") from e
|
|
|
|
with self._lock, sqlite3.connect(self.db_path) as conn:
|
|
try:
|
|
conn.execute(
|
|
"INSERT INTO storage (key_hash, encrypted_value, created_by) VALUES (?, ?, ?)",
|
|
(key_hash, encrypted_value, user_id)
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO access_log (key_hash, operation, user_id) VALUES (?, ?, ?)",
|
|
(key_hash, "create", user_id)
|
|
)
|
|
return True
|
|
except sqlite3.IntegrityError:
|
|
return False
|
|
|
|
def read(self, key: str, user_id: str) -> Optional[Any]:
|
|
"""Read storage entry with RBAC check.
|
|
Integrates with dispatcher's RBAC system.
|
|
|
|
Args:
|
|
key: Storage key
|
|
user_id: User ID for RBAC validation
|
|
|
|
Returns:
|
|
Decrypted value or None if unauthorized/not found
|
|
|
|
Raises:
|
|
ValueError: If key is invalid
|
|
RuntimeError: If decryption fails
|
|
"""
|
|
if not key or not isinstance(key, str):
|
|
raise ValueError("Key must be non-empty string")
|
|
|
|
if not self.rbac or not self.rbac.validate_permission(user_id, "storage", "read"):
|
|
self.logger.warning(f"Unauthorized read attempt by {user_id}")
|
|
return None
|
|
|
|
key_hash = self._hash_key(key)
|
|
|
|
with self._lock, sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.execute(
|
|
"SELECT encrypted_value FROM storage WHERE key_hash = ?",
|
|
(key_hash,))
|
|
row = cursor.fetchone()
|
|
|
|
if row:
|
|
conn.execute(
|
|
"INSERT INTO access_log (key_hash, operation, user_id) VALUES (?, ?, ?)",
|
|
(key_hash, "read", user_id)
|
|
)
|
|
try:
|
|
return decrypt_data(row[0], self.encryption_key)
|
|
except Exception as e:
|
|
self.logger.error(f"Decryption failed: {str(e)}")
|
|
raise RuntimeError("Data decryption failed") from e
|
|
return None
|
|
|
|
def delete(self, key: str, user_id: str) -> bool:
|
|
"""Delete storage entry with RBAC check.
|
|
Integrates with dispatcher's RBAC system.
|
|
|
|
Args:
|
|
key: Storage key
|
|
user_id: User ID for RBAC validation
|
|
|
|
Returns:
|
|
bool: True if successful, False if unauthorized or not found
|
|
|
|
Raises:
|
|
ValueError: If key is invalid
|
|
"""
|
|
if not key or not isinstance(key, str):
|
|
raise ValueError("Key must be non-empty string")
|
|
|
|
if not self.rbac or not self.rbac.validate_permission(user_id, "storage", "delete"):
|
|
self.logger.warning(f"Unauthorized delete attempt by {user_id}")
|
|
return False
|
|
|
|
key_hash = self._hash_key(key)
|
|
|
|
with self._lock, sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.execute(
|
|
"DELETE FROM storage WHERE key_hash = ?",
|
|
(key_hash,))
|
|
|
|
if cursor.rowcount > 0:
|
|
conn.execute(
|
|
"INSERT INTO access_log (key_hash, operation, user_id) VALUES (?, ?, ?)",
|
|
(key_hash, "delete", user_id)
|
|
)
|
|
return True
|
|
return False
|
|
|
|
def close(self):
|
|
"""Close database connections."""
|
|
pass # SQLite connections are closed automatically
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
self.close()
|
|
|
|
def update(self, key: str, value: bytes, user_id: str) -> None:
|
|
"""Update storage entry with RBAC check.
|
|
Integrates with dispatcher's RBAC system.
|
|
|
|
Args:
|
|
key: Storage key (non-empty string)
|
|
value: New value to store as bytes (will be encrypted)
|
|
user_id: User ID for RBAC validation
|
|
|
|
Returns:
|
|
None
|
|
|
|
Raises:
|
|
ValueError: If key is invalid
|
|
NotFound: If key doesn't exist
|
|
AccessDenied: If RBAC validation fails
|
|
EncryptionError: If encryption fails
|
|
"""
|
|
if not key or not isinstance(key, str):
|
|
raise ValueError("Key must be non-empty string")
|
|
|
|
if not self.rbac or not self.rbac.validate_permission(user_id, "storage", "update"):
|
|
self.logger.warning(f"Unauthorized update attempt by {user_id}")
|
|
raise AccessDenied("Update permission denied")
|
|
|
|
key_hash = self._hash_key(key)
|
|
try:
|
|
encrypted_value = encrypt_data(value, self.encryption_key)
|
|
except Exception as e:
|
|
self.logger.error(f"Encryption failed: {str(e)}")
|
|
raise EncryptionError("Data encryption failed") from e
|
|
|
|
with self._lock, sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.execute(
|
|
"UPDATE storage SET encrypted_value = ?, updated_at = CURRENT_TIMESTAMP WHERE key_hash = ?",
|
|
(encrypted_value, key_hash))
|
|
|
|
if cursor.rowcount == 0:
|
|
raise NotFound(f"Key {key} not found")
|
|
|
|
conn.execute(
|
|
"INSERT INTO access_log (key_hash, operation, user_id) VALUES (?, ?, ?)",
|
|
(key_hash, "update", user_id)
|
|
)
|
|
|
|
def begin_transaction(self):
|
|
"""Begin a new transaction."""
|
|
self._lock.acquire()
|
|
self._transaction_conn = sqlite3.connect(self.db_path)
|
|
self._transaction_conn.execute("BEGIN")
|
|
|
|
def commit_transaction(self):
|
|
"""Commit the current transaction."""
|
|
if hasattr(self, '_transaction_conn'):
|
|
self._transaction_conn.commit()
|
|
self._transaction_conn.close()
|
|
self._lock.release()
|
|
|
|
def rollback_transaction(self):
|
|
"""Rollback the current transaction."""
|
|
if hasattr(self, '_transaction_conn'):
|
|
self._transaction_conn.rollback()
|
|
self._transaction_conn.close()
|
|
self._lock.release() |