cmboulanger's picture
disaster-recovery deploy of pdf-tei-editor
6a49f21 verified
Raw
History Blame Contribute Delete
8.75 kB
"""
Authentication management for PDF-TEI-Editor.
This module provides framework-agnostic authentication utilities with dependency injection.
No Flask or FastAPI dependencies - all parameters are explicitly passed.
"""
import json
import sys
import threading
from pathlib import Path
from typing import Optional
# Platform-specific imports for file locking
if sys.platform == 'win32':
import msvcrt
else:
import fcntl
def _lock_file(file_handle):
"""Cross-platform file locking"""
if sys.platform == 'win32':
try:
msvcrt.locking(file_handle.fileno(), msvcrt.LK_LOCK, 1)
except OSError:
pass
else:
fcntl.flock(file_handle, fcntl.LOCK_EX)
def _unlock_file(file_handle):
"""Cross-platform file unlocking"""
if sys.platform == 'win32':
try:
msvcrt.locking(file_handle.fileno(), msvcrt.LK_UNLCK, 1)
except OSError:
pass
else:
fcntl.flock(file_handle, fcntl.LOCK_UN)
class AuthManager:
"""
Authentication manager with dependency injection.
Manages user authentication and password verification.
Uses SHA-256 password hashing for compatibility with existing system.
"""
def __init__(self, db_dir: Path, logger=None):
"""
Initialize authentication manager.
Args:
db_dir: Path to the database directory containing users.json
logger: Optional logger instance for logging operations
"""
self.db_dir = db_dir
self.logger = logger
self.users_file = db_dir / 'users.json'
self.lock = threading.Lock()
def _read_users(self) -> list:
"""
Read users from users.json file.
Returns:
List of user dictionaries
"""
with self.lock:
try:
if not self.users_file.exists():
# Create empty users file
self.users_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.users_file, 'w', encoding='utf-8') as f:
json.dump([], f, indent=2)
return []
with open(self.users_file, 'r', encoding='utf-8') as f:
return json.load(f)
except (IOError, json.JSONDecodeError) as e:
if self.logger:
self.logger.error(f"Error reading users file: {e}")
return []
def _write_users(self, users_data: list):
"""
Write users to users.json file.
Args:
users_data: List of user dictionaries to write
"""
with self.lock:
try:
self.users_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.users_file, 'w', encoding='utf-8') as f:
json.dump(users_data, f, indent=2)
except IOError as e:
if self.logger:
self.logger.error(f"Error writing users file: {e}")
def get_user_by_username(self, username: str) -> Optional[dict]:
"""
Get user by username.
Args:
username: Username to look up
Returns:
User dictionary or None if not found
"""
users = self._read_users()
for user in users:
if user.get('username') == username:
# Return copy without sensitive session data
user_copy = user.copy()
user_copy.pop('session_id', None) # Remove legacy session_id
return user_copy
return None
def verify_password(self, username: str, passwd_hash: str) -> Optional[dict]:
"""
Verify password hash and return user if valid.
Uses SHA-256 password hashing for compatibility.
Args:
username: Username to verify
passwd_hash: SHA-256 hash of the password
Returns:
User dictionary if credentials valid, None otherwise
"""
user = self.get_user_by_username(username)
if user and user.get('passwd_hash') == passwd_hash:
if self.logger:
self.logger.info(f"Password verified for user {username}")
return user
if self.logger:
self.logger.warning(f"Failed password verification for user {username}")
return None
def get_user_by_session_id(self, session_id: str, session_manager=None) -> Optional[dict]:
"""
Get user by session ID.
This is a convenience method that delegates to the session manager.
Requires a SessionManager instance to be passed in.
Args:
session_id: Session ID to look up
session_manager: SessionManager instance for session validation
Returns:
User dictionary or None if session invalid
"""
if not session_id or not session_manager:
return None
# Get username from session
username = session_manager.get_username_by_session_id(session_id)
if not username:
return None
# Get user data
return self.get_user_by_username(username)
def create_user(self, username: str, passwd_hash: str, **kwargs) -> bool:
"""
Create a new user.
Args:
username: Username for the new user
passwd_hash: SHA-256 hash of the password
**kwargs: Additional user attributes (e.g., email, role)
Returns:
True if user created, False if user already exists
"""
# Hold lock for entire check-then-create operation to prevent race conditions
with self.lock:
try:
# Read current users while holding the lock
if not self.users_file.exists():
self.users_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.users_file, 'w', encoding='utf-8') as f:
json.dump([], f, indent=2)
users = []
else:
with open(self.users_file, 'r', encoding='utf-8') as f:
users = json.load(f)
# Check if user already exists
if any(user.get('username') == username for user in users):
if self.logger:
self.logger.warning(f"User {username} already exists")
return False
# Create new user
new_user = {
'username': username,
'passwd_hash': passwd_hash,
**kwargs
}
users.append(new_user)
# Write users while still holding the lock
self.users_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.users_file, 'w', encoding='utf-8') as f:
json.dump(users, f, indent=2)
if self.logger:
self.logger.info(f"Created user {username}")
return True
except (IOError, json.JSONDecodeError) as e:
if self.logger:
self.logger.error(f"Error creating user: {e}")
return False
def update_user(self, username: str, **kwargs) -> bool:
"""
Update user attributes.
Args:
username: Username of the user to update
**kwargs: Attributes to update
Returns:
True if user updated, False if user not found
"""
users = self._read_users()
for user in users:
if user.get('username') == username:
user.update(kwargs)
self._write_users(users)
if self.logger:
self.logger.info(f"Updated user {username}")
return True
if self.logger:
self.logger.warning(f"User {username} not found for update")
return False
def delete_user(self, username: str) -> bool:
"""
Delete a user.
Args:
username: Username of the user to delete
Returns:
True if user deleted, False if user not found
"""
users = self._read_users()
original_length = len(users)
users = [user for user in users if user.get('username') != username]
if len(users) < original_length:
self._write_users(users)
if self.logger:
self.logger.info(f"Deleted user {username}")
return True
if self.logger:
self.logger.warning(f"User {username} not found for deletion")
return False