msyavuz commented on code in PR #45996:
URL: https://github.com/apache/superset/pull/45996#discussion_r4494063487
##########
superset/utils/json.py:
##########
@@ -362,6 +363,99 @@ def get_masked_fields(
return masked
+# A single PEM block, possibly wrapped in newline escapes left over from JSON.
+_PEM_BLOCK = re.compile(
+ r"(?:\\[rn])*-----BEGIN ([A-Z]+(?: [A-Z]+)*)-----(.+)-----END
\1-----(?:\\[rn])*",
+ re.DOTALL,
+)
+_PEM_LINE_BREAKS = re.compile(r"\s|\\[rn]")
+
+
+def _pem_body(value: str) -> str | None:
+ """
+ Return the base64 body of a PEM block without line breaks, or ``None``.
+
+ Every PEM value ends with the same ``-----END <label>-----`` footer, so
only
+ the body tells two keys apart.
+ """
+ if match := _PEM_BLOCK.fullmatch(value):
+ return _PEM_LINE_BREAKS.sub("", match.group(2))
+ return None
+
+
+def _sensitive_string_values(
+ payload: dict[str, Any],
+ sensitive_fields: set[str],
+) -> dict[str, str]:
+ """
+ Map each concrete path whose value is a string to that value, sorted by
path.
+ """
+ if not isinstance(payload, dict):
+ return {}
+
+ values: dict[str, str] = {}
+ for json_path in sensitive_fields:
+ for match in parse(json_path).find(payload):
+ if isinstance(match.value, str):
+ values[f"$.{_render_jsonpath(match.full_path)}"] = match.value
+ return dict(sorted(values.items()))
+
+
+def get_sensitive_field_paths(
+ payload: dict[str, Any],
+ sensitive_fields: set[str],
+) -> list[str]:
+ """
+ Returns the concrete paths of the string values found at sensitive paths.
+
+ Wildcards expand per match and non-string values are skipped, so the result
+ lists exactly the fields :func:`get_sensitive_field_hints` would hint.
+
+ :param payload: The payload to inspect; anything but a dict yields no paths
+ :param sensitive_fields: The set of fields to look for, as JSONPath
expressions
+ :returns: JSONPath expressions sorted by path
+ """
+ return list(_sensitive_string_values(payload, sensitive_fields))
+
+
+def get_sensitive_field_hints(
+ payload: dict[str, Any],
+ sensitive_fields: set[str],
+ length: int,
+) -> list[dict[str, Any]]:
+ """
+ Returns a hint for each string value found at a sensitive path.
+
+ A hint holds the path, the length of the trimmed value and, when the value
is at
+ least ``4 * length`` characters long, its last ``length`` characters. That
caps
+ what is revealed at 25% of the value; shorter values only report their
length.
+ For a PEM block, the suffix and the cap apply to its base64 body.
+
+ :param payload: The payload to inspect
+ :param sensitive_fields: The set of fields to hint, as JSONPath expressions
+ :param length: The number of trailing characters to reveal; ``<= 0``
disables hints
+ :returns: Hints sorted by path, as ``{"path", "suffix", "length"}`` dicts
+ """
+ if length <= 0:
+ return []
+
+ hints: list[dict[str, Any]] = []
+ for path, stored in _sensitive_string_values(payload,
sensitive_fields).items():
+ value = stored.strip()
+ body = _pem_body(value)
+ revealable = value if body is None else body
+ hints.append(
+ {
+ "path": path,
+ "suffix": (
+ revealable[-length:] if len(revealable) >= 4 * length else
None
+ ),
+ "length": len(value),
Review Comment:
`length` is returned even when the suffix is withheld, so a short password
still leaks its exact length. Should this be null below the 4x threshold, or
should password-labelled paths be excluded from hints?
##########
superset/utils/json.py:
##########
@@ -362,6 +363,99 @@ def get_masked_fields(
return masked
+# A single PEM block, possibly wrapped in newline escapes left over from JSON.
+_PEM_BLOCK = re.compile(
+ r"(?:\\[rn])*-----BEGIN ([A-Z]+(?: [A-Z]+)*)-----(.+)-----END
\1-----(?:\\[rn])*",
+ re.DOTALL,
+)
+_PEM_LINE_BREAKS = re.compile(r"\s|\\[rn]")
+
+
+def _pem_body(value: str) -> str | None:
+ """
+ Return the base64 body of a PEM block without line breaks, or ``None``.
+
+ Every PEM value ends with the same ``-----END <label>-----`` footer, so
only
+ the body tells two keys apart.
+ """
+ if match := _PEM_BLOCK.fullmatch(value):
+ return _PEM_LINE_BREAKS.sub("", match.group(2))
+ return None
+
+
+def _sensitive_string_values(
+ payload: dict[str, Any],
+ sensitive_fields: set[str],
+) -> dict[str, str]:
+ """
+ Map each concrete path whose value is a string to that value, sorted by
path.
+ """
+ if not isinstance(payload, dict):
+ return {}
+
+ values: dict[str, str] = {}
+ for json_path in sensitive_fields:
+ for match in parse(json_path).find(payload):
+ if isinstance(match.value, str):
+ values[f"$.{_render_jsonpath(match.full_path)}"] = match.value
+ return dict(sorted(values.items()))
+
+
+def get_sensitive_field_paths(
+ payload: dict[str, Any],
+ sensitive_fields: set[str],
+) -> list[str]:
+ """
+ Returns the concrete paths of the string values found at sensitive paths.
+
+ Wildcards expand per match and non-string values are skipped, so the result
+ lists exactly the fields :func:`get_sensitive_field_hints` would hint.
+
+ :param payload: The payload to inspect; anything but a dict yields no paths
+ :param sensitive_fields: The set of fields to look for, as JSONPath
expressions
+ :returns: JSONPath expressions sorted by path
+ """
+ return list(_sensitive_string_values(payload, sensitive_fields))
+
+
+def get_sensitive_field_hints(
+ payload: dict[str, Any],
+ sensitive_fields: set[str],
+ length: int,
+) -> list[dict[str, Any]]:
+ """
+ Returns a hint for each string value found at a sensitive path.
+
+ A hint holds the path, the length of the trimmed value and, when the value
is at
+ least ``4 * length`` characters long, its last ``length`` characters. That
caps
+ what is revealed at 25% of the value; shorter values only report their
length.
+ For a PEM block, the suffix and the cap apply to its base64 body.
+
+ :param payload: The payload to inspect
+ :param sensitive_fields: The set of fields to hint, as JSONPath expressions
+ :param length: The number of trailing characters to reveal; ``<= 0``
disables hints
+ :returns: Hints sorted by path, as ``{"path", "suffix", "length"}`` dicts
+ """
+ if length <= 0:
+ return []
+
+ hints: list[dict[str, Any]] = []
+ for path, stored in _sensitive_string_values(payload,
sensitive_fields).items():
+ value = stored.strip()
+ body = _pem_body(value)
+ revealable = value if body is None else body
+ hints.append(
+ {
+ "path": path,
+ "suffix": (
+ revealable[-length:] if len(revealable) >= 4 * length else
None
Review Comment:
PEM bodies usually end in `=`/`==`, so 1-2 of the revealed chars are
padding. Strip trailing `=` before slicing.
##########
superset/databases/api.py:
##########
@@ -390,6 +404,58 @@ def get_connection(self, pk: int) -> Response:
except SupersetException as ex:
return self.response(ex.status, message=ex.message)
+ @expose("/<int:pk>/encrypted_extra_hints/", methods=("GET",))
+ @protect()
+ @safe
+ @statsd_metrics
+ @event_logger.log_this_with_context(
+ action=lambda self, *args, **kwargs: (
+ f"{self.__class__.__name__}.encrypted_extra_hints"
+ ),
+ log_to_statsd=False,
+ include_request_data=False,
+ )
+ def encrypted_extra_hints(self, pk: int, **kwargs: Any) -> Response:
Review Comment:
`**kwargs` is never read.
##########
superset/config.py:
##########
@@ -3260,6 +3260,12 @@ def EMAIL_HEADER_MUTATOR( # pylint:
disable=invalid-name,unused-argument # noq
# Typically these should not be allowed.
PREVENT_UNSAFE_DB_CONNECTIONS = True
+# Number of trailing characters of each masked `encrypted_extra` field that
users
+# who can edit a database may reveal as a hint, together with the value's
length.
+# Never more than 25% of a value is revealed: shorter values only report their
+# length. 0 disables the hints.
+ENCRYPTED_EXTRA_HINT_CHARS = 0
Review Comment:
New config has no docs entry.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]