Skip to content

AuditGroupAdmin change form leaves identity-critical group_ref and derived divergent_message_count editable — a Save can split a group's evidence or fabricate its divergence badge #326

Description

@erskingardner

Severity: MEDIUM (evidence integrity / forensic-conclusion corruption via the admin)

AuditGroupAdmin declares no readonly_fields, no exclude, and no fields/fieldsets, so Django renders every editable model field on the change form: name, slug, group_ref, divergent_message_count, and notes. Two of those are not operator-owned metadata — group_ref is the group's forensic identity key, and divergent_message_count is a derived rollup — yet both are freely writable and a Save persists whatever an operator types with no audit trail.

Location

forensics/admin.py:33-37:

@admin.register(AuditGroup)
class AuditGroupAdmin(admin.ModelAdmin):
    list_display = ("name", "slug", "group_ref", "created_at", "updated_at")
    search_fields = ("name", "slug", "group_ref", "notes")
    prepopulated_fields = {"slug": ("name",)}

Model fields (forensics/models.py:13-18):

class AuditGroup(models.Model):
    ...
    group_ref = models.CharField(max_length=512, blank=True, db_index=True)
    divergent_message_count = models.PositiveIntegerField(default=0)
    notes = models.TextField(blank=True)

(created_at/updated_at are auto fields and are auto-excluded from the form; everything else renders as an editable widget.)

Failure scenario

1. Editing group_ref splits a group's evidence. A staff operator opens a group and changes group_ref (e.g. "correcting" a perceived typo, or pasting the wrong value) and Saves. Ingestion resolves a group by exact group_ref:

# forensics/ingest.py:1032-1035  (group_for_ref)
def group_for_ref(group_ref: str) -> AuditGroup:
    existing = AuditGroup.objects.filter(group_ref=group_ref).first()
    if existing is not None:
        return existing
    ...  # else mint a brand-new AuditGroup

The next upload that references the original group_ref no longer matches the mutated row, so ingest mints a new AuditGroup. The group's evidence is now split across two workspace rows — exactly the identity-fragmentation hazard the app treats as a real forensic problem elsewhere (cf. the case-variant/slug-merge issues #195 / #236 and the closed "Avoid merging distinct long group_ref values through truncated slugs").

2. Editing divergent_message_count fabricates the divergence badge. This column is a derived rollup, recomputed only on the next upload for the group (see #257). An operator can type any integer and Save; the group-header divergence badge then reports an operator-fabricated count — with no indication it is not machine-derived — until the next upload recomputes it. Between uploads the UI presents fabricated forensic data as if it were computed evidence.

Why this is not a duplicate

The existing "editable evidence in the admin" cluster is explicitly scoped to other tables and does not touch AuditGroup:

The only existing AuditGroupAdmin items are #158 (show_full_result_count, performance) and #175 (a rebuild-command group-resolution bug) — neither concerns editable derived/identity columns on the change form. So the AuditGroup change form is an uncovered gap in that cluster, and it is arguably the highest-value one to close because group_ref is a group's identity key.

Suggested fix

Mark the identity/derived columns read-only on the change form, mirroring the treatment UploadTokenAdmin / AuditFileAdmin already apply to their evidence columns:

@admin.register(AuditGroup)
class AuditGroupAdmin(admin.ModelAdmin):
    list_display = ("name", "slug", "group_ref", "divergent_message_count", "created_at", "updated_at")
    search_fields = ("name", "slug", "group_ref", "notes")
    prepopulated_fields = {"slug": ("name",)}
    readonly_fields = ("group_ref", "divergent_message_count", "created_at", "updated_at")

name, slug, and notes remain editable (they are legitimately operator-owned); group_ref and divergent_message_count become read-only so a Save can no longer split a group or fabricate its divergence rollup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    MEDIUMSeverity: important bug or performance issue with bounded impactbugSomething isn't workingsecurity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions