Includes: - Odoo 19 framework (odoo/) - 27 custom EnCoach addons (new_project/custom_addons/) - encoach_core, encoach_api, encoach_lms_api, encoach_adaptive_api - encoach_exam, encoach_taxonomy, encoach_adaptive, encoach_assignment - encoach_ai, encoach_ai_grading, encoach_ai_generation, encoach_ai_media - encoach_courseware, encoach_communication, encoach_subscription - encoach_notification, encoach_approval, encoach_branding - encoach_classroom, encoach_registration, encoach_stats - encoach_faq, encoach_ticket, encoach_training, encoach_resources - encoach_adaptive_ai, encoach_sis - 21 OpenEduCat Enterprise modules (new_project/enterprise-19/) - 14 OpenEduCat Community modules (new_project/openeducat_erp-19.0/) - Configuration: odoo.conf, requirements.txt, scripts - 200+ REST API endpoints with JWT authentication - SRS and test documentation Made-with: Cursor
91 lines
2.9 KiB
Python
91 lines
2.9 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
import re
|
|
|
|
|
|
class SensitiveDataFilter(logging.Filter):
|
|
|
|
def __init__(self, sensitive_keys):
|
|
super().__init__()
|
|
if sensitive_keys is None:
|
|
self._sensitive_keys = set()
|
|
else:
|
|
self._sensitive_keys = sensitive_keys
|
|
self._compile_patterns()
|
|
|
|
def _compile_patterns(self):
|
|
"""Precompile regex patterns for all sensitive keys, matching double/single-quoted JSON
|
|
entries.
|
|
|
|
:return: None
|
|
"""
|
|
self._patterns = []
|
|
for key in self._sensitive_keys:
|
|
# 1st group: "<key>" or '<key>'
|
|
# 2nd group: The quote char for the value
|
|
pattern = re.compile(rf'([\'"]{key}[\'"])\s*:\s*([\'"])([^\'"]+)\2')
|
|
self._patterns.append(pattern)
|
|
|
|
def filter(self, record):
|
|
"""Override of `logging` to mask any sensitive data in record.args before the record is
|
|
emitted. Always returns True to allow the record through.
|
|
|
|
:return: True
|
|
:rtype: bool
|
|
"""
|
|
if len(self._patterns) != len(self._sensitive_keys): # If keys changed.
|
|
self._compile_patterns() # Recompile the patterns.
|
|
|
|
record.args = self._mask(record.args)
|
|
return True
|
|
|
|
def _mask(self, data):
|
|
"""Recursively mask dicts, iterables, and strings.
|
|
|
|
:param data: The data to mask.
|
|
:return: The masked data.
|
|
"""
|
|
if isinstance(data, dict):
|
|
masked_dict = {}
|
|
for k, v in data.items():
|
|
masked_dict[k] = "[REDACTED]" if k in self._sensitive_keys else self._mask(v)
|
|
return masked_dict
|
|
if isinstance(data, (list, tuple, set)):
|
|
cls = type(data)
|
|
return cls(self._mask(v) for v in data)
|
|
if isinstance(data, str):
|
|
return self._mask_string(data)
|
|
return data
|
|
|
|
def _mask_string(self, text):
|
|
"""Apply each pattern, replacing the value with [REDACTED].
|
|
|
|
:param str text: The string to mask.
|
|
:return: The masked string.
|
|
:rtype: str
|
|
"""
|
|
def replace(m):
|
|
# 1st group: "<key>" or '<key>'
|
|
# 2nd group: The quote char for the value
|
|
quote = m.group(2)
|
|
return f"{m.group(1)}: {quote}[REDACTED]{quote}"
|
|
|
|
for pattern in self._patterns:
|
|
text = re.sub(pattern, replace, text)
|
|
return text
|
|
|
|
|
|
def get_payment_logger(name, sensitive_keys=None):
|
|
"""Return a logger with a SensitiveDataFilter added if sensitive keys are provided.
|
|
|
|
:param str name: The name of the logger.
|
|
:param set sensitive_keys: The keys of the payment data that should not be logged.
|
|
:return: The logger.
|
|
:rtype: logging.Logger
|
|
"""
|
|
logger = logging.getLogger(name)
|
|
if sensitive_keys is not None:
|
|
logger.addFilter(SensitiveDataFilter(sensitive_keys))
|
|
return logger
|