Skip to content

Commit 7f3a1e1

Browse files
committed
feat: Adding support for redacted environment variable values through openjd_redacted_env
Signed-off-by: Brian Axelson <86568017+baxeaz@users.noreply.github.com>
1 parent 79cd8f3 commit 7f3a1e1

File tree

8 files changed

+1466
-131
lines changed

8 files changed

+1466
-131
lines changed

src/openjd/sessions/_action_filter.py

Lines changed: 232 additions & 49 deletions
Large diffs are not rendered by default.

src/openjd/sessions/_session.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ def __init__(
318318
callback: Optional[SessionCallbackType] = None,
319319
os_env_vars: Optional[dict[str, str]] = None,
320320
session_root_directory: Optional[Path] = None,
321+
enabled_extensions: Optional[list[str]] = None,
321322
):
322323
"""
323324
Arguments:
@@ -389,9 +390,14 @@ def __init__(
389390
)
390391
self._reset_action_state()
391392

393+
# Store the enabled extensions
394+
self._enabled_extensions = enabled_extensions or []
395+
392396
# Set up our logging hook & callback
393397
self._log_filter = ActionMonitoringFilter(
394-
session_id=self._session_id, callback=self._action_log_filter_callback
398+
session_id=self._session_id,
399+
callback=self._action_log_filter_callback,
400+
enabled_extensions=self._enabled_extensions,
395401
)
396402
LOG.addFilter(self._log_filter)
397403
self._logger = LoggerAdapter(LOG, extra={"session_id": self._session_id})

src/openjd/sessions/_subprocess.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ._logging import LoggerAdapter, LogContent, LogExtraInfo
2020
from ._os_checker import is_linux, is_posix, is_windows
2121
from ._session_user import PosixSessionUser, WindowsSessionUser, SessionUser
22+
from ._action_filter import pre_redact_command
2223

2324
if is_windows(): # pragma: nocover
2425
from subprocess import CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW # type: ignore
@@ -274,11 +275,15 @@ def _start_subprocess(self) -> Optional[Popen]:
274275
# https://docs.python.org/2/library/subprocess.html#subprocess.CREATE_NEW_PROCESS_GROUP
275276
popen_args["creationflags"] = CREATE_NEW_PROCESS_GROUP
276277

278+
# Get the command string for logging
277279
cmd_line_for_logger: str
278280
if is_posix():
279281
cmd_line_for_logger = shlex.join(command)
280282
else:
281-
cmd_line_for_logger = list2cmdline(self._args)
283+
# On Windows, we need to handle redaction in command strings
284+
cmd_line = list2cmdline(self._args)
285+
# Pre-redact any sensitive information in the command string
286+
cmd_line_for_logger = pre_redact_command(cmd_line)
282287
self._logger.info(
283288
"Running command %s",
284289
cmd_line_for_logger,

test/openjd/sessions/conftest.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
from logging import INFO, getLogger
77
from logging.handlers import QueueHandler
88
from queue import Empty, SimpleQueue
9-
from typing import Generator
9+
from typing import Generator, Optional
10+
from hashlib import sha256
11+
from unittest.mock import MagicMock
1012
import pytest
1113

1214
from openjd.sessions import PosixSessionUser, WindowsSessionUser, BadCredentialsException
1315
from openjd.sessions._os_checker import is_posix, is_windows
1416
from openjd.sessions._logging import LoggerAdapter
17+
from openjd.sessions._action_filter import ActionMonitoringFilter
1518

1619
if is_windows():
1720
from openjd.sessions._win32._helpers import ( # type: ignore
@@ -55,15 +58,102 @@ def pytest_collection_modifyitems(config, items):
5558
config.option.markexpr = mark_expr
5659

5760

61+
def create_unique_logger_name(prefix: str = "", seed: Optional[str] = None) -> str:
62+
"""Create a unique logger name using a hash to avoid collisions.
63+
64+
Args:
65+
prefix: Optional prefix for the logger name
66+
seed: Optional seed string to use for generating the hash
67+
68+
Returns:
69+
A unique logger name
70+
"""
71+
if seed:
72+
h = sha256()
73+
h.update(seed.encode("utf-8"))
74+
suffix = h.hexdigest()[0:32]
75+
else:
76+
charset = string.ascii_letters + string.digits
77+
suffix = "".join(random.choices(charset, k=32))
78+
79+
return f"{prefix}{suffix}"
80+
81+
5882
def build_logger(handler: QueueHandler) -> LoggerAdapter:
59-
charset = string.ascii_letters + string.digits + string.punctuation
60-
name_suffix = "".join(random.choices(charset, k=32))
83+
"""Build a logger for testing purposes.
84+
85+
Args:
86+
handler: The queue handler to attach to the logger
87+
88+
Returns:
89+
A configured LoggerAdapter
90+
"""
91+
name_suffix = create_unique_logger_name()
6192
log = getLogger(".".join((__name__, name_suffix)))
6293
log.setLevel(INFO)
6394
log.addHandler(handler)
6495
return LoggerAdapter(log, extra=dict())
6596

6697

98+
def setup_action_filter_test(
99+
queue_handler: QueueHandler,
100+
session_id: str = "foo",
101+
callback: Optional[MagicMock] = None,
102+
suppress_filtered: bool = False,
103+
enabled_extensions: Optional[list[str]] = None,
104+
) -> tuple[LoggerAdapter, ActionMonitoringFilter, MagicMock]:
105+
"""Set up a test environment for testing ActionMonitoringFilter.
106+
107+
This helper method creates a unique logger name, sets up the ActionMonitoringFilter,
108+
and configures the logger with the filter.
109+
110+
Args:
111+
queue_handler: The QueueHandler to attach to the logger
112+
session_id: The session ID to use for the filter
113+
callback: Optional mock callback to use for the filter
114+
suppress_filtered: Whether to suppress filtered messages
115+
enabled_extensions: Optional list of extensions to enable
116+
117+
Returns:
118+
A tuple containing (logger_adapter, action_filter, callback_mock)
119+
120+
Note:
121+
This helper works for most tests, but for tests that need to verify specific
122+
callback behavior with redacted values, it's better to create the filter and
123+
logger directly in the test. This is because when multiple filters are applied
124+
to the same log message (which can happen when running multiple tests), the
125+
redaction can happen before the callback is invoked, resulting in the callback
126+
receiving redacted values instead of the original values.
127+
"""
128+
# Create a unique logger name WITHOUT using the message as seed
129+
# This ensures each test gets a truly unique logger name
130+
logger_name = create_unique_logger_name(prefix="action_filter_")
131+
132+
# Create a mock callback if one wasn't provided
133+
if callback is None:
134+
callback = MagicMock()
135+
136+
# Create the filter directly with the provided parameters
137+
action_filter = ActionMonitoringFilter(
138+
session_id=session_id,
139+
callback=callback,
140+
suppress_filtered=suppress_filtered,
141+
enabled_extensions=enabled_extensions,
142+
)
143+
144+
# Set up the logger
145+
log = getLogger(".".join((__name__, logger_name)))
146+
log.setLevel(INFO)
147+
log.addHandler(queue_handler)
148+
log.addFilter(action_filter)
149+
150+
# Create and return the logger adapter with the session_id
151+
# This is critical for the filter to work properly
152+
logger_adapter = LoggerAdapter(log, extra={"session_id": session_id})
153+
154+
return logger_adapter, action_filter, callback
155+
156+
67157
def collect_queue_messages(queue: SimpleQueue) -> list[str]:
68158
"""Extract the text of messages from a SimpleQueue containing LogRecords"""
69159
messages: list[str] = []

0 commit comments

Comments
 (0)