updates
This commit is contained in:
Binary file not shown.
@@ -123,6 +123,34 @@ def get_current_user(
|
||||
detail=f'Account is temporarily locked due to multiple failed login attempts. Please try again in {remaining_minutes} minute(s).'
|
||||
)
|
||||
|
||||
# SECURITY: Check MFA for accountant/admin roles (warn but allow access for MFA setup)
|
||||
try:
|
||||
from ...payments.services.accountant_security_service import accountant_security_service
|
||||
from ...shared.utils.role_helpers import is_accountant, is_admin
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
if is_accountant(user, db) or is_admin(user, db):
|
||||
is_enforced, reason = accountant_security_service.is_mfa_enforced(user, db)
|
||||
if not is_enforced and reason:
|
||||
# Log warning but allow access so user can set up MFA
|
||||
# Individual routes can enforce MFA for sensitive operations
|
||||
logger.warning(
|
||||
f'User {user.id} ({user.email}) accessed system without MFA enabled. '
|
||||
f'MFA is required for {user.role.name if user.role else "unknown"} role. '
|
||||
f'Reason: {reason}'
|
||||
)
|
||||
# Store MFA requirement in user object for route-level checks
|
||||
user._mfa_setup_required = True
|
||||
user._mfa_setup_reason = reason
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Don't block authentication if MFA check fails (log and continue)
|
||||
logger = get_logger(__name__)
|
||||
logger.warning(f'Error checking MFA enforcement: {str(e)}')
|
||||
|
||||
return user
|
||||
|
||||
def authorize_roles(*allowed_roles: str):
|
||||
@@ -130,6 +158,8 @@ def authorize_roles(*allowed_roles: str):
|
||||
def role_checker(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)) -> User:
|
||||
# PERFORMANCE: Use eager-loaded relationship if available, otherwise query
|
||||
# This reduces database queries since get_current_user now eager loads the role
|
||||
from ...shared.utils.role_helpers import get_user_role_name
|
||||
|
||||
if hasattr(current_user, 'role') and current_user.role is not None:
|
||||
user_role_name = current_user.role.name
|
||||
else:
|
||||
@@ -139,8 +169,21 @@ def authorize_roles(*allowed_roles: str):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='User role not found')
|
||||
user_role_name = role.name
|
||||
|
||||
# Backwards‑compatible support for accountant sub‑roles:
|
||||
# any role starting with "accountant_" is treated as "accountant"
|
||||
# when a route allows plain "accountant".
|
||||
if user_role_name not in allowed_roles:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='You do not have permission to access this resource')
|
||||
# Normalise accountant_* → accountant for role checks
|
||||
if (
|
||||
user_role_name.startswith("accountant_")
|
||||
and "accountant" in allowed_roles
|
||||
):
|
||||
# treated as allowed
|
||||
return current_user
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='You do not have permission to access this resource'
|
||||
)
|
||||
return current_user
|
||||
return role_checker
|
||||
|
||||
|
||||
87
Backend/src/security/middleware/step_up_auth.py
Normal file
87
Backend/src/security/middleware/step_up_auth.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Step-up authentication middleware for high-risk operations.
|
||||
"""
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user
|
||||
from ...auth.models.user import User
|
||||
from ...payments.services.accountant_security_service import accountant_security_service
|
||||
from ...shared.utils.role_helpers import is_accountant, is_admin
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def require_step_up_auth(
|
||||
action_description: str = "this high-risk action"
|
||||
):
|
||||
"""
|
||||
Dependency to require step-up authentication for high-risk operations.
|
||||
"""
|
||||
async def step_up_checker(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> User:
|
||||
# Only enforce for accountant/admin roles
|
||||
if not (is_accountant(current_user, db) or is_admin(current_user, db)):
|
||||
return current_user # Regular users don't need step-up
|
||||
|
||||
# Get session token from request
|
||||
session_token = None
|
||||
# Try to get from custom header
|
||||
session_token = request.headers.get('X-Session-Token')
|
||||
# Or from cookie
|
||||
if not session_token:
|
||||
session_token = request.cookies.get('session_token')
|
||||
|
||||
# Check if step-up is required
|
||||
requires_step_up, reason = accountant_security_service.require_step_up(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
session_token=session_token,
|
||||
action_description=action_description
|
||||
)
|
||||
|
||||
if requires_step_up:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
'error': 'step_up_required',
|
||||
'message': reason or f'Step-up authentication required for {action_description}',
|
||||
'action': action_description
|
||||
}
|
||||
)
|
||||
|
||||
return current_user
|
||||
|
||||
return step_up_checker
|
||||
|
||||
|
||||
def enforce_mfa_for_accountants():
|
||||
"""
|
||||
Dependency to enforce MFA for accountant/admin roles.
|
||||
"""
|
||||
async def mfa_enforcer(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> User:
|
||||
# Check if MFA is required
|
||||
is_enforced, reason = accountant_security_service.is_mfa_enforced(current_user, db)
|
||||
|
||||
if not is_enforced and reason:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
'error': 'mfa_required',
|
||||
'message': reason,
|
||||
'requires_mfa_setup': True
|
||||
}
|
||||
)
|
||||
|
||||
return current_user
|
||||
|
||||
return mfa_enforcer
|
||||
|
||||
Reference in New Issue
Block a user