Add sensitive variable redaction in history
This commit is contained in:
parent
d9643f689f
commit
cdd2c180f4
@ -80,6 +80,7 @@ class HistoryEntry:
|
|||||||
response: Optional[HttpResponse]
|
response: Optional[HttpResponse]
|
||||||
error: Optional[str] # Error message if request failed
|
error: Optional[str] # Error message if request failed
|
||||||
id: str = None # Unique identifier for the entry
|
id: str = None # Unique identifier for the entry
|
||||||
|
has_redacted_variables: bool = False # True if sensitive variables were redacted
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""Generate UUID if id not provided."""
|
"""Generate UUID if id not provided."""
|
||||||
@ -94,7 +95,8 @@ class HistoryEntry:
|
|||||||
'timestamp': self.timestamp,
|
'timestamp': self.timestamp,
|
||||||
'request': self.request.to_dict(),
|
'request': self.request.to_dict(),
|
||||||
'response': self.response.to_dict() if self.response else None,
|
'response': self.response.to_dict() if self.response else None,
|
||||||
'error': self.error
|
'error': self.error,
|
||||||
|
'has_redacted_variables': self.has_redacted_variables
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -105,7 +107,8 @@ class HistoryEntry:
|
|||||||
timestamp=data['timestamp'],
|
timestamp=data['timestamp'],
|
||||||
request=HttpRequest.from_dict(data['request']),
|
request=HttpRequest.from_dict(data['request']),
|
||||||
response=HttpResponse.from_dict(data['response']) if data.get('response') else None,
|
response=HttpResponse.from_dict(data['response']) if data.get('response') else None,
|
||||||
error=data.get('error')
|
error=data.get('error'),
|
||||||
|
has_redacted_variables=data.get('has_redacted_variables', False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -127,3 +127,118 @@ class VariableSubstitution:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return new_request, all_undefined
|
return new_request, all_undefined
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def substitute_excluding_variables(text: str, variables: Dict[str, str], excluded_vars: List[str]) -> Tuple[str, List[str]]:
|
||||||
|
"""
|
||||||
|
Replace {{variable_name}} with values, but skip excluded variables (leave as {{var}}).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The text containing variable placeholders
|
||||||
|
variables: Dictionary mapping variable names to their values
|
||||||
|
excluded_vars: List of variable names to skip (leave as placeholders)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (substituted_text, list_of_undefined_variables)
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return text, []
|
||||||
|
|
||||||
|
undefined_vars = []
|
||||||
|
|
||||||
|
def replace_var(match):
|
||||||
|
var_name = match.group(1)
|
||||||
|
|
||||||
|
# Skip excluded variables - leave as {{var_name}}
|
||||||
|
if var_name in excluded_vars:
|
||||||
|
return match.group(0) # Return original {{var_name}}
|
||||||
|
|
||||||
|
# Substitute non-excluded variables
|
||||||
|
if var_name in variables:
|
||||||
|
value = variables[var_name]
|
||||||
|
# Check if value is empty/None - treat as undefined for warning purposes
|
||||||
|
if not value:
|
||||||
|
undefined_vars.append(var_name)
|
||||||
|
return ""
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
# Variable not defined - track it and replace with empty string
|
||||||
|
undefined_vars.append(var_name)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text)
|
||||||
|
return substituted, undefined_vars
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def substitute_request_excluding_sensitive(
|
||||||
|
request: HttpRequest,
|
||||||
|
environment: Environment,
|
||||||
|
sensitive_variables: List[str]
|
||||||
|
) -> Tuple[HttpRequest, Set[str], bool]:
|
||||||
|
"""
|
||||||
|
Substitute variables in request, but exclude sensitive variables (leave as {{var}}).
|
||||||
|
|
||||||
|
This is useful for saving requests to history without exposing sensitive values.
|
||||||
|
Sensitive variables remain in their {{variable_name}} placeholder form.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The HTTP request with variable placeholders
|
||||||
|
environment: The environment containing variable values
|
||||||
|
sensitive_variables: List of variable names to exclude from substitution
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (redacted_request, set_of_undefined_variables, has_sensitive_vars)
|
||||||
|
- redacted_request: Request with only non-sensitive variables substituted
|
||||||
|
- set_of_undefined_variables: Variables that were referenced but not defined
|
||||||
|
- has_sensitive_vars: True if any sensitive variables were found and redacted
|
||||||
|
"""
|
||||||
|
all_undefined = set()
|
||||||
|
|
||||||
|
# Substitute URL (excluding sensitive variables)
|
||||||
|
new_url, url_undefined = VariableSubstitution.substitute_excluding_variables(
|
||||||
|
request.url, environment.variables, sensitive_variables
|
||||||
|
)
|
||||||
|
all_undefined.update(url_undefined)
|
||||||
|
|
||||||
|
# Substitute headers (both keys and values, excluding sensitive variables)
|
||||||
|
new_headers = {}
|
||||||
|
for key, value in request.headers.items():
|
||||||
|
new_key, key_undefined = VariableSubstitution.substitute_excluding_variables(
|
||||||
|
key, environment.variables, sensitive_variables
|
||||||
|
)
|
||||||
|
new_value, value_undefined = VariableSubstitution.substitute_excluding_variables(
|
||||||
|
value, environment.variables, sensitive_variables
|
||||||
|
)
|
||||||
|
all_undefined.update(key_undefined)
|
||||||
|
all_undefined.update(value_undefined)
|
||||||
|
new_headers[new_key] = new_value
|
||||||
|
|
||||||
|
# Substitute body (excluding sensitive variables)
|
||||||
|
new_body, body_undefined = VariableSubstitution.substitute_excluding_variables(
|
||||||
|
request.body, environment.variables, sensitive_variables
|
||||||
|
)
|
||||||
|
all_undefined.update(body_undefined)
|
||||||
|
|
||||||
|
# Create new HttpRequest with redacted values
|
||||||
|
redacted_request = HttpRequest(
|
||||||
|
method=request.method,
|
||||||
|
url=new_url,
|
||||||
|
headers=new_headers,
|
||||||
|
body=new_body,
|
||||||
|
syntax=request.syntax
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if any sensitive variables were actually used in the request
|
||||||
|
all_vars_in_request = set()
|
||||||
|
all_vars_in_request.update(VariableSubstitution.find_variables(request.url))
|
||||||
|
all_vars_in_request.update(VariableSubstitution.find_variables(request.body))
|
||||||
|
for key, value in request.headers.items():
|
||||||
|
all_vars_in_request.update(VariableSubstitution.find_variables(key))
|
||||||
|
all_vars_in_request.update(VariableSubstitution.find_variables(value))
|
||||||
|
|
||||||
|
# Determine if any sensitive variables were actually present
|
||||||
|
has_sensitive_vars = bool(
|
||||||
|
set(sensitive_variables) & all_vars_in_request
|
||||||
|
)
|
||||||
|
|
||||||
|
return redacted_request, all_undefined, has_sensitive_vars
|
||||||
|
|||||||
@ -62,6 +62,19 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
|
||||||
|
<!-- Redaction Indicator (shown when sensitive variables were redacted) -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage" id="redaction_indicator">
|
||||||
|
<property name="icon-name">dialog-information-symbolic</property>
|
||||||
|
<property name="tooltip-text">Sensitive variables were redacted from this history entry</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="visible">False</property>
|
||||||
|
<style>
|
||||||
|
<class name="warning"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
<!-- Delete Button -->
|
<!-- Delete Button -->
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="delete_button">
|
<object class="GtkButton" id="delete_button">
|
||||||
|
|||||||
@ -34,6 +34,7 @@ class HistoryItem(Gtk.Box):
|
|||||||
url_label = Gtk.Template.Child()
|
url_label = Gtk.Template.Child()
|
||||||
timestamp_label = Gtk.Template.Child()
|
timestamp_label = Gtk.Template.Child()
|
||||||
status_label = Gtk.Template.Child()
|
status_label = Gtk.Template.Child()
|
||||||
|
redaction_indicator = Gtk.Template.Child()
|
||||||
delete_button = Gtk.Template.Child()
|
delete_button = Gtk.Template.Child()
|
||||||
request_headers_label = Gtk.Template.Child()
|
request_headers_label = Gtk.Template.Child()
|
||||||
request_body_scroll = Gtk.Template.Child()
|
request_body_scroll = Gtk.Template.Child()
|
||||||
@ -77,6 +78,10 @@ class HistoryItem(Gtk.Box):
|
|||||||
|
|
||||||
self.timestamp_label.set_text(timestamp_str)
|
self.timestamp_label.set_text(timestamp_str)
|
||||||
|
|
||||||
|
# Show redaction indicator if sensitive variables were redacted
|
||||||
|
if self.entry.has_redacted_variables:
|
||||||
|
self.redaction_indicator.set_visible(True)
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
if self.entry.response:
|
if self.entry.response:
|
||||||
status_text = f"{self.entry.response.status_code} {self.entry.response.status_text}"
|
status_text = f"{self.entry.response.status_code} {self.entry.response.status_text}"
|
||||||
|
|||||||
@ -569,12 +569,41 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
if tab:
|
if tab:
|
||||||
tab.response = response
|
tab.response = response
|
||||||
|
|
||||||
# Create history entry (save the substituted request with actual values)
|
# Create history entry with redacted sensitive variables
|
||||||
|
# Determine which request to save to history
|
||||||
|
request_for_history = modified_request
|
||||||
|
has_redacted = False
|
||||||
|
|
||||||
|
if widget.selected_environment_id:
|
||||||
|
env = widget.get_selected_environment()
|
||||||
|
if env and widget.project_id:
|
||||||
|
# Get the project's sensitive variables list
|
||||||
|
projects = self.project_manager.load_projects()
|
||||||
|
sensitive_vars = []
|
||||||
|
for p in projects:
|
||||||
|
if p.id == widget.project_id:
|
||||||
|
sensitive_vars = p.sensitive_variables
|
||||||
|
break
|
||||||
|
|
||||||
|
# Create redacted request (only non-sensitive variables substituted)
|
||||||
|
if sensitive_vars:
|
||||||
|
from .variable_substitution import VariableSubstitution
|
||||||
|
request_for_history, _, has_redacted = VariableSubstitution.substitute_request_excluding_sensitive(
|
||||||
|
modified_request, env, sensitive_vars
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No sensitive variables defined, use fully substituted request
|
||||||
|
request_for_history = substituted_request
|
||||||
|
elif env:
|
||||||
|
# Environment selected but no project - use fully substituted request
|
||||||
|
request_for_history = substituted_request
|
||||||
|
|
||||||
entry = HistoryEntry(
|
entry = HistoryEntry(
|
||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
request=substituted_request,
|
request=request_for_history,
|
||||||
response=response,
|
response=response,
|
||||||
error=error
|
error=error,
|
||||||
|
has_redacted_variables=has_redacted
|
||||||
)
|
)
|
||||||
self.history_manager.add_entry(entry)
|
self.history_manager.add_entry(entry)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user