Skip to content

fix: Enhance PII checks to fail on new/existing PII fields in no_pii annotated models - #559

Open
Akanshu-2u wants to merge 10 commits into
openedx:masterfrom
Akanshu-2u:aaich/BOMS-587-annotation
Open

fix: Enhance PII checks to fail on new/existing PII fields in no_pii annotated models#559
Akanshu-2u wants to merge 10 commits into
openedx:masterfrom
Akanshu-2u:aaich/BOMS-587-annotation

Conversation

@Akanshu-2u

@Akanshu-2u Akanshu-2u commented Jul 23, 2026

Copy link
Copy Markdown

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:

  • Created a new Pylint plugin: Added a checker that specifically targets concrete Django models.
  • Annotation validation: The checker parses model docstrings and preceding comment lines to identify classes explicitly annotated with .. no_pii: .
  • Strict field checking: When a .. 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):

    # pylint: disable-next=pii-invalid-no-pii-annotation  # field does not store user PII data, safe under OEP-30
    email = models.BooleanField(default=False, null=False, blank=False)

Private JIRA Link:

BOMS-587

@Akanshu-2u Akanshu-2u changed the title fix: Enhance PII checks to fail on new PII fields in no_pii annotated models fix: Enhance PII checks to fail on new/existing PII fields in no_pii annotated models Jul 27, 2026
Comment thread edx_lint/pylint/pii_annotation_check.py Outdated
if self._pii_terms_cache is not None:
return
cfg = self.linter.config
raw_terms = getattr(cfg, "pii_terms", ["email", "username"])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Add None as default and validate if its None then throw an error.


# Message definitions
msgs = {
("W%d33" % BASE_ID): (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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): (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.)?

Comment thread edx_lint/pylint/pii_annotation_check.py Outdated
pii_fields = self._collect_pii_fields(node)
for field_name, field_node in pii_fields:
self.add_message(
"pii-invalid-no-pii-annotation",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

if self._is_pii_name(child.target.name):
found.append((child.target.name, child))

# Instance attributes set inside methods: ``self.email = ...``

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What about self.email: str = ...?

"""Parse pii-terms config on first call within a module."""
if self._parsed_pii_terms is not None:
return
cfg = self.linter.config

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I would recommend a better variable name than cfg


Substring match of any pii-term inside *name* → PII.
"""
lower = name.lower()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I would recommend a better variable name than lower

Comment on lines +227 to +231
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_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?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants