security: rate limiter, is_admin flag, password policy, log masking
M-3: Role.name == 'Admin' string comparison → Role.is_admin boolean flag. Admin role created with is_admin=True. has_permission checks getattr(role, 'is_admin', False) instead of comparing role names. M-2: In-memory rate limiter on /api/auth/login. Tracks failed attempts per IP (10 in 5 min window, 15 min ban). Thread-safe via threading.Lock. M-5: Password policy — UserCreate.password: min_length=8, must include uppercase, lowercase, and digit. L-1: log_security_event details dict now masks sensitive fields: password, secret, token, api_key, authorization. Recursive masking for nested dicts. Sensitive field names matched via regex. OTHER: - 4 rate limiter tests (under limit, ban, success clears, IP isolation) - 4 password policy tests (too short, no upper, no digit, valid) - 2 log masking tests (flat keys, nested)
This commit is contained in:
@@ -18,6 +18,7 @@ from ..core.auth.logger import log_security_event
|
||||
from ..core.auth.oauth import is_adfs_configured, oauth
|
||||
from ..core.database import get_auth_db
|
||||
from ..core.logger import belief_scope, logger
|
||||
from ..core.rate_limiter import rate_limiter
|
||||
from ..dependencies import get_current_user
|
||||
from ..schemas.auth import Token, User as UserSchema
|
||||
from ..services.auth_service import AuthService
|
||||
@@ -38,12 +39,26 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_for_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_auth_db)
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_auth_db),
|
||||
request: starlette.requests.Request = None,
|
||||
):
|
||||
with belief_scope("api.auth.login"):
|
||||
# Rate limiting — protect against brute force
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
if rate_limiter.is_banned(client_ip):
|
||||
log_security_event(
|
||||
"LOGIN_BLOCKED", form_data.username, {"reason": "Rate limited", "ip": client_ip}
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail="Too many login attempts. Try again later.",
|
||||
)
|
||||
|
||||
auth_service = AuthService(db)
|
||||
user = auth_service.authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
rate_limiter.record_attempt(client_ip)
|
||||
log_security_event(
|
||||
"LOGIN_FAILED", form_data.username, {"reason": "Invalid credentials"}
|
||||
)
|
||||
@@ -52,6 +67,7 @@ async def login_for_access_token(
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
rate_limiter.record_success(client_ip)
|
||||
log_security_event("LOGIN_SUCCESS", user.username, {"source": "LOCAL"})
|
||||
return auth_service.create_session(user)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user