fix: Enhance PII checks to fail on new/existing PII fields in no_pii annotated models - #559
Conversation
| if self._pii_terms_cache is not None: | ||
| return | ||
| cfg = self.linter.config | ||
| raw_terms = getattr(cfg, "pii_terms", ["email", "username"]) |
There was a problem hiding this comment.
Add None as default and validate if its None then throw an error.
|
|
||
| # Message definitions | ||
| msgs = { | ||
| ("W%d33" % BASE_ID): ( |
There was a problem hiding this comment.
Every annotation-compliance check in annotations_check.py (feature-toggle-needs-doc, toggle-no-name, setting-boolean-default-value, etc.) is registered as an E (error), not W. Since this enforces the same category of thing — OEP-30 compliance intended to fail CI — should this be E7633 for consistency, rather than W7633?
|
|
||
| # Message definitions | ||
| msgs = { | ||
| ("W%d33" % BASE_ID): ( |
There was a problem hiding this comment.
Can we confirm W7633 doesn't collide with an existing message ID under BASE_ID in another checker (range_check.py, super_check.py, getattr_check.py, etc.)?
There was a problem hiding this comment.
Yes, I confirmed that W7633 does not collide with any existing message ID under BASE_ID across the other checkers (range_check.py, super_check.py, getattr_check.py, etc.).
I’ll keep W7633 as-is since the E76xx series in annotations_check.py is reserved for OEP toggle/setting compliance, while W7630/W7633 belong to the PII checker family.
| pii_fields = self._collect_pii_fields(node) | ||
| for field_name, field_node in pii_fields: | ||
| self.add_message( | ||
| "pii-invalid-no-pii-annotation", |
There was a problem hiding this comment.
Elsewhere in the codebase (TOGGLE_NOT_ANNOTATED_MESSAGE_ID = "feature-toggle-needs-doc"), the message symbol is a class-level constant referenced in both msgs and add_message(). Here "pii-invalid-no-pii-annotation" is inlined as a bare string in both places — minor, but worth aligning with the existing pattern for consistency.
|
|
||
|
|
||
| @check_visitors | ||
| class PiiAnnotationChecker(BaseChecker): |
There was a problem hiding this comment.
annotations_check.py already has AnnotationBaseChecker built on code_annotations.find_static.StaticSearch/AnnotationConfig for parsing .. toggle_name:-style annotations, and .. no_pii:/.. pii: follow the same annotation grammar per OEP-30. Could this checker subclass AnnotationBaseChecker instead of using custom regexes + a 10-line source lookahead? The hand-rolled scan is fragile (e.g. two classes declared close together could bleed a no_pii comment across class boundaries) and duplicates parsing logic this repo already solves correctly elsewhere.
There was a problem hiding this comment.
Before the changes, AnnotationBaseChecker / StaticSearch was mainly designed for multi-field annotation groups and loads YAML from code_annotations/contrib/config/. There is no pii_annotations.yaml there.
Since no_pii is a standalone annotation rather than an annotation group, iter_groups() doesn't cover it. Also, StaticSearch only returns the file, line number, and annotation, so we'd still need additional logic to determine which ClassDef owns that annotation.
So using it would add complexity without solving the core issue more directly than the current approach.
| if self._is_pii_name(child.target.name): | ||
| found.append((child.target.name, child)) | ||
|
|
||
| # Instance attributes set inside methods: ``self.email = ...`` |
There was a problem hiding this comment.
What about self.email: str = ...?
There was a problem hiding this comment.
Good catch. We only handled self.email = ... Assign in methods; self.email: str = ... is AnnAssign and was a gap. I’ve added AnnAssign handling for instance attrs and a regression test test_no_pii_annotated_instance_attr_in_method_flagged.
| """Parse pii-terms config on first call within a module.""" | ||
| if self._parsed_pii_terms is not None: | ||
| return | ||
| cfg = self.linter.config |
There was a problem hiding this comment.
I would recommend a better variable name than cfg
There was a problem hiding this comment.
Renamed cfg to linter_config.
|
|
||
| Substring match of any pii-term inside *name* → PII. | ||
| """ | ||
| lower = name.lower() |
There was a problem hiding this comment.
I would recommend a better variable name than lower
There was a problem hiding this comment.
Updated for readability: renamed lower to normalized_name.
| def _class_has_no_pii_annotation(self, node): | ||
| """ | ||
| Return True if the class docstring carries a ``.. no_pii:`` annotation. | ||
| """ | ||
| return self._docstring_has_no_pii(node) |
There was a problem hiding this comment.
_class_has_no_pii_annotation just calls _docstring_has_no_pii with no extra logic, So can we just change line number 113 to call _docstring_has_no_pii directly and delete this (_class_has_no_pii_annotation) wrapper?
There was a problem hiding this comment.
Good suggestion. I inlined the call to _docstring_has_no_pii(node) in visit_classdef and removed _class_has_no_pii_annotation since it had no additional logic.
bmtcril
left a comment
There was a problem hiding this comment.
Thanks for this, seems reasonable to me after a rebase!
Description:
This PR introduces a new pylint checker
(pii-invalid-no-pii-annotation / W7633)to ensure that Django models explicitly marked as not containing PII remain compliant with OEP-0030. It enforces this compliance by failing CI if any model annotated with.. no_pii:contains fields that match known PII terms. This ensures that existing incorrect annotations are audited and corrected, and prevents developers from accidentally merging new sensitive fields into these models without proper review and annotation updates.Solution:
.. no_pii:... no_pii:model is found, the checker scans its class attributes and init assignments. If any tentative PII field is detected (matching the configurable pii-terms), it raises a W7633 warning. This forces the developer to either rename the non-sensitive field or update the model's metadata to.. pii:.PII detection while testing on CI:
Inline comment example to bypass linter check (False positives):
Private JIRA Link:
BOMS-587