Skip to content

[feature] Added "x509_renew" signal for Ca and Cert model #236#237

Merged
nemesifier merged 4 commits into
masterfrom
issues/236-renew-signals
Jul 8, 2026
Merged

[feature] Added "x509_renew" signal for Ca and Cert model #236#237
nemesifier merged 4 commits into
masterfrom
issues/236-renew-signals

Conversation

@pandafy

@pandafy pandafy commented Jul 3, 2026

Copy link
Copy Markdown
Member

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #236

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

This PR adds the x509_renewed Django signal, emits it after successful renewal of Ca and Cert objects, documents the signal contract in README.rst, and adds tests covering both CA and certificate renewal flows.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BaseX509
  participant transaction
  participant x509_renewed as x509_renewed Signal

  BaseX509->>BaseX509: renew()
  BaseX509->>transaction: on_commit(send_robust)
  transaction->>x509_renewed: send_robust(sender, instance)
Loading

Suggested labels: enhancement

Suggested reviewers: nemesifier


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors)

Check name Status Explanation Resolution
Description check ❌ Error The description is missing the required change summary and screenshot sections from the template. Add a Description of Changes section and a Screenshot section, or note that no screenshot is needed if applicable.
General Rules ❌ Error Issue #236 is validated, but x509_renewed uses send_robust without logging receiver exceptions, so failures are swallowed. Handle send_robust results and log receiver exceptions at error level; add a test for a failing receiver.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the feature work and references the linked issue.
Linked Issues check ✅ Passed The changes implement the renew signal, emit it after successful commits, pass the instance, and add docs and tests as requested.
Out of Scope Changes check ✅ Passed The modified files are limited to the signal feature, its documentation, and tests, with no obvious unrelated changes.
Bug Fixes ✅ Passed Not a bug-fix PR; it adds new renew-signal functionality and includes deterministic tests that would fail if signal emission were removed.
Features ✅ Passed Issue #236 is labeled/assigned/on projects; docs and tests were added for the new signal, and coverage did not drop.
Changes ✅ Passed Docs add x509_renewed, tests cover CA/cert emissions, and no UI or breaking public API changes are evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issues/236-renew-signals

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Comment @coderabbitai help to get the list of available commands.

@coveralls

coveralls commented Jul 3, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 95.789% (+0.03%) from 95.758% — issues/236-renew-signals into master

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
django_x509/base/models.py (1)

879-887: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use send_robust() for the renewal signal.
A receiver exception from x509_renewed.send() will abort AbstractCa.renew() before the remaining certs are renewed.

Proposed fix
-        x509_renewed.send(sender=self.__class__, instance=self)
+        x509_renewed.send_robust(sender=self.__class__, instance=self)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@django_x509/base/models.py` around lines 879 - 887, In AbstractCa.renew, the
x509_renewed signal is sent with send(), which lets a receiver exception abort
the renewal flow; switch this to send_robust() so one failing receiver does not
stop remaining certificates from being renewed. Update the signal dispatch in
renew() after self.save() and keep the rest of the renewal logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.rst`:
- Around line 417-435: The x509_renewed signal documentation should warn
consumers not to log or serialize the full instance, since the Ca/Cert object
can include private_key and passphrase data. Update the Signals section for
django_x509.signals.x509_renewed to add a brief safety note near the instance
argument description, explicitly referencing Ca and Cert so downstream handlers
avoid accidental secret exposure.

---

Outside diff comments:
In `@django_x509/base/models.py`:
- Around line 879-887: In AbstractCa.renew, the x509_renewed signal is sent with
send(), which lets a receiver exception abort the renewal flow; switch this to
send_robust() so one failing receiver does not stop remaining certificates from
being renewed. Update the signal dispatch in renew() after self.save() and keep
the rest of the renewal logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 10553e06-68ab-4c72-ab53-6ebc0433655d

📥 Commits

Reviewing files that changed from the base of the PR and between 1abf8ce and 48490de.

📒 Files selected for processing (5)
  • README.rst
  • django_x509/base/models.py
  • django_x509/signals.py
  • django_x509/tests/test_ca.py
  • django_x509/tests/test_cert.py
📜 Review details
⏰ Context from checks skipped due to timeout. (16)
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
{README.rst,docs/**}

📄 CodeRabbit inference engine (AGENTS.md)

Update documentation when behavior, settings, public APIs, setup steps, or supported versions change

Files:

  • README.rst
**/*.py

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Place imports at the top of the file. Only defer imports when necessary (e.g., Django model imports inside functions or methods where the app registry is not yet ready)
Avoid unnecessary blank lines inside function and method bodies
Preserve object-level permissions and swappable model support when present
Be careful with certificate authority state, certificate revocation, serial numbers, extensions, private key handling, admin actions, and migrations
Watch for private key exposure, unsafe file paths, weak certificate options, invalid extensions, and secrets
Preserve validation around CA material, certificate material, revocation, downloads, and uploaded/generated files
Write comments and docstrings only when they explain why code is shaped a certain way. Put comments before the relevant code block instead of scattering them inside it

Files:

  • django_x509/signals.py
  • django_x509/base/models.py
  • django_x509/tests/test_cert.py
  • django_x509/tests/test_ca.py
{django_x509/tests/**,tests/**}

📄 CodeRabbit inference engine (AGENTS.md)

{django_x509/tests/**,tests/**}: Add or update tests for every behavior change
For bug fixes, write the regression test first, run it against the unfixed code, confirm it fails for the expected reason, then implement the fix
Prefer in-process tests so coverage tools can measure changed code

Files:

  • django_x509/tests/test_cert.py
  • django_x509/tests/test_ca.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T21:54:25.644Z
Learnt from: stktyagi
Repo: openwisp/django-x509 PR: 201
File: tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py:18-26
Timestamp: 2026-01-16T21:54:25.644Z
Learning: Do not review or suggest adding 224-bit ECDSA support in django-x509 code. SECP224R1 provides only 112 bits of cryptographic strength and is deprecated for asymmetric use by NIST SP 800-131A Rev.3 after 2030. Prefer and enforce the use of modern curves (e.g., SECP256R1/P-256) as the minimum. This guideline applies broadly to Python files in this repository; ensure reviews advocate using at least 256-bit curves and document any cryptographic choices accordingly.

Applied to files:

  • django_x509/signals.py
  • django_x509/base/models.py
  • django_x509/tests/test_cert.py
  • django_x509/tests/test_ca.py
🔇 Additional comments (4)
django_x509/signals.py (1)

1-6: LGTM!

django_x509/base/models.py (1)

20-20: LGTM!

django_x509/tests/test_ca.py (1)

13-16: LGTM!

Also applies to: 803-819

django_x509/tests/test_cert.py (1)

11-16: LGTM!

Also applies to: 882-890

Comment thread README.rst
@github-project-automation github-project-automation Bot moved this from To do (general) to In progress in OpenWISP Contributor's Board Jul 3, 2026
@stktyagi

stktyagi commented Jul 3, 2026

Copy link
Copy Markdown
Member

@coderabbitai resolve

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 3, 2026
@github-project-automation github-project-automation Bot moved this from In progress to Reviewer approved in OpenWISP Priorities for next releases Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

stktyagi
stktyagi previously approved these changes Jul 3, 2026
@openwisp-companion

Copy link
Copy Markdown

Proposed change log entry:

[feature] Added "x509_renewed" signal for Ca and Cert models #236

Added a new signal, `x509_renewed`, which is emitted when a Ca or Cert
instance is renewed. This signal can be used by other parts of the
application to react to certificate renewal events. The signal is sent
with the renewed instance as an argument.

Closes #236

self.validity_end = default_ca_validity_end()
self._generate()
self.save()
x509_renewed.send_robust(sender=self.__class__, instance=self)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pandafy this sends the signal while the DB transaction may be active, are we aware of this and is it on purpose right? This means the receivers have to execute when the transaction is committed. I'd leave a notes about this in the docs.

@pandafy
pandafy dismissed stale reviews from stktyagi and coderabbitai[bot] via 1f125d5 July 8, 2026 13:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
django_x509/base/models.py (1)

879-889: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a brief "why" comment for the deferred signal emission.

The deferred, commit-gated emission is non-obvious (it already prompted a reviewer question about whether this was intentional). A short comment explaining that the signal is fired after commit so receivers never see a state that could be rolled back would help future readers avoid re-litigating this.

📝 Suggested comment
         self._generate()
         self.save()
+        # Deferred until commit so receivers never observe a renewal that is
+        # later rolled back (see README "Signals" section for full contract).
         transaction.on_commit(
             lambda: x509_renewed.send_robust(sender=self.__class__, instance=self)
         )
As per coding guidelines, "Write comments and docstrings only when they explain why code is shaped a certain way. Put comments before the relevant code block instead of scattering them inside it."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@django_x509/base/models.py` around lines 879 - 889, Add a brief why-comment
in renew() right before the transaction.on_commit block, explaining that
x509_renewed is emitted only after the transaction commits so receivers never
observe a state that could still be rolled back. Keep the comment adjacent to
the deferred signal emission and reference the existing renew() flow and
x509_renewed.send_robust call so future readers understand the commit-gated
behavior.

Source: Coding guidelines

django_x509/tests/test_ca.py (1)

803-820: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing regression test for the rollback-discards-signal claim.

README now documents: "If the transaction is rolled back... the signal will not be sent at all," but neither this file nor test_cert.py has a test asserting x509_renewed is not sent when the surrounding transaction rolls back. Since this is a new, explicitly documented behavior, it's worth a dedicated regression test (e.g., wrap ca.renew() in transaction.atomic() and force a rollback via an exception, then assert the captured on-commit callback list is empty or the signal handler was never invoked).

🧪 Suggested test sketch
+    def test_x509_renewed_signal_not_sent_on_rollback(self):
+        ca = self._create_ca()
+        with catch_signal(x509_renewed) as handler:
+            with self.assertRaises(RuntimeError):
+                with transaction.atomic():
+                    ca.renew()
+                    raise RuntimeError("force rollback")
+            handler.assert_not_called()
As per path instructions, "Add or update tests for every behavior change."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@django_x509/tests/test_ca.py` around lines 803 - 820, Add a regression test
for the rollback behavior of x509_renewed around Ca.renew(): the current tests
only cover the committed path in test_x509_renewed_signal_sent, but not the
documented case where a transaction rollback prevents the signal from being
emitted. In django_x509/tests/test_ca.py (or the related cert tests if that is
where the transaction boundary is clearer), wrap ca.renew() in
transaction.atomic(), force a rollback with an exception, and assert that the
catch_signal(x509_renewed) handler was never called or that
captureOnCommitCallbacks(execute=False) stays empty.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@django_x509/base/models.py`:
- Around line 887-889: Capture and handle the return value from
x509_renewed.send_robust inside the transaction.on_commit callback instead of
discarding it. The issue is that receiver exceptions are being swallowed
silently, so update the lambda around x509_renewed.send_robust to inspect the
`(receiver, response)` results and log any exception responses with enough
context to identify the failing receiver and instance. Use the existing
x509_renewed signal and the on_commit callback in django_x509/base/models.py as
the place to add the logging.

---

Outside diff comments:
In `@django_x509/base/models.py`:
- Around line 879-889: Add a brief why-comment in renew() right before the
transaction.on_commit block, explaining that x509_renewed is emitted only after
the transaction commits so receivers never observe a state that could still be
rolled back. Keep the comment adjacent to the deferred signal emission and
reference the existing renew() flow and x509_renewed.send_robust call so future
readers understand the commit-gated behavior.

In `@django_x509/tests/test_ca.py`:
- Around line 803-820: Add a regression test for the rollback behavior of
x509_renewed around Ca.renew(): the current tests only cover the committed path
in test_x509_renewed_signal_sent, but not the documented case where a
transaction rollback prevents the signal from being emitted. In
django_x509/tests/test_ca.py (or the related cert tests if that is where the
transaction boundary is clearer), wrap ca.renew() in transaction.atomic(), force
a rollback with an exception, and assert that the catch_signal(x509_renewed)
handler was never called or that captureOnCommitCallbacks(execute=False) stays
empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 82eb461f-0c40-485a-a916-84ea925730a8

📥 Commits

Reviewing files that changed from the base of the PR and between a11588e and 1f125d5.

📒 Files selected for processing (4)
  • README.rst
  • django_x509/base/models.py
  • django_x509/tests/test_ca.py
  • django_x509/tests/test_cert.py
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Place imports at the top of the file. Only defer imports when necessary (e.g., Django model imports inside functions or methods where the app registry is not yet ready)
Avoid unnecessary blank lines inside function and method bodies
Preserve object-level permissions and swappable model support when present
Be careful with certificate authority state, certificate revocation, serial numbers, extensions, private key handling, admin actions, and migrations
Watch for private key exposure, unsafe file paths, weak certificate options, invalid extensions, and secrets
Preserve validation around CA material, certificate material, revocation, downloads, and uploaded/generated files
Write comments and docstrings only when they explain why code is shaped a certain way. Put comments before the relevant code block instead of scattering them inside it

Files:

  • django_x509/tests/test_cert.py
  • django_x509/tests/test_ca.py
  • django_x509/base/models.py
{django_x509/tests/**,tests/**}

📄 CodeRabbit inference engine (AGENTS.md)

{django_x509/tests/**,tests/**}: Add or update tests for every behavior change
For bug fixes, write the regression test first, run it against the unfixed code, confirm it fails for the expected reason, then implement the fix
Prefer in-process tests so coverage tools can measure changed code

Files:

  • django_x509/tests/test_cert.py
  • django_x509/tests/test_ca.py
{README.rst,docs/**}

📄 CodeRabbit inference engine (AGENTS.md)

Update documentation when behavior, settings, public APIs, setup steps, or supported versions change

Files:

  • README.rst
🧠 Learnings (1)
📚 Learning: 2026-01-16T21:54:25.644Z
Learnt from: stktyagi
Repo: openwisp/django-x509 PR: 201
File: tests/openwisp2/sample_x509/migrations/0003_alter_ca_key_length_alter_cert_key_length_and_more.py:18-26
Timestamp: 2026-01-16T21:54:25.644Z
Learning: Do not review or suggest adding 224-bit ECDSA support in django-x509 code. SECP224R1 provides only 112 bits of cryptographic strength and is deprecated for asymmetric use by NIST SP 800-131A Rev.3 after 2030. Prefer and enforce the use of modern curves (e.g., SECP256R1/P-256) as the minimum. This guideline applies broadly to Python files in this repository; ensure reviews advocate using at least 256-bit curves and document any cryptographic choices accordingly.

Applied to files:

  • django_x509/tests/test_cert.py
  • django_x509/tests/test_ca.py
  • django_x509/base/models.py
🔇 Additional comments (4)
django_x509/base/models.py (1)

12-12: LGTM!

README.rst (1)

417-449: LGTM!

django_x509/tests/test_ca.py (1)

13-16: LGTM!

Also applies to: 803-820

django_x509/tests/test_cert.py (1)

11-16: LGTM!

Also applies to: 882-891

Comment thread django_x509/base/models.py Outdated
Comment on lines +887 to +889
transaction.on_commit(
lambda: x509_renewed.send_robust(sender=self.__class__, instance=self)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Receiver failures from send_robust are silently discarded.

send_robust catches exceptions per-receiver and returns them as (receiver, response) tuples instead of raising, but the return value here isn't captured, so a failing receiver (e.g. a broken logging/workflow hook — exactly the use cases this signal targets) fails silently with no trace anywhere. Consider capturing the result and logging any exceptions.

🔍 Suggested fix
+        def _emit_renewed():
+            for receiver, response in x509_renewed.send_robust(
+                sender=self.__class__, instance=self
+            ):
+                if isinstance(response, Exception):
+                    logger.exception(
+                        "Error in x509_renewed receiver %r", receiver, exc_info=response
+                    )
+
-        transaction.on_commit(
-            lambda: x509_renewed.send_robust(sender=self.__class__, instance=self)
-        )
+        transaction.on_commit(_emit_renewed)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
transaction.on_commit(
lambda: x509_renewed.send_robust(sender=self.__class__, instance=self)
)
def _emit_renewed():
for receiver, response in x509_renewed.send_robust(
sender=self.__class__, instance=self
):
if isinstance(response, Exception):
logger.exception(
"Error in x509_renewed receiver %r", receiver, exc_info=response
)
transaction.on_commit(_emit_renewed)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@django_x509/base/models.py` around lines 887 - 889, Capture and handle the
return value from x509_renewed.send_robust inside the transaction.on_commit
callback instead of discarding it. The issue is that receiver exceptions are
being swallowed silently, so update the lambda around x509_renewed.send_robust
to inspect the `(receiver, response)` results and log any exception responses
with enough context to identify the failing receiver and instance. Use the
existing x509_renewed signal and the on_commit callback in
django_x509/base/models.py as the place to add the logging.

@github-project-automation github-project-automation Bot moved this from Reviewer approved to In progress in OpenWISP Priorities for next releases Jul 8, 2026
@nemesifier
nemesifier merged commit e259c9f into master Jul 8, 2026
23 checks passed
@nemesifier
nemesifier deleted the issues/236-renew-signals branch July 8, 2026 14:22
@github-project-automation github-project-automation Bot moved this from In progress to Done in OpenWISP Contributor's Board Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

[feature] Add renew signal for Ca and Cert objects

4 participants