04/06 remediate some issues

This commit is contained in:
2026-04-06 16:31:50 -04:00
parent d7293f2747
commit 3b17705911
8 changed files with 211 additions and 28 deletions
+47 -5
View File
@@ -25,6 +25,23 @@ logger = logging.getLogger(__name__)
ALLOWED_EXT = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx', 'txt', 'zip', 'log'}
def _strip_html(text: str) -> str:
"""Remove HTML tags from *text* for plain-text search matching.
Comment bodies are stored as sanitized HTML (rendered at write time via
render_comment_body). Searching with ilike('%term%') against raw HTML
produces two problems:
1. A search for 'bold' misses '<strong>bold</strong>' in the stored body.
2. HTML tag names ('strong', 'pre') can accidentally match search terms.
Stripping tags before comparison gives consistent, tag-agnostic results.
Uses a simple regex rather than a full HTML parser — sufficient for the
sanitized subset of HTML that bleach allows in comment bodies.
"""
import re
return re.sub(r'<[^>]+>', '', text)
def _resolve_mime_type(att):
"""Return a reliable MIME type for an attachment.
@@ -195,8 +212,13 @@ def ticket_list():
if search:
# Extend search to cover comments and assignee name via outer joins.
# distinct() prevents duplicate ticket rows when multiple comments match.
# Comment bodies are stored as sanitized HTML — use REGEXP_REPLACE to
# strip tags at the SQL level before matching so 'bold' finds
# '<strong>bold</strong>' and HTML tag names don't pollute results.
from app.models import Comment
from sqlalchemy import func
assignee_alias = db.aliased(User)
stripped_body = func.regexp_replace(Comment.body, r'<[^>]+>', '', 'g')
query = (
query
.outerjoin(Comment, Comment.ticket_id == Ticket.id)
@@ -205,7 +227,7 @@ def ticket_list():
Ticket.title.ilike(f'%{search}%') |
Ticket.ticket_number.ilike(f'%{search}%') |
Ticket.description.ilike(f'%{search}%') |
Comment.body.ilike(f'%{search}%') |
stripped_body.ilike(f'%{search}%') |
assignee_alias.full_name.ilike(f'%{search}%')
)
.distinct()
@@ -389,12 +411,32 @@ def update_ticket(ticket_id):
@tickets_bp.route('/comments/<int:comment_id>/delete', methods=['POST'])
@login_required
def delete_comment(comment_id):
comment = db.session.get(Comment, comment_id) or abort(404)
if not current_user.is_it_staff and comment.author_id != current_user.id:
abort(403)
comment = db.session.get(Comment, comment_id) or abort(404)
ticket_id = comment.ticket_id
# Authorization: IT staff may always delete any comment.
# Employees may only delete their own comments, and only while the
# ticket is still open or in-progress. Allowing deletion on resolved
# or closed tickets would silently alter the historical record of a
# completed support interaction.
if not current_user.is_it_staff:
if comment.author_id != current_user.id:
abort(403)
ticket = db.session.get(Ticket, ticket_id) or abort(404)
if ticket.status in (TicketStatus.RESOLVED, TicketStatus.CLOSED):
logger.warning(
f'[COMMENT DELETE BLOCKED] comment_id={comment.id} '
f'ticket_id={ticket_id} status={ticket.status} '
f'user_id={current_user.id} — ticket is {ticket.status}'
)
flash('Comments cannot be deleted on resolved or closed tickets.', 'warning')
return redirect(url_for('tickets.ticket_detail', ticket_id=ticket_id))
# Include a truncated snapshot of the body in the audit log so the
# content is recoverable from logs even after the DB row is gone.
body_snapshot = comment.body[:200].replace('\n', ' ')
log_action(current_user.id, 'comment_delete', 'comment', comment.id,
f'ticket_id={ticket_id}')
f'ticket_id={ticket_id} body_snapshot="{body_snapshot}"')
logger.info(f'[COMMENT DELETE] comment_id={comment.id} ticket_id={ticket_id} by user_id={current_user.id}')
db.session.delete(comment)
db.session.commit()