Skip to content

Commit c189c6d

Browse files
aap-platform-services-cicd-bot-saAegis-botjeffh-osscursoragent
authored
[AAP-76852] Fix: catch InvalidToken in from_db() methods to log CRITICAL before crash (#1044)
## Summary When `SECRET_KEY` changes (e.g., after `podman system reset` destroys the Podman secret and the installer regenerates a new one), all existing encrypted database rows become undecryptable. Two `from_db()` methods in `django-ansible-base` called `decrypt_string()` with no error handling for `cryptography.fernet.InvalidToken`, causing unhandled exceptions that crash the service on startup with no actionable diagnostic output. ## Root Cause - **`AbstractCommonModel.from_db()`** (`ansible_base/lib/abstract_models/common.py:197-205`) — iterates over `encrypted_fields` calling `ansible_encryption.decrypt_string()` with no guard. Affects `ServiceKey.secret`, `AuthenticatorUser.extra_data`, and any other model with `encrypted_fields`. - **`Authenticator.from_db()`** (`ansible_base/authentication/models/authenticator.py:76-90`) — has a `try/except ImportError` but does not catch `InvalidToken`. Encrypted authenticator config fields (LDAP `BIND_PASSWORD`, OIDC `SECRET`, SAML `SP_PRIVATE_KEY`) cause an unhandled crash. Per the staff engineering decision (June 18): catch `InvalidToken`, log CRITICAL with model/PK/recovery context, then re-raise. This preserves the crash behaviour while providing actionable diagnostic output before the system fails. ## Changes - `ansible_base/lib/abstract_models/common.py`: Wrap each `decrypt_string()` call in the `encrypted_fields` loop in `from_db()` with `try/except InvalidToken`. Log CRITICAL with model class name, field name, and PK. Re-raise unchanged. - `ansible_base/authentication/models/authenticator.py`: Add `except InvalidToken` clause in `from_db()` alongside the existing `except ImportError`. Log CRITICAL with authenticator name and type. Re-raise unchanged. Add `from cryptography.fernet import InvalidToken` import and a module-level logger. - `test_app/tests/lib/abstract_models/test_common.py`: Add `test_from_db_invalid_token_logs_and_reraises` — patches `decrypt_string` to raise `InvalidToken`, asserts re-raise and CRITICAL log. - `test_app/tests/authentication/models/test_authenticator.py`: Add `test_authenticator_from_db_invalid_token_logs_and_reraises` — same pattern for the LDAP authenticator fixture. ## Risk Assessment - **Confidence:** high - **Risk:** low — exception is re-raised so runtime behaviour is unchanged; the only addition is the CRITICAL log message before the crash; no data is modified; no migrations; no API changes - **Scope:** 4 files changed ## Testing - [ ] Unit tests pass - [ ] Regression test added (test patches decrypt_string to raise InvalidToken, asserts re-raise + CRITICAL log) - [ ] Lint checks pass ## JIRA - Ticket: [AAP-76852](https://issues.redhat.com/browse/AAP-76852) --- *Assisted-by: Debuggernaut (claude-opus-4-6) <noreply@redhat.com> | Review carefully before merging.* [AAP-76852]: https://redhat.atlassian.net/browse/AAP-76852?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved error handling when encrypted configuration or model data cannot be decrypted. * Added critical logging with relevant details to make decryption failures easier to diagnose. * Prevented invalid encrypted data from being loaded silently by preserving the original error. * **Tests** * Added coverage confirming decryption failures are logged and correctly re-raised. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Aegis-bot <bot@ambient-code.local> Co-authored-by: jeffh-oss <jheadley@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f313c89 commit c189c6d

4 files changed

Lines changed: 75 additions & 1 deletion

File tree

ansible_base/authentication/models/authenticator.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import logging
2+
3+
from cryptography.fernet import InvalidToken
14
from django.db.models import SET_NULL, ForeignKey, JSONField, fields
25

36
from ansible_base.authentication.authenticator_plugins.utils import generate_authenticator_slug, get_authenticator_plugin
47
from ansible_base.lib.abstract_models.common import UniqueNamedCommonModel
58
from ansible_base.lib.utils.models import prevent_search
69

10+
logger = logging.getLogger('ansible_base.authentication.models.authenticator')
11+
712

813
def get_next_authenticator_order():
914
"""
@@ -86,6 +91,16 @@ def from_db(cls, db, field_names, values):
8691
except ImportError:
8792
# A log message will already be displayed if this fails
8893
pass
94+
except InvalidToken:
95+
logger.critical(
96+
"Failed to decrypt configuration field on Authenticator(name=%r, type=%r): "
97+
"the SECRET_KEY may have changed. "
98+
"Restore the original SECRET_KEY that encrypted this database, then restart. "
99+
"Re-raising to prevent startup with corrupted authenticator configuration.",
100+
instance.name,
101+
instance.type,
102+
)
103+
raise
89104

90105
return instance
91106

ansible_base/lib/abstract_models/common.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,24 @@ def save(self, *args, **kwargs):
196196

197197
@classmethod
198198
def from_db(self, db, field_names, values):
199+
from cryptography.fernet import InvalidToken
200+
199201
instance = super().from_db(db, field_names, values)
200202

201203
for field in self.encrypted_fields:
202204
field_value = getattr(instance, field, None)
203-
setattr(instance, field, ansible_encryption.decrypt_string(field_value))
205+
try:
206+
setattr(instance, field, ansible_encryption.decrypt_string(field_value))
207+
except InvalidToken:
208+
logger.critical(
209+
"Failed to decrypt field %r on %s (pk=%r): the SECRET_KEY may have changed. "
210+
"Restore the original SECRET_KEY that encrypted this database, then restart. "
211+
"Re-raising to prevent the model from loading with corrupted data.",
212+
field,
213+
self.__name__,
214+
getattr(instance, 'pk', None),
215+
)
216+
raise
204217

205218
return instance
206219

test_app/tests/authentication/models/test_authenticator.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import logging
12
from unittest import mock
3+
from unittest.mock import patch
24

35
import pytest
46

@@ -47,3 +49,23 @@ def test_dupe_slug(ldap_authenticator):
4749

4850
dupe.save()
4951
assert dupe.slug != ldap_slug, "authenticator slugs should be unique"
52+
53+
54+
@pytest.mark.django_db
55+
def test_authenticator_from_db_invalid_token_logs_and_reraises(ldap_authenticator, caplog):
56+
"""Authenticator.from_db() must log CRITICAL and re-raise InvalidToken.
57+
58+
Simulates a SECRET_KEY change making encrypted authenticator configuration
59+
fields (e.g. LDAP BIND_PASSWORD) undecryptable -- part of AAP-76852.
60+
"""
61+
from cryptography.fernet import InvalidToken
62+
63+
with patch("ansible_base.lib.utils.encryption.ansible_encryption.decrypt_string", side_effect=InvalidToken):
64+
with caplog.at_level(logging.CRITICAL, logger="ansible_base.authentication.models.authenticator"):
65+
with pytest.raises(InvalidToken):
66+
Authenticator.objects.get(pk=ldap_authenticator.pk)
67+
68+
critical_records = [r for r in caplog.records if r.levelno == logging.CRITICAL]
69+
assert critical_records, "Expected at least one CRITICAL log record"
70+
assert any("SECRET_KEY" in r.message for r in critical_records), "Expected CRITICAL log to mention SECRET_KEY"
71+
assert any(ldap_authenticator.name in r.message for r in critical_records), "Expected CRITICAL log to include the authenticator name"

test_app/tests/lib/abstract_models/test_common.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,27 @@ def test_compare_data_types_after_decryption(input):
366366
decryptor = Fernet256()
367367
results = decryptor.decrypt_string(decryptor.encrypt_string(input))
368368
assert type(results) is type(input)
369+
370+
371+
@pytest.mark.django_db
372+
def test_from_db_invalid_token_logs_and_reraises(caplog):
373+
"""AbstractCommonModel.from_db() must log CRITICAL and re-raise InvalidToken.
374+
375+
Simulates a SECRET_KEY change that leaves encrypted_fields rows unreadable
376+
-- the DAB side of AAP-76852.
377+
"""
378+
import logging
379+
380+
from cryptography.fernet import InvalidToken
381+
382+
model = EncryptionModel.objects.create(testing1='sensitive_value')
383+
384+
with patch("ansible_base.lib.utils.encryption.ansible_encryption.decrypt_string", side_effect=InvalidToken):
385+
with caplog.at_level(logging.CRITICAL, logger="ansible_base.lib.abstract_models.common"):
386+
with pytest.raises(InvalidToken):
387+
EncryptionModel.objects.get(pk=model.pk)
388+
389+
critical_records = [r for r in caplog.records if r.levelno == logging.CRITICAL]
390+
assert critical_records, "Expected at least one CRITICAL log record"
391+
assert any("SECRET_KEY" in r.message for r in critical_records), "Expected CRITICAL log to mention SECRET_KEY"
392+
assert any(str(model.pk) in r.message for r in critical_records), "Expected CRITICAL log to include the model pk"

0 commit comments

Comments
 (0)