Add sensitive variable redaction in history
This commit is contained in:
parent
d9643f689f
commit
cdd2c180f4
@ -80,6 +80,7 @@ class HistoryEntry:
|
||||
response: Optional[HttpResponse]
|
||||
error: Optional[str] # Error message if request failed
|
||||
id: str = None # Unique identifier for the entry
|
||||
has_redacted_variables: bool = False # True if sensitive variables were redacted
|
||||
|
||||
def __post_init__(self):
|
||||
"""Generate UUID if id not provided."""
|
||||
@ -94,7 +95,8 @@ class HistoryEntry:
|
||||
'timestamp': self.timestamp,
|
||||
'request': self.request.to_dict(),
|
||||
'response': self.response.to_dict() if self.response else None,
|
||||
'error': self.error
|
||||
'error': self.error,
|
||||
'has_redacted_variables': self.has_redacted_variables
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@ -105,7 +107,8 @@ class HistoryEntry:
|
||||
timestamp=data['timestamp'],
|
||||
request=HttpRequest.from_dict(data['request']),
|
||||
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
|
||||
|
||||
@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>
|
||||
</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 -->
|
||||
<child>
|
||||
<object class="GtkButton" id="delete_button">
|
||||
|
||||
@ -34,6 +34,7 @@ class HistoryItem(Gtk.Box):
|
||||
url_label = Gtk.Template.Child()
|
||||
timestamp_label = Gtk.Template.Child()
|
||||
status_label = Gtk.Template.Child()
|
||||
redaction_indicator = Gtk.Template.Child()
|
||||
delete_button = Gtk.Template.Child()
|
||||
request_headers_label = Gtk.Template.Child()
|
||||
request_body_scroll = Gtk.Template.Child()
|
||||
@ -77,6 +78,10 @@ class HistoryItem(Gtk.Box):
|
||||
|
||||
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
|
||||
if self.entry.response:
|
||||
status_text = f"{self.entry.response.status_code} {self.entry.response.status_text}"
|
||||
|
||||
@ -569,12 +569,41 @@ class RosterWindow(Adw.ApplicationWindow):
|
||||
if tab:
|
||||
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(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
request=substituted_request,
|
||||
request=request_for_history,
|
||||
response=response,
|
||||
error=error
|
||||
error=error,
|
||||
has_redacted_variables=has_redacted
|
||||
)
|
||||
self.history_manager.add_entry(entry)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user