Implement MVP: Anonymous feedback submission (User Story 1)

Complete implementation of Phase 1-3 (64 tasks):
- Phase 1: Project setup with Flask, pytest, configuration
- Phase 2: Core infrastructure (auth, models, services, testing)
- Phase 3: Anonymous feedback submission with file uploads

Features:
- Anonymous feedback submission (text and/or up to 3 file attachments)
- Multi-language support (any language accepted)
- File validation (type, size) and virus scanning (ClamAV)
- Product management with active/archived status
- File-based storage with YAML metadata
- User authentication system (Flask-Login)
- CSRF protection and rate limiting
- Test coverage: 10 passing tests (contract + integration)

Security:
- No IP address logging (FR-055 compliance)
- File type whitelist and size limits (10MB max)
- Virus scanning with graceful degradation
- Filename sanitization and secure storage

Test Results:
- 8 contract tests passed
- 2 integration tests passed
- End-to-end workflow verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-16 15:14:51 +02:00
co-authored by Claude
parent 07e51d7468
commit b301def134
37 changed files with 2416 additions and 39 deletions
+2
View File
@@ -0,0 +1,2 @@
"""Utilities package"""
# Utility functions for validation, security, etc.
+144
View File
@@ -0,0 +1,144 @@
"""File upload validation utilities"""
import os
from werkzeug.utils import secure_filename
import clamd
from flask import current_app
# Allowed file extensions for attachments
ALLOWED_EXTENSIONS = {
'txt', 'log', 'pdf', 'png', 'jpg', 'jpeg', 'gif',
'doc', 'docx', 'xls', 'xlsx', 'csv'
}
# Maximum file size (10MB)
MAX_FILE_SIZE = 10 * 1024 * 1024
def allowed_file(filename):
"""Check if file extension is allowed
Args:
filename: Name of the uploaded file
Returns:
bool: True if extension is allowed, False otherwise
"""
if not filename:
return False
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def validate_file_size(file_stream):
"""Check if file size is within limits
Args:
file_stream: File stream object
Returns:
bool: True if size is acceptable, False otherwise
"""
# Seek to end to get file size
file_stream.seek(0, os.SEEK_END)
size = file_stream.tell()
# Reset to beginning
file_stream.seek(0)
return size <= MAX_FILE_SIZE
def get_safe_filename(filename):
"""Get secure version of filename
Args:
filename: Original filename
Returns:
str: Secure filename safe for filesystem storage
"""
return secure_filename(filename)
def validate_file(file):
"""Validate uploaded file
Args:
file: Werkzeug FileStorage object
Returns:
tuple: (is_valid, error_message)
is_valid: bool indicating if file is valid
error_message: str with error description or None
"""
if not file:
return False, "No file provided"
if file.filename == '':
return False, "No file selected"
if not allowed_file(file.filename):
return False, f"File type not allowed. Allowed types: {', '.join(ALLOWED_EXTENSIONS)}"
if not validate_file_size(file.stream):
return False, f"File size exceeds maximum of {MAX_FILE_SIZE / (1024 * 1024):.0f}MB"
return True, None
def scan_file_for_viruses(file):
"""Scan file for viruses using ClamAV
Args:
file: Werkzeug FileStorage object
Returns:
tuple: (is_clean, error_message)
is_clean: bool indicating if file is clean (True) or infected (False)
error_message: str with error description or None
"""
try:
# Connect to ClamAV daemon
clamd_socket = current_app.config.get('CLAMD_SOCKET')
if not clamd_socket:
# ClamAV not configured, skip scanning
current_app.logger.warning("ClamAV socket not configured, skipping virus scan")
return True, None
cd = clamd.ClamdUnixSocket(clamd_socket)
# Ping to check if ClamAV is available
try:
cd.ping()
except Exception as e:
current_app.logger.warning(f"ClamAV not available: {e}, skipping virus scan")
return True, None
# Read file content
file.stream.seek(0)
file_data = file.stream.read()
file.stream.seek(0) # Reset for later use
# Scan file
scan_result = cd.instream(file_data)
# Check result
if scan_result and 'stream' in scan_result:
status, virus_name = scan_result['stream']
if status == 'OK':
return True, None
elif status == 'FOUND':
return False, f"Virus detected: {virus_name}"
else:
return False, f"Scan error: {status}"
return True, None
except Exception as e:
current_app.logger.error(f"ClamAV scanning error: {e}")
# On error, we'll allow the file but log the error
# In production, you might want to reject files if scanning fails
return True, None