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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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