first commit
This commit is contained in:
47
app/core/__init__.py
Normal file
47
app/core/__init__.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""
|
||||
Core Module
|
||||
|
||||
Provides core functionality for the application:
|
||||
- database: Database configuration and session management
|
||||
- security: Password hashing, JWT tokens, and security utilities
|
||||
- logging_config: Structured logging configuration
|
||||
|
||||
Usage:
|
||||
from app.core.database import db
|
||||
from app.core.security import hash_password, verify_password
|
||||
from app.core.logging_config import get_logger
|
||||
"""
|
||||
|
||||
from app.core.database import db, init_db, get_db_session
|
||||
from app.core.security import (
|
||||
hash_password,
|
||||
verify_password,
|
||||
create_jwt_token,
|
||||
decode_jwt_token,
|
||||
create_id_token,
|
||||
generate_secure_token,
|
||||
generate_client_secret,
|
||||
validate_password_strength
|
||||
)
|
||||
from app.core.logging_config import setup_logging, get_logger
|
||||
|
||||
__all__ = [
|
||||
# Database
|
||||
'db',
|
||||
'init_db',
|
||||
'get_db_session',
|
||||
|
||||
# Security
|
||||
'hash_password',
|
||||
'verify_password',
|
||||
'create_jwt_token',
|
||||
'decode_jwt_token',
|
||||
'create_id_token',
|
||||
'generate_secure_token',
|
||||
'generate_client_secret',
|
||||
'validate_password_strength',
|
||||
|
||||
# Logging
|
||||
'setup_logging',
|
||||
'get_logger',
|
||||
]
|
||||
56
app/core/database.py
Normal file
56
app/core/database.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""
|
||||
Database Configuration and Session Management
|
||||
|
||||
Provides database initialization, session management, and base models
|
||||
following the Python Quick Start Guide best practices.
|
||||
"""
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from typing import Generator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Database instance
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
def init_db(app) -> None:
|
||||
"""
|
||||
Initialize database with Flask app.
|
||||
|
||||
Args:
|
||||
app: Flask application instance
|
||||
"""
|
||||
db.init_app(app)
|
||||
|
||||
|
||||
def get_db_session() -> Session:
|
||||
"""
|
||||
Get current database session.
|
||||
|
||||
Returns:
|
||||
SQLAlchemy session instance
|
||||
|
||||
Note:
|
||||
This is a Flask-SQLAlchemy session, managed automatically.
|
||||
Use db.session throughout the application.
|
||||
"""
|
||||
return db.session
|
||||
|
||||
|
||||
# For FastAPI-style dependency injection (if migrating to FastAPI later)
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
"""
|
||||
Get database session for dependency injection.
|
||||
|
||||
Yields:
|
||||
Database session
|
||||
|
||||
Usage:
|
||||
def some_function(db: Session = Depends(get_db)):
|
||||
# Use db session
|
||||
"""
|
||||
try:
|
||||
yield db.session
|
||||
finally:
|
||||
# Flask-SQLAlchemy handles cleanup automatically
|
||||
pass
|
||||
255
app/core/logging_config.py
Normal file
255
app/core/logging_config.py
Normal file
@ -0,0 +1,255 @@
|
||||
"""
|
||||
Logging Configuration
|
||||
|
||||
Provides structured logging setup for the application.
|
||||
Based on Python Quick Start Guide best practices.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def setup_logging(
|
||||
log_level: str = "INFO",
|
||||
environment: str = "development"
|
||||
) -> None:
|
||||
"""
|
||||
Configure application logging.
|
||||
|
||||
Args:
|
||||
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
environment: Environment name (development, production)
|
||||
|
||||
Usage:
|
||||
setup_logging(log_level="INFO", environment="production")
|
||||
"""
|
||||
level = getattr(logging, log_level.upper(), logging.INFO)
|
||||
|
||||
# Clear existing handlers
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
# Console handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(level)
|
||||
|
||||
# Format based on environment
|
||||
if environment == "development":
|
||||
# Human-readable format for development
|
||||
formatter = logging.Formatter(
|
||||
fmt='%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d | %(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
else:
|
||||
# Structured format for production (easier to parse)
|
||||
formatter = logging.Formatter(
|
||||
fmt='%(asctime)s | %(levelname)s | %(name)s | %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
console_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
root_logger.setLevel(level)
|
||||
|
||||
# Suppress noisy loggers
|
||||
logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""
|
||||
Get a logger instance.
|
||||
|
||||
Args:
|
||||
name: Logger name (typically __name__)
|
||||
|
||||
Returns:
|
||||
Logger instance
|
||||
|
||||
Example:
|
||||
logger = get_logger(__name__)
|
||||
logger.info("User logged in", extra={"user_id": 123})
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Structured Logging Helpers
|
||||
# ==========================================
|
||||
|
||||
def log_audit_event(
|
||||
logger: logging.Logger,
|
||||
action: str,
|
||||
user_id: Optional[int] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Log an audit event with structured data.
|
||||
|
||||
Args:
|
||||
logger: Logger instance
|
||||
action: Action performed (e.g., "login_success", "user_created")
|
||||
user_id: User ID performing action
|
||||
ip_address: IP address of request
|
||||
**kwargs: Additional context data
|
||||
|
||||
Example:
|
||||
log_audit_event(
|
||||
logger,
|
||||
action="login_success",
|
||||
user_id=123,
|
||||
ip_address="192.168.1.1",
|
||||
username="admin"
|
||||
)
|
||||
"""
|
||||
extra_data = {
|
||||
'action': action,
|
||||
'user_id': user_id,
|
||||
'ip_address': ip_address,
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Filter out None values
|
||||
extra_data = {k: v for k, v in extra_data.items() if v is not None}
|
||||
|
||||
logger.info(f"AUDIT: {action}", extra=extra_data)
|
||||
|
||||
|
||||
def log_security_event(
|
||||
logger: logging.Logger,
|
||||
event_type: str,
|
||||
severity: str = "warning",
|
||||
**kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Log a security-related event.
|
||||
|
||||
Args:
|
||||
logger: Logger instance
|
||||
event_type: Type of security event (e.g., "failed_login", "rate_limit_exceeded")
|
||||
severity: Severity level (debug, info, warning, error, critical)
|
||||
**kwargs: Additional context data
|
||||
|
||||
Example:
|
||||
log_security_event(
|
||||
logger,
|
||||
event_type="failed_login",
|
||||
severity="warning",
|
||||
username="admin",
|
||||
ip_address="192.168.1.1",
|
||||
attempts=3
|
||||
)
|
||||
"""
|
||||
extra_data = {
|
||||
'event_type': event_type,
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
**kwargs
|
||||
}
|
||||
|
||||
log_method = getattr(logger, severity.lower(), logger.warning)
|
||||
log_method(f"SECURITY: {event_type}", extra=extra_data)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Request Logging Helpers
|
||||
# ==========================================
|
||||
|
||||
def log_request(
|
||||
logger: logging.Logger,
|
||||
method: str,
|
||||
path: str,
|
||||
status_code: int,
|
||||
duration_ms: float,
|
||||
user_id: Optional[int] = None
|
||||
) -> None:
|
||||
"""
|
||||
Log an HTTP request.
|
||||
|
||||
Args:
|
||||
logger: Logger instance
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
path: Request path
|
||||
status_code: HTTP status code
|
||||
duration_ms: Request duration in milliseconds
|
||||
user_id: Authenticated user ID (if any)
|
||||
|
||||
Example:
|
||||
log_request(
|
||||
logger,
|
||||
method="POST",
|
||||
path="/api/users",
|
||||
status_code=201,
|
||||
duration_ms=45.2,
|
||||
user_id=123
|
||||
)
|
||||
"""
|
||||
extra_data = {
|
||||
'method': method,
|
||||
'path': path,
|
||||
'status_code': status_code,
|
||||
'duration_ms': round(duration_ms, 2),
|
||||
'user_id': user_id
|
||||
}
|
||||
|
||||
# Filter out None values
|
||||
extra_data = {k: v for k, v in extra_data.items() if v is not None}
|
||||
|
||||
# Use different log levels based on status code
|
||||
if status_code >= 500:
|
||||
logger.error(f"{method} {path} {status_code}", extra=extra_data)
|
||||
elif status_code >= 400:
|
||||
logger.warning(f"{method} {path} {status_code}", extra=extra_data)
|
||||
else:
|
||||
logger.info(f"{method} {path} {status_code}", extra=extra_data)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Error Logging Helpers
|
||||
# ==========================================
|
||||
|
||||
def log_exception(
|
||||
logger: logging.Logger,
|
||||
error: Exception,
|
||||
context: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Log an exception with context.
|
||||
|
||||
Args:
|
||||
logger: Logger instance
|
||||
error: Exception instance
|
||||
context: Additional context about where error occurred
|
||||
**kwargs: Additional context data
|
||||
|
||||
Example:
|
||||
try:
|
||||
# Some operation
|
||||
except Exception as e:
|
||||
log_exception(
|
||||
logger,
|
||||
error=e,
|
||||
context="Failed to create user",
|
||||
user_id=123
|
||||
)
|
||||
"""
|
||||
extra_data = {
|
||||
'error_type': type(error).__name__,
|
||||
'error_message': str(error),
|
||||
'context': context,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Filter out None values
|
||||
extra_data = {k: v for k, v in extra_data.items() if v is not None}
|
||||
|
||||
logger.error(
|
||||
f"Exception: {type(error).__name__}: {str(error)}",
|
||||
extra=extra_data,
|
||||
exc_info=True
|
||||
)
|
||||
237
app/core/security.py
Normal file
237
app/core/security.py
Normal file
@ -0,0 +1,237 @@
|
||||
"""
|
||||
Security Utilities
|
||||
|
||||
Provides password hashing, JWT token management, and other security functions
|
||||
following the Python Quick Start Guide best practices.
|
||||
"""
|
||||
|
||||
import bcrypt
|
||||
import jwt
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Password Hashing (bcrypt)
|
||||
# ==========================================
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""
|
||||
Hash a password using bcrypt.
|
||||
|
||||
Args:
|
||||
password: Plain text password
|
||||
|
||||
Returns:
|
||||
Hashed password as string
|
||||
|
||||
Example:
|
||||
hashed = hash_password("mypassword123")
|
||||
"""
|
||||
password_bytes = password.encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password_bytes, salt)
|
||||
return hashed.decode('utf-8')
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""
|
||||
Verify a password against a hash.
|
||||
|
||||
Args:
|
||||
plain_password: Plain text password to check
|
||||
hashed_password: Hashed password to compare against
|
||||
|
||||
Returns:
|
||||
True if password matches, False otherwise
|
||||
|
||||
Example:
|
||||
if verify_password("mypassword123", user.password_hash):
|
||||
# Password is correct
|
||||
"""
|
||||
password_bytes = plain_password.encode('utf-8')
|
||||
hash_bytes = hashed_password.encode('utf-8')
|
||||
return bcrypt.checkpw(password_bytes, hash_bytes)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# JWT Token Management
|
||||
# ==========================================
|
||||
|
||||
def create_jwt_token(
|
||||
payload: Dict[str, Any],
|
||||
secret_key: str,
|
||||
algorithm: str = 'HS256',
|
||||
expires_in: int = 3600
|
||||
) -> str:
|
||||
"""
|
||||
Create a JWT token with expiration.
|
||||
|
||||
Args:
|
||||
payload: Token payload data
|
||||
secret_key: Secret key for signing
|
||||
algorithm: JWT algorithm (HS256, RS256, etc.)
|
||||
expires_in: Token lifetime in seconds
|
||||
|
||||
Returns:
|
||||
Encoded JWT token string
|
||||
|
||||
Example:
|
||||
token = create_jwt_token(
|
||||
payload={'user_id': 123, 'role': 'admin'},
|
||||
secret_key=app.config['SECRET_KEY'],
|
||||
expires_in=3600
|
||||
)
|
||||
"""
|
||||
payload = payload.copy()
|
||||
expire = datetime.utcnow() + timedelta(seconds=expires_in)
|
||||
payload.update({'exp': expire, 'iat': datetime.utcnow()})
|
||||
|
||||
return jwt.encode(payload, secret_key, algorithm=algorithm)
|
||||
|
||||
|
||||
def decode_jwt_token(
|
||||
token: str,
|
||||
secret_key: str,
|
||||
algorithm: str = 'HS256'
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Decode and verify a JWT token.
|
||||
|
||||
Args:
|
||||
token: JWT token string
|
||||
secret_key: Secret key for verification
|
||||
algorithm: JWT algorithm used
|
||||
|
||||
Returns:
|
||||
Decoded payload dict, or None if invalid
|
||||
|
||||
Example:
|
||||
payload = decode_jwt_token(token, app.config['SECRET_KEY'])
|
||||
if payload:
|
||||
user_id = payload['user_id']
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(token, secret_key, algorithms=[algorithm])
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
# Token has expired
|
||||
return None
|
||||
except jwt.InvalidTokenError:
|
||||
# Token is invalid
|
||||
return None
|
||||
|
||||
|
||||
def create_id_token(
|
||||
user_data: Dict[str, Any],
|
||||
client_id: str,
|
||||
issuer: str,
|
||||
private_key: str,
|
||||
algorithm: str = 'RS256',
|
||||
expires_in: int = 3600
|
||||
) -> str:
|
||||
"""
|
||||
Create an OIDC ID Token (JWT).
|
||||
|
||||
Args:
|
||||
user_data: User information (sub, email, name, etc.)
|
||||
client_id: OAuth client ID (aud claim)
|
||||
issuer: OIDC issuer URL (iss claim)
|
||||
private_key: Private key for RS256 signing
|
||||
algorithm: JWT algorithm (should be RS256 for OIDC)
|
||||
expires_in: Token lifetime in seconds
|
||||
|
||||
Returns:
|
||||
Encoded ID token string
|
||||
|
||||
Example:
|
||||
id_token = create_id_token(
|
||||
user_data={'sub': 'user-123', 'email': 'user@example.com'},
|
||||
client_id='my-app',
|
||||
issuer='https://auth.example.com',
|
||||
private_key=app.config['OIDC_JWT_PRIVATE_KEY']
|
||||
)
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
payload = {
|
||||
'iss': issuer,
|
||||
'sub': user_data.get('sub'),
|
||||
'aud': client_id,
|
||||
'exp': now + timedelta(seconds=expires_in),
|
||||
'iat': now,
|
||||
**user_data # Include all user claims
|
||||
}
|
||||
|
||||
return jwt.encode(payload, private_key, algorithm=algorithm)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Token Generation
|
||||
# ==========================================
|
||||
|
||||
def generate_secure_token(length: int = 32) -> str:
|
||||
"""
|
||||
Generate a cryptographically secure random token.
|
||||
|
||||
Args:
|
||||
length: Token length in bytes (default 32)
|
||||
|
||||
Returns:
|
||||
URL-safe token string
|
||||
|
||||
Example:
|
||||
auth_code = generate_secure_token(32)
|
||||
access_token = generate_secure_token(64)
|
||||
"""
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
|
||||
def generate_client_secret() -> str:
|
||||
"""
|
||||
Generate a secure client secret for OIDC clients.
|
||||
|
||||
Returns:
|
||||
URL-safe client secret string
|
||||
|
||||
Example:
|
||||
client_secret = generate_client_secret()
|
||||
"""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# Password Validation
|
||||
# ==========================================
|
||||
|
||||
def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate password strength.
|
||||
|
||||
Args:
|
||||
password: Password to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
|
||||
Rules:
|
||||
- Minimum 8 characters
|
||||
- At least one digit (optional but recommended)
|
||||
|
||||
Example:
|
||||
is_valid, error = validate_password_strength("password123")
|
||||
if not is_valid:
|
||||
raise ValueError(error)
|
||||
"""
|
||||
if len(password) < 8:
|
||||
return False, "Password must be at least 8 characters long"
|
||||
|
||||
# Optional: Check for digit
|
||||
# if not any(char.isdigit() for char in password):
|
||||
# return False, "Password must contain at least one digit"
|
||||
|
||||
# Optional: Check for uppercase
|
||||
# if not any(char.isupper() for char in password):
|
||||
# return False, "Password must contain at least one uppercase letter"
|
||||
|
||||
return True, None
|
||||
Reference in New Issue
Block a user