OpenSPP Attachment Scan: Protecting Admin-Tuned Crons From Upgrade Resets

Platform: OpenSPP (OpenSPP2) · Module: spp_attachment_av_scan · Type: Backend / Upgrade safety and configuration persistence · Status: Pull request open, awaiting CI and review · Issue: #473 · PR: #479

Context

The spp_attachment_av_scan module ships some scheduled jobs and settings as data records. In data/quarantine_cron.xml there are two crons, one that purges old quarantined files daily and one that cleans up forensic download copies hourly, plus two configuration parameters: a quarantine retention window of 90 days and a forensic-download retention window of 24 hours.

These are exactly the kind of values a deployment is expected to tune. An administrator might lengthen the retention window, change how often a cron runs, or deliberately switch one off.

Problem

The file declared all four records in a plain <odoo> block, with no noupdate flag:

<odoo>
    <record id="ir_cron_purge_quarantined_files" model="ir.cron"> ... </record>
    <record id="ir_cron_cleanup_forensic_downloads" model="ir.cron"> ... </record>
    <record id="config_param_quarantine_retention_days" ...>90</...>
    <record id="config_param_forensic_download_retention_hours" ...>24</...>
</odoo>

Without noupdate, Odoo rewrites those records on every module upgrade. So an admin who tuned a retention window, changed a cron interval, or disabled a cron would find their change silently reverted to the shipped default the next time the module was upgraded. The tuning just vanished, with nothing to point at.

Background

This defect was already known in a neighboring file. The review of pull request #470 fixed the same class of problem in that module's newer scan_sweep_cron.xml, and added a regression test for it. The pre-existing quarantine_cron.xml had the same flaw, and was split out into its own issue so it could be fixed the same way. That gave me a proven template to follow rather than a blank page.

My Approach

The fix has three parts, because marking the file noupdate is necessary but not sufficient on databases that already exist.

First, wrap the data file so new installs create the records as protected:

<odoo noupdate="1">

Second, a migration. Odoo only honors noupdate at the moment a record is first created. On any deployment that already installed the module, the four ir.model.data rows already exist with noupdate = False, and changing the XML alone flips nothing. So I added a post-migration that updates the existing rows in place:

cr.execute(
    """
    UPDATE ir_model_data
    SET noupdate = TRUE
    WHERE module = 'spp_attachment_av_scan'
      AND name IN %s
    """,
    (_RECORDS,),
)

It sets the flag and leaves the stored values untouched, which is the safe default. Where an admin never tuned a value it is still the shipped default, and where they did tune it, their value is preserved.

Third, a manifest version bump to 19.0.2.2.0, with the migration placed under migrations/19.0.2.2.0/post-migrate.py. Odoo runs migration scripts from the folder named after the version it is upgrading to, so the folder name has to match the new manifest version exactly.

Tests

Rather than write a new test, I extended the one #470 already created: test_the_cron_and_config_defaults_are_not_reset_by_a_module_upgrade. It loops over each shipped record and asserts its ir.model.data row has noupdate = True. I added the four quarantine record names to that loop, so the single test now covers both cron files in the module.

Considerations

Fix Process

Current Status

Pull request #479 is open against the 19.0 branch and merges cleanly, awaiting CI and maintainer review. As with my other contributions, I could not run the Odoo suite locally, so I am relying on the project's CI to exercise the spp_attachment_av_scan tests and the migration on the pull request.

Reflection

The one-line XML change is the visible part, but the interesting part is what it does not do. Because noupdate is only read at creation time, the honest fix has to reach back to the records that already exist and correct them, without disturbing the very values it is trying to protect. That distinction between a fresh install and an upgraded one is easy to miss, and missing it would have shipped a change that quietly did nothing on every real deployment.

For an antivirus quarantine, the settings being protected decide how long potentially harmful files are retained. Letting an operator's deliberate choices survive an upgrade is a small piece of trustworthiness that matters more than its size suggests.