Aug 25 - Fixed inspection access denied by inspector

This commit is contained in:
2026-08-25 12:52:06 -04:00
parent aced3d0602
commit a36cc93594
3 changed files with 80 additions and 5 deletions
+66 -2
View File
@@ -772,7 +772,11 @@ def view(inspection_id):
if inspection is None:
abort(404)
if current_user.is_inspector and inspection.inspector_id != current_user.id:
# Read access matches the LIST (rule 58) — an inspector may open anything
# at their contracted facilities, not only what they performed. Editing
# someone else's inspection is still refused (execute / save-draft /
# upload-photo / flag-issue keep the authorship check).
if current_user.is_inspector and not _inspector_may_read(inspection, current_user):
flash('Access denied.', 'danger')
return redirect(url_for('inspections.index'))
if current_user.role == 'customer':
@@ -970,8 +974,22 @@ def view(inspection_id):
}
followup_assignees = _followup_assignees_for(inspection, current_user)
# An inspector viewing SOMEBODY ELSE's inspection gets a read-only page.
# Without this the buttons would all render and then fail on click — the
# same list-says-yes / page-says-no mismatch this change removes.
is_own_inspection = (not current_user.is_inspector
or inspection.inspector_id == current_user.id)
# Whoever is expected to carry out the follow-up (phase53) may start the
# re-inspection even though the original inspection is not theirs.
owns_follow_up = bool(
inspection.follow_up_required
and inspection.follow_up_owner
and inspection.follow_up_owner.id == current_user.id
)
return render_template('inspections/view.html',
followup_assignees=followup_assignees,
is_own_inspection=is_own_inspection,
owns_follow_up=owns_follow_up,
inspection=inspection,
form_fields=form_fields,
form_data=form_data,
@@ -1311,7 +1329,11 @@ def export_pdf(inspection_id):
if inspection is None:
abort(404)
if current_user.is_inspector and inspection.inspector_id != current_user.id:
# Read access matches the LIST (rule 58) — an inspector may open anything
# at their contracted facilities, not only what they performed. Editing
# someone else's inspection is still refused (execute / save-draft /
# upload-photo / flag-issue keep the authorship check).
if current_user.is_inspector and not _inspector_may_read(inspection, current_user):
flash('Access denied.', 'danger')
return redirect(url_for('inspections.index'))
if current_user.role == 'customer':
@@ -1396,6 +1418,28 @@ def _view_url(inspection_id):
return url_for('inspections.view', inspection_id=inspection_id)
def _inspector_may_read(inspection, user):
"""May this inspector OPEN someone else's inspection?
Yes, when it happened at a facility on one of their contracts — the same
scope `index()` uses (rule 58: an inspector's scope covers all data in their
contracted facilities, not just their own work).
This used to test authorship instead, and the two disagreed: the list
showed every inspection at the inspector's facilities, then clicking one
said "Access denied". It also blocked the phase53 follow-up assignee from
opening the parent inspection they had just been asked to re-inspect —
the button they needed was on a page they could not reach.
READ only. Editing someone else's inspection is still refused: execute,
save-draft, upload-photo and flag-issue all keep the authorship check.
"""
fids = get_inspector_scope(user)
if fids is None: # not an inspector — no scoping applies here
return True
return bool(fids) and inspection.facility_id in fids
def _followup_assignees_for(inspection, actor):
"""Inspectors who may be handed this inspection's follow-up.
@@ -1826,6 +1870,26 @@ def reinspect(inspection_id):
flash('Access denied.', 'danger')
return redirect(url_for('inspections.index'))
# An inspector may re-inspect their OWN work, or work they have been
# handed the follow-up for (phase53). Anything else at a contracted
# facility is readable but not theirs to redo — starting a re-inspection
# of a colleague's inspection uninvited only creates confusion about who
# is doing it.
if current_user.is_inspector:
if parent.follow_up_required and parent.follow_up_owner:
# A live follow-up has exactly ONE owner (phase53). Even the
# original inspector does not start it once it has been handed to
# someone else — that is the whole point of assigning it, and two
# people turning up is the failure being designed out.
may = parent.follow_up_owner.id == current_user.id
else:
# No follow-up outstanding: re-inspecting your own work is fine,
# someone else's is not yours to redo uninvited.
may = parent.inspector_id == current_user.id
if not may:
flash('That re-inspection has been assigned to someone else.', 'warning')
return redirect(_view_url(inspection_id))
session['reinspect_parent_id'] = parent.id
session['reinspect_template_id'] = parent.template_id
session['reinspect_facility_id'] = parent.facility_id
+10 -3
View File
@@ -353,7 +353,12 @@
<button onclick="window.print()" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-printer"></i> Print
</button>
{% if current_user.role not in ['customer'] %}
{# Offered to managers, to the inspector who did this inspection, and to
whoever the follow-up was assigned to (phase53). Not to any other
inspector who can merely SEE it: reinspect() refuses them, and showing
a button that fails on click is the mismatch this page just fixed. #}
{% if current_user.role not in ['customer']
and (is_own_inspection or owns_follow_up) %}
<a href="{{ url_for('inspections.reinspect', inspection_id=inspection.id) }}"
class="btn btn-sm btn-outline-primary"
title="Start a follow-up re-inspection with the same template and facility">
@@ -427,8 +432,10 @@
</div>
{% endif %}
{% if inspection.follow_up_note %}<br><span class="small">{{ inspection.follow_up_note }}</span>{% endif %}
{# Re-inspection is staff work — reinspect() already refuses customers. #}
{% if current_user.role != 'customer' %}
{# Re-inspection is staff work — reinspect() already refuses customers
and among inspectors it belongs to the follow-up's OWNER. #}
{% if current_user.role != 'customer'
and (is_own_inspection or owns_follow_up) %}
<div class="mt-2">
<a href="{{ url_for('inspections.reinspect', inspection_id=inspection.id) }}"
class="btn btn-sm btn-warning">