Use XDG Secrets Portal instead of direct D-Bus access
This commit is contained in:
parent
198bb9ad59
commit
0720649036
@ -9,8 +9,7 @@
|
||||
"--share=ipc",
|
||||
"--socket=fallback-x11",
|
||||
"--device=dri",
|
||||
"--socket=wayland",
|
||||
"--talk-name=org.freedesktop.secrets"
|
||||
"--socket=wayland"
|
||||
],
|
||||
"cleanup": [
|
||||
"/include",
|
||||
@ -32,11 +31,8 @@
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://git.bugsy.cz/beval/roster.git",
|
||||
"tag": "v0.8.1"
|
||||
"tag": "v0.8.2"
|
||||
}
|
||||
],
|
||||
"config-opts": [
|
||||
"--libdir=lib"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -9,8 +9,7 @@
|
||||
"--share=ipc",
|
||||
"--socket=fallback-x11",
|
||||
"--device=dri",
|
||||
"--socket=wayland",
|
||||
"--talk-name=org.freedesktop.secrets"
|
||||
"--socket=wayland"
|
||||
],
|
||||
"cleanup" : [
|
||||
"/include",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
project('roster',
|
||||
version: '0.8.1',
|
||||
version: '0.8.2',
|
||||
meson_version: '>= 1.0.0',
|
||||
default_options: [ 'warning_level=2', 'werror=false', ],
|
||||
)
|
||||
|
||||
@ -169,10 +169,12 @@ class EnvironmentsDialog(Adw.Dialog):
|
||||
|
||||
def on_response(dlg, response):
|
||||
if response == "delete":
|
||||
self.project_manager.delete_variable(self.project.id, var_name)
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
def on_delete_complete():
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
|
||||
self.project_manager.delete_variable(self.project.id, var_name, on_delete_complete)
|
||||
|
||||
dialog.connect("response", on_response)
|
||||
dialog.present(self)
|
||||
@ -181,10 +183,12 @@ class EnvironmentsDialog(Adw.Dialog):
|
||||
"""Handle variable name change."""
|
||||
new_name = row.get_variable_name()
|
||||
if new_name and new_name != old_name and new_name not in self.project.variable_names:
|
||||
self.project_manager.rename_variable(self.project.id, old_name, new_name)
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
def on_rename_complete():
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
|
||||
self.project_manager.rename_variable(self.project.id, old_name, new_name, on_rename_complete)
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def on_add_environment_clicked(self, button):
|
||||
@ -267,10 +271,12 @@ class EnvironmentsDialog(Adw.Dialog):
|
||||
|
||||
def on_response(dlg, response):
|
||||
if response == "delete":
|
||||
self.project_manager.delete_environment(self.project.id, environment.id)
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
def on_delete_complete():
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
|
||||
self.project_manager.delete_environment(self.project.id, environment.id, on_delete_complete)
|
||||
|
||||
dialog.connect("response", on_response)
|
||||
dialog.present(self)
|
||||
@ -283,22 +289,19 @@ class EnvironmentsDialog(Adw.Dialog):
|
||||
|
||||
def _on_sensitivity_changed(self, widget, is_sensitive, var_name):
|
||||
"""Handle variable sensitivity toggle."""
|
||||
def on_complete(success):
|
||||
if success:
|
||||
# Reload and refresh UI
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
|
||||
if is_sensitive:
|
||||
# Mark as sensitive (move to keyring)
|
||||
success = self.project_manager.mark_variable_as_sensitive(self.project.id, var_name)
|
||||
if success:
|
||||
# Reload and refresh UI
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
self.project_manager.mark_variable_as_sensitive(self.project.id, var_name, on_complete)
|
||||
else:
|
||||
# Mark as non-sensitive (move to JSON)
|
||||
success = self.project_manager.mark_variable_as_nonsensitive(self.project.id, var_name)
|
||||
if success:
|
||||
# Reload and refresh UI
|
||||
self._reload_project()
|
||||
self._populate_table()
|
||||
self.emit('environments-updated')
|
||||
self.project_manager.mark_variable_as_nonsensitive(self.project.id, var_name, on_complete)
|
||||
|
||||
def _reload_project(self):
|
||||
"""Reload project data from manager."""
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
#
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
@ -22,7 +22,7 @@ import json
|
||||
import uuid
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Callable
|
||||
from datetime import datetime, timezone
|
||||
import gi
|
||||
gi.require_version('GLib', '2.0')
|
||||
@ -132,18 +132,23 @@ class ProjectManager:
|
||||
break
|
||||
self.save_projects(projects)
|
||||
|
||||
def delete_project(self, project_id: str):
|
||||
"""Delete a project and all its requests."""
|
||||
def delete_project(self, project_id: str,
|
||||
callback: Optional[Callable[[], None]] = None):
|
||||
"""Delete a project and all its requests (async for secret cleanup)."""
|
||||
projects = self.load_projects()
|
||||
secret_manager = get_secret_manager()
|
||||
|
||||
# Delete all secrets for this project
|
||||
secret_manager.delete_all_project_secrets(project_id)
|
||||
|
||||
# Remove project from list
|
||||
# Remove project from list and save immediately
|
||||
projects = [p for p in projects if p.id != project_id]
|
||||
self.save_projects(projects)
|
||||
|
||||
# Delete all secrets for this project asynchronously
|
||||
def on_secrets_deleted(success):
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
secret_manager.delete_all_project_secrets(project_id, on_secrets_deleted)
|
||||
|
||||
def add_request(self, project_id: str, name: str, request: HttpRequest, scripts=None) -> SavedRequest:
|
||||
"""Add request to a project."""
|
||||
projects = self.load_projects()
|
||||
@ -239,20 +244,27 @@ class ProjectManager:
|
||||
break
|
||||
self.save_projects(projects)
|
||||
|
||||
def delete_environment(self, project_id: str, env_id: str):
|
||||
"""Delete an environment."""
|
||||
def delete_environment(self, project_id: str, env_id: str,
|
||||
callback: Optional[Callable[[], None]] = None):
|
||||
"""Delete an environment (async for secret cleanup)."""
|
||||
projects = self.load_projects()
|
||||
secret_manager = get_secret_manager()
|
||||
|
||||
for p in projects:
|
||||
if p.id == project_id:
|
||||
# Delete all secrets for this environment
|
||||
secret_manager.delete_all_environment_secrets(project_id, env_id)
|
||||
# Remove environment from list
|
||||
p.environments = [e for e in p.environments if e.id != env_id]
|
||||
break
|
||||
|
||||
self.save_projects(projects)
|
||||
|
||||
# Delete all secrets for this environment asynchronously
|
||||
def on_secrets_deleted(success):
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
secret_manager.delete_all_environment_secrets(project_id, env_id, on_secrets_deleted)
|
||||
|
||||
def add_variable(self, project_id: str, variable_name: str):
|
||||
"""Add a variable to the project and all environments."""
|
||||
projects = self.load_projects()
|
||||
@ -266,10 +278,12 @@ class ProjectManager:
|
||||
break
|
||||
self.save_projects(projects)
|
||||
|
||||
def rename_variable(self, project_id: str, old_name: str, new_name: str):
|
||||
"""Rename a variable in the project and all environments."""
|
||||
def rename_variable(self, project_id: str, old_name: str, new_name: str,
|
||||
callback: Optional[Callable[[], None]] = None):
|
||||
"""Rename a variable in the project and all environments (async for secrets)."""
|
||||
projects = self.load_projects()
|
||||
secret_manager = get_secret_manager()
|
||||
is_sensitive = False
|
||||
|
||||
for p in projects:
|
||||
if p.id == project_id:
|
||||
@ -278,24 +292,33 @@ class ProjectManager:
|
||||
idx = p.variable_names.index(old_name)
|
||||
p.variable_names[idx] = new_name
|
||||
|
||||
# Update sensitive_variables list if applicable
|
||||
# Check if sensitive and update list
|
||||
if old_name in p.sensitive_variables:
|
||||
is_sensitive = True
|
||||
idx_sensitive = p.sensitive_variables.index(old_name)
|
||||
p.sensitive_variables[idx_sensitive] = new_name
|
||||
# Rename secrets in keyring
|
||||
secret_manager.rename_variable_secrets(project_id, old_name, new_name)
|
||||
|
||||
# Update all environments
|
||||
for env in p.environments:
|
||||
if old_name in env.variables:
|
||||
env.variables[new_name] = env.variables.pop(old_name)
|
||||
break
|
||||
|
||||
self.save_projects(projects)
|
||||
|
||||
def delete_variable(self, project_id: str, variable_name: str):
|
||||
"""Delete a variable from the project and all environments."""
|
||||
# Rename secrets in keyring if sensitive
|
||||
if is_sensitive:
|
||||
secret_manager.rename_variable_secrets(project_id, old_name, new_name, lambda success: callback() if callback else None)
|
||||
elif callback:
|
||||
callback()
|
||||
|
||||
def delete_variable(self, project_id: str, variable_name: str,
|
||||
callback: Optional[Callable[[], None]] = None):
|
||||
"""Delete a variable from the project and all environments (async for secrets)."""
|
||||
projects = self.load_projects()
|
||||
secret_manager = get_secret_manager()
|
||||
is_sensitive = False
|
||||
environments_to_cleanup = []
|
||||
|
||||
for p in projects:
|
||||
if p.id == project_id:
|
||||
@ -303,19 +326,33 @@ class ProjectManager:
|
||||
if variable_name in p.variable_names:
|
||||
p.variable_names.remove(variable_name)
|
||||
|
||||
# Remove from sensitive_variables if applicable
|
||||
# Check if sensitive and get environments for cleanup
|
||||
if variable_name in p.sensitive_variables:
|
||||
is_sensitive = True
|
||||
p.sensitive_variables.remove(variable_name)
|
||||
# Delete all secrets for this variable
|
||||
for env in p.environments:
|
||||
secret_manager.delete_secret(project_id, env.id, variable_name)
|
||||
environments_to_cleanup = [env.id for env in p.environments]
|
||||
|
||||
# Remove from all environments
|
||||
for env in p.environments:
|
||||
env.variables.pop(variable_name, None)
|
||||
break
|
||||
|
||||
self.save_projects(projects)
|
||||
|
||||
# Delete secrets for this variable asynchronously
|
||||
if is_sensitive and environments_to_cleanup:
|
||||
pending_count = [len(environments_to_cleanup)]
|
||||
|
||||
def on_single_delete(success):
|
||||
pending_count[0] -= 1
|
||||
if pending_count[0] == 0 and callback:
|
||||
callback()
|
||||
|
||||
for env_id in environments_to_cleanup:
|
||||
secret_manager.delete_secret(project_id, env_id, variable_name, on_single_delete)
|
||||
elif callback:
|
||||
callback()
|
||||
|
||||
def update_environment_variable(self, project_id: str, env_id: str, variable_name: str, value: str):
|
||||
"""Update a single variable value in an environment."""
|
||||
projects = self.load_projects()
|
||||
@ -351,9 +388,10 @@ class ProjectManager:
|
||||
|
||||
# ========== Sensitive Variable Management ==========
|
||||
|
||||
def mark_variable_as_sensitive(self, project_id: str, variable_name: str) -> bool:
|
||||
def mark_variable_as_sensitive(self, project_id: str, variable_name: str,
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Mark a variable as sensitive (move values from JSON to keyring).
|
||||
Mark a variable as sensitive (move values from JSON to keyring) - async.
|
||||
|
||||
This will:
|
||||
1. Add variable to sensitive_variables list
|
||||
@ -363,12 +401,11 @@ class ProjectManager:
|
||||
Args:
|
||||
project_id: Project ID
|
||||
variable_name: Variable name to mark as sensitive
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
callback: Optional callback called with success boolean
|
||||
"""
|
||||
projects = self.load_projects()
|
||||
secret_manager = get_secret_manager()
|
||||
secrets_to_store = []
|
||||
|
||||
for p in projects:
|
||||
if p.id == project_id:
|
||||
@ -376,30 +413,56 @@ class ProjectManager:
|
||||
if variable_name not in p.sensitive_variables:
|
||||
p.sensitive_variables.append(variable_name)
|
||||
|
||||
# Move all environment values to keyring
|
||||
# Collect all environment values to store in keyring
|
||||
for env in p.environments:
|
||||
if variable_name in env.variables:
|
||||
value = env.variables[variable_name]
|
||||
# Store in keyring
|
||||
secret_manager.store_secret(
|
||||
project_id=project_id,
|
||||
environment_id=env.id,
|
||||
variable_name=variable_name,
|
||||
value=value,
|
||||
project_name=p.name,
|
||||
environment_name=env.name
|
||||
)
|
||||
secrets_to_store.append({
|
||||
'environment_id': env.id,
|
||||
'environment_name': env.name,
|
||||
'value': value,
|
||||
'project_name': p.name
|
||||
})
|
||||
# Clear from JSON (keep empty placeholder)
|
||||
env.variables[variable_name] = ""
|
||||
|
||||
self.save_projects(projects)
|
||||
return True
|
||||
|
||||
return False
|
||||
# Store all secrets asynchronously
|
||||
if not secrets_to_store:
|
||||
if callback:
|
||||
callback(True)
|
||||
return
|
||||
|
||||
def mark_variable_as_nonsensitive(self, project_id: str, variable_name: str) -> bool:
|
||||
pending_count = [len(secrets_to_store)]
|
||||
all_success = [True]
|
||||
|
||||
def on_single_store(success):
|
||||
if not success:
|
||||
all_success[0] = False
|
||||
pending_count[0] -= 1
|
||||
if pending_count[0] == 0 and callback:
|
||||
callback(all_success[0])
|
||||
|
||||
for secret_info in secrets_to_store:
|
||||
secret_manager.store_secret(
|
||||
project_id=project_id,
|
||||
environment_id=secret_info['environment_id'],
|
||||
variable_name=variable_name,
|
||||
value=secret_info['value'],
|
||||
project_name=secret_info['project_name'],
|
||||
environment_name=secret_info['environment_name'],
|
||||
callback=on_single_store
|
||||
)
|
||||
return
|
||||
|
||||
if callback:
|
||||
callback(False)
|
||||
|
||||
def mark_variable_as_nonsensitive(self, project_id: str, variable_name: str,
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Mark a variable as non-sensitive (move values from keyring to JSON).
|
||||
Mark a variable as non-sensitive (move values from keyring to JSON) - async.
|
||||
|
||||
This will:
|
||||
1. Remove variable from sensitive_variables list
|
||||
@ -409,40 +472,62 @@ class ProjectManager:
|
||||
Args:
|
||||
project_id: Project ID
|
||||
variable_name: Variable name to mark as non-sensitive
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
callback: Optional callback called with success boolean
|
||||
"""
|
||||
projects = self.load_projects()
|
||||
secret_manager = get_secret_manager()
|
||||
|
||||
target_project = None
|
||||
environments_to_migrate = []
|
||||
|
||||
for p in projects:
|
||||
if p.id == project_id:
|
||||
target_project = p
|
||||
# Remove from sensitive list
|
||||
if variable_name in p.sensitive_variables:
|
||||
p.sensitive_variables.remove(variable_name)
|
||||
|
||||
# Move all environment values from keyring to JSON
|
||||
for env in p.environments:
|
||||
# Retrieve from keyring
|
||||
value = secret_manager.retrieve_secret(
|
||||
project_id=project_id,
|
||||
environment_id=env.id,
|
||||
variable_name=variable_name
|
||||
)
|
||||
# Store in JSON
|
||||
env.variables[variable_name] = value or ""
|
||||
# Delete from keyring
|
||||
secret_manager.delete_secret(
|
||||
project_id=project_id,
|
||||
environment_id=env.id,
|
||||
variable_name=variable_name
|
||||
)
|
||||
environments_to_migrate = list(p.environments)
|
||||
break
|
||||
|
||||
if not target_project or not environments_to_migrate:
|
||||
if callback:
|
||||
callback(False)
|
||||
return
|
||||
|
||||
# Retrieve all secrets, then update JSON, then delete from keyring
|
||||
pending_count = [len(environments_to_migrate)]
|
||||
all_success = [True]
|
||||
|
||||
def on_single_migrate(env):
|
||||
def on_retrieve(value):
|
||||
# Store in JSON
|
||||
env.variables[variable_name] = value or ""
|
||||
self.save_projects(projects)
|
||||
return True
|
||||
|
||||
return False
|
||||
# Delete from keyring
|
||||
def on_delete(success):
|
||||
if not success:
|
||||
all_success[0] = False
|
||||
pending_count[0] -= 1
|
||||
if pending_count[0] == 0 and callback:
|
||||
callback(all_success[0])
|
||||
|
||||
secret_manager.delete_secret(
|
||||
project_id=project_id,
|
||||
environment_id=env.id,
|
||||
variable_name=variable_name,
|
||||
callback=on_delete
|
||||
)
|
||||
return on_retrieve
|
||||
|
||||
for env in environments_to_migrate:
|
||||
secret_manager.retrieve_secret(
|
||||
project_id=project_id,
|
||||
environment_id=env.id,
|
||||
variable_name=variable_name,
|
||||
callback=on_single_migrate(env)
|
||||
)
|
||||
|
||||
def is_variable_sensitive(self, project_id: str, variable_name: str) -> bool:
|
||||
"""Check if a variable is marked as sensitive."""
|
||||
@ -452,9 +537,10 @@ class ProjectManager:
|
||||
return variable_name in p.sensitive_variables
|
||||
return False
|
||||
|
||||
def get_variable_value(self, project_id: str, env_id: str, variable_name: str) -> str:
|
||||
def get_variable_value(self, project_id: str, env_id: str, variable_name: str,
|
||||
callback: Callable[[str], None]):
|
||||
"""
|
||||
Get variable value from appropriate storage (keyring or JSON).
|
||||
Get variable value from appropriate storage (keyring or JSON) - async.
|
||||
|
||||
This is a convenience method that handles both sensitive and non-sensitive variables.
|
||||
|
||||
@ -462,9 +548,7 @@ class ProjectManager:
|
||||
project_id: Project ID
|
||||
env_id: Environment ID
|
||||
variable_name: Variable name
|
||||
|
||||
Returns:
|
||||
Variable value (empty string if not found)
|
||||
callback: Callback called with variable value (empty string if not found)
|
||||
"""
|
||||
projects = self.load_projects()
|
||||
|
||||
@ -472,25 +556,32 @@ class ProjectManager:
|
||||
if p.id == project_id:
|
||||
# Check if sensitive
|
||||
if variable_name in p.sensitive_variables:
|
||||
# Retrieve from keyring
|
||||
# Retrieve from keyring asynchronously
|
||||
secret_manager = get_secret_manager()
|
||||
value = secret_manager.retrieve_secret(
|
||||
|
||||
def on_retrieve(value):
|
||||
callback(value or "")
|
||||
|
||||
secret_manager.retrieve_secret(
|
||||
project_id=project_id,
|
||||
environment_id=env_id,
|
||||
variable_name=variable_name
|
||||
variable_name=variable_name,
|
||||
callback=on_retrieve
|
||||
)
|
||||
return value or ""
|
||||
return
|
||||
else:
|
||||
# Retrieve from JSON
|
||||
# Retrieve from JSON synchronously
|
||||
for env in p.environments:
|
||||
if env.id == env_id:
|
||||
return env.variables.get(variable_name, "")
|
||||
callback(env.variables.get(variable_name, ""))
|
||||
return
|
||||
|
||||
return ""
|
||||
callback("")
|
||||
|
||||
def set_variable_value(self, project_id: str, env_id: str, variable_name: str, value: str):
|
||||
def set_variable_value(self, project_id: str, env_id: str, variable_name: str, value: str,
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Set variable value in appropriate storage (keyring or JSON).
|
||||
Set variable value in appropriate storage (keyring or JSON) - async.
|
||||
|
||||
This is a convenience method that handles both sensitive and non-sensitive variables.
|
||||
|
||||
@ -499,6 +590,7 @@ class ProjectManager:
|
||||
env_id: Environment ID
|
||||
variable_name: Variable name
|
||||
value: Value to set
|
||||
callback: Optional callback called with success boolean
|
||||
"""
|
||||
projects = self.load_projects()
|
||||
secret_manager = get_secret_manager()
|
||||
@ -507,7 +599,7 @@ class ProjectManager:
|
||||
if p.id == project_id:
|
||||
# Check if sensitive
|
||||
if variable_name in p.sensitive_variables:
|
||||
# Store in keyring
|
||||
# Store in keyring asynchronously
|
||||
for env in p.environments:
|
||||
if env.id == env_id:
|
||||
secret_manager.store_secret(
|
||||
@ -516,24 +608,32 @@ class ProjectManager:
|
||||
variable_name=variable_name,
|
||||
value=value,
|
||||
project_name=p.name,
|
||||
environment_name=env.name
|
||||
environment_name=env.name,
|
||||
callback=callback
|
||||
)
|
||||
# Keep empty placeholder in JSON
|
||||
env.variables[variable_name] = ""
|
||||
break
|
||||
self.save_projects(projects)
|
||||
return
|
||||
else:
|
||||
# Store in JSON
|
||||
# Store in JSON synchronously
|
||||
for env in p.environments:
|
||||
if env.id == env_id:
|
||||
env.variables[variable_name] = value
|
||||
break
|
||||
|
||||
self.save_projects(projects)
|
||||
return
|
||||
self.save_projects(projects)
|
||||
if callback:
|
||||
callback(True)
|
||||
return
|
||||
|
||||
def get_environment_with_secrets(self, project_id: str, env_id: str) -> Optional[Environment]:
|
||||
if callback:
|
||||
callback(False)
|
||||
|
||||
def get_environment_with_secrets(self, project_id: str, env_id: str,
|
||||
callback: Callable[[Optional[Environment]], None]):
|
||||
"""
|
||||
Get an environment with all variable values (including secrets from keyring).
|
||||
Get an environment with all variable values (including secrets from keyring) - async.
|
||||
|
||||
This is useful for variable substitution - it returns a complete Environment
|
||||
object with all values populated from both JSON and keyring.
|
||||
@ -541,9 +641,7 @@ class ProjectManager:
|
||||
Args:
|
||||
project_id: Project ID
|
||||
env_id: Environment ID
|
||||
|
||||
Returns:
|
||||
Environment object with all values, or None if not found
|
||||
callback: Callback called with Environment object (or None if not found)
|
||||
"""
|
||||
projects = self.load_projects()
|
||||
secret_manager = get_secret_manager()
|
||||
@ -560,16 +658,24 @@ class ProjectManager:
|
||||
created_at=env.created_at
|
||||
)
|
||||
|
||||
# If no sensitive variables, return immediately
|
||||
if not p.sensitive_variables:
|
||||
callback(complete_env)
|
||||
return
|
||||
|
||||
# Fill in sensitive variable values from keyring
|
||||
for var_name in p.sensitive_variables:
|
||||
value = secret_manager.retrieve_secret(
|
||||
project_id=project_id,
|
||||
environment_id=env_id,
|
||||
variable_name=var_name
|
||||
)
|
||||
if value is not None:
|
||||
complete_env.variables[var_name] = value
|
||||
def on_secrets_retrieved(secrets_dict):
|
||||
for var_name, value in secrets_dict.items():
|
||||
if value is not None:
|
||||
complete_env.variables[var_name] = value
|
||||
callback(complete_env)
|
||||
|
||||
return complete_env
|
||||
secret_manager.retrieve_multiple_secrets(
|
||||
project_id=project_id,
|
||||
environment_id=env_id,
|
||||
variable_names=list(p.sensitive_variables),
|
||||
callback=on_secrets_retrieved
|
||||
)
|
||||
return
|
||||
|
||||
return None
|
||||
callback(None)
|
||||
|
||||
@ -1318,45 +1318,55 @@ class RequestTabWidget(Gtk.Box):
|
||||
# Always update visual indicators when environment changes
|
||||
self._update_variable_indicators()
|
||||
|
||||
def get_selected_environment(self):
|
||||
def get_selected_environment(self, callback):
|
||||
"""
|
||||
Get the currently selected environment object with all values.
|
||||
Get the currently selected environment object with all values (async).
|
||||
|
||||
This includes sensitive variable values from the keyring.
|
||||
|
||||
Args:
|
||||
callback: Function called with the Environment object (or None)
|
||||
"""
|
||||
if not self.selected_environment_id or not self.project_manager or not self.project_id:
|
||||
return None
|
||||
callback(None)
|
||||
return
|
||||
|
||||
# Get environment with secrets (includes values from keyring)
|
||||
return self.project_manager.get_environment_with_secrets(
|
||||
# Get environment with secrets (includes values from keyring) - async
|
||||
self.project_manager.get_environment_with_secrets(
|
||||
self.project_id,
|
||||
self.selected_environment_id
|
||||
self.selected_environment_id,
|
||||
callback
|
||||
)
|
||||
|
||||
def _detect_undefined_variables(self):
|
||||
"""Detect undefined variables in the current request. Returns set of undefined variable names."""
|
||||
def _detect_undefined_variables(self, callback):
|
||||
"""Detect undefined variables in the current request (async).
|
||||
|
||||
Args:
|
||||
callback: Function called with set of undefined variable names
|
||||
"""
|
||||
from .variable_substitution import VariableSubstitution
|
||||
|
||||
# Get current request from UI
|
||||
request = self.get_request()
|
||||
|
||||
# Get selected environment
|
||||
env = self.get_selected_environment()
|
||||
def on_environment_loaded(env):
|
||||
if not env:
|
||||
# No environment selected - ALL variables are undefined
|
||||
# Find all variables in the request
|
||||
all_vars = set()
|
||||
all_vars.update(VariableSubstitution.find_variables(request.url))
|
||||
all_vars.update(VariableSubstitution.find_variables(request.body))
|
||||
for key, value in request.headers.items():
|
||||
all_vars.update(VariableSubstitution.find_variables(key))
|
||||
all_vars.update(VariableSubstitution.find_variables(value))
|
||||
callback(all_vars)
|
||||
else:
|
||||
# Environment selected - find which variables are undefined
|
||||
_, undefined = VariableSubstitution.substitute_request(request, env)
|
||||
callback(undefined)
|
||||
|
||||
if not env:
|
||||
# No environment selected - ALL variables are undefined
|
||||
# Find all variables in the request
|
||||
all_vars = set()
|
||||
all_vars.update(VariableSubstitution.find_variables(request.url))
|
||||
all_vars.update(VariableSubstitution.find_variables(request.body))
|
||||
for key, value in request.headers.items():
|
||||
all_vars.update(VariableSubstitution.find_variables(key))
|
||||
all_vars.update(VariableSubstitution.find_variables(value))
|
||||
return all_vars
|
||||
else:
|
||||
# Environment selected - find which variables are undefined
|
||||
_, undefined = VariableSubstitution.substitute_request(request, env)
|
||||
return undefined
|
||||
# Get selected environment asynchronously
|
||||
self.get_selected_environment(on_environment_loaded)
|
||||
|
||||
def _schedule_indicator_update(self):
|
||||
"""Schedule an indicator update with debouncing."""
|
||||
@ -1374,22 +1384,26 @@ class RequestTabWidget(Gtk.Box):
|
||||
return False # Don't repeat
|
||||
|
||||
def _update_variable_indicators(self):
|
||||
"""Update visual indicators for undefined variables."""
|
||||
"""Update visual indicators for undefined variables (async)."""
|
||||
# Only update if we have a project (variables only make sense in project context)
|
||||
if not self.project_id:
|
||||
return
|
||||
|
||||
# Detect undefined variables
|
||||
self.undefined_variables = self._detect_undefined_variables()
|
||||
def on_undefined_detected(undefined_vars):
|
||||
# Store detected undefined variables
|
||||
self.undefined_variables = undefined_vars
|
||||
|
||||
# Update URL entry
|
||||
self._update_url_indicator()
|
||||
# Update URL entry
|
||||
self._update_url_indicator()
|
||||
|
||||
# Update headers
|
||||
self._update_header_indicators()
|
||||
# Update headers
|
||||
self._update_header_indicators()
|
||||
|
||||
# Update body
|
||||
self._update_body_indicators()
|
||||
# Update body
|
||||
self._update_body_indicators()
|
||||
|
||||
# Detect undefined variables asynchronously
|
||||
self._detect_undefined_variables(on_undefined_detected)
|
||||
|
||||
def _update_url_indicator(self):
|
||||
"""Update warning indicator on URL entry."""
|
||||
|
||||
@ -23,6 +23,9 @@ Manager for storing and retrieving sensitive variable values using GNOME Keyring
|
||||
This module provides a clean interface to libsecret for storing environment
|
||||
variable values that contain sensitive data (API keys, passwords, tokens).
|
||||
|
||||
Uses async APIs for compatibility with the XDG Secrets Portal in sandboxed
|
||||
environments (Flatpak).
|
||||
|
||||
Each secret is identified by:
|
||||
- project_id: UUID of the project
|
||||
- environment_id: UUID of the environment
|
||||
@ -31,8 +34,9 @@ Each secret is identified by:
|
||||
|
||||
import gi
|
||||
gi.require_version('Secret', '1')
|
||||
from gi.repository import Secret, GLib
|
||||
from gi.repository import Secret, GLib, Gio
|
||||
import logging
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -50,7 +54,10 @@ ROSTER_SCHEMA = Secret.Schema.new(
|
||||
|
||||
|
||||
class SecretManager:
|
||||
"""Manages sensitive variable storage using GNOME Keyring via libsecret."""
|
||||
"""Manages sensitive variable storage using GNOME Keyring via libsecret.
|
||||
|
||||
All methods use async APIs for compatibility with the XDG Secrets Portal.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the secret manager."""
|
||||
@ -58,9 +65,10 @@ class SecretManager:
|
||||
|
||||
def store_secret(self, project_id: str, environment_id: str,
|
||||
variable_name: str, value: str, project_name: str = "",
|
||||
environment_name: str = "") -> bool:
|
||||
environment_name: str = "",
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Store a sensitive variable value in GNOME Keyring.
|
||||
Store a sensitive variable value in GNOME Keyring (async).
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project
|
||||
@ -69,9 +77,7 @@ class SecretManager:
|
||||
value: The secret value to store
|
||||
project_name: Optional human-readable project name (for display in Seahorse)
|
||||
environment_name: Optional human-readable environment name (for display)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
callback: Optional callback function called with success boolean
|
||||
"""
|
||||
# Create a descriptive label for the keyring UI
|
||||
label = f"Roster: {project_name}/{environment_name}/{variable_name}" if project_name else \
|
||||
@ -84,36 +90,40 @@ class SecretManager:
|
||||
"variable_name": variable_name,
|
||||
}
|
||||
|
||||
try:
|
||||
# Store the secret synchronously
|
||||
# Using PASSWORD collection (default keyring)
|
||||
Secret.password_store_sync(
|
||||
self.schema,
|
||||
attributes,
|
||||
Secret.COLLECTION_DEFAULT,
|
||||
label,
|
||||
value,
|
||||
None # cancellable
|
||||
)
|
||||
logger.info(f"Stored secret for {variable_name} in {environment_id}")
|
||||
return True
|
||||
def on_store_complete(source, result, user_data):
|
||||
try:
|
||||
Secret.password_store_finish(result)
|
||||
logger.info(f"Stored secret for {variable_name} in {environment_id}")
|
||||
if callback:
|
||||
callback(True)
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to store secret: {e.message}")
|
||||
if callback:
|
||||
callback(False)
|
||||
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to store secret: {e.message}")
|
||||
return False
|
||||
# Store the secret asynchronously
|
||||
Secret.password_store(
|
||||
self.schema,
|
||||
attributes,
|
||||
Secret.COLLECTION_DEFAULT,
|
||||
label,
|
||||
value,
|
||||
None, # cancellable
|
||||
on_store_complete,
|
||||
None # user_data
|
||||
)
|
||||
|
||||
def retrieve_secret(self, project_id: str, environment_id: str,
|
||||
variable_name: str) -> str | None:
|
||||
variable_name: str,
|
||||
callback: Callable[[Optional[str]], None]):
|
||||
"""
|
||||
Retrieve a sensitive variable value from GNOME Keyring.
|
||||
Retrieve a sensitive variable value from GNOME Keyring (async).
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project
|
||||
environment_id: UUID of the environment
|
||||
variable_name: Name of the variable
|
||||
|
||||
Returns:
|
||||
The secret value if found, None otherwise
|
||||
callback: Callback function called with the secret value (or None if not found)
|
||||
"""
|
||||
attributes = {
|
||||
"project_id": project_id,
|
||||
@ -121,37 +131,38 @@ class SecretManager:
|
||||
"variable_name": variable_name,
|
||||
}
|
||||
|
||||
try:
|
||||
# Retrieve the secret synchronously
|
||||
value = Secret.password_lookup_sync(
|
||||
self.schema,
|
||||
attributes,
|
||||
None # cancellable
|
||||
)
|
||||
def on_lookup_complete(source, result, user_data):
|
||||
try:
|
||||
value = Secret.password_lookup_finish(result)
|
||||
if value is None:
|
||||
logger.debug(f"No secret found for {variable_name}")
|
||||
else:
|
||||
logger.debug(f"Retrieved secret for {variable_name}")
|
||||
callback(value)
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to retrieve secret: {e.message}")
|
||||
callback(None)
|
||||
|
||||
if value is None:
|
||||
logger.debug(f"No secret found for {variable_name}")
|
||||
else:
|
||||
logger.debug(f"Retrieved secret for {variable_name}")
|
||||
|
||||
return value
|
||||
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to retrieve secret: {e.message}")
|
||||
return None
|
||||
# Retrieve the secret asynchronously
|
||||
Secret.password_lookup(
|
||||
self.schema,
|
||||
attributes,
|
||||
None, # cancellable
|
||||
on_lookup_complete,
|
||||
None # user_data
|
||||
)
|
||||
|
||||
def delete_secret(self, project_id: str, environment_id: str,
|
||||
variable_name: str) -> bool:
|
||||
variable_name: str,
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Delete a sensitive variable value from GNOME Keyring.
|
||||
Delete a sensitive variable value from GNOME Keyring (async).
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project
|
||||
environment_id: UUID of the environment
|
||||
variable_name: Name of the variable
|
||||
|
||||
Returns:
|
||||
True if deleted (or didn't exist), False on error
|
||||
callback: Optional callback function called with success boolean
|
||||
"""
|
||||
attributes = {
|
||||
"project_id": project_id,
|
||||
@ -159,85 +170,98 @@ class SecretManager:
|
||||
"variable_name": variable_name,
|
||||
}
|
||||
|
||||
try:
|
||||
# Delete the secret synchronously
|
||||
deleted = Secret.password_clear_sync(
|
||||
self.schema,
|
||||
attributes,
|
||||
None # cancellable
|
||||
)
|
||||
def on_clear_complete(source, result, user_data):
|
||||
try:
|
||||
deleted = Secret.password_clear_finish(result)
|
||||
if deleted:
|
||||
logger.info(f"Deleted secret for {variable_name}")
|
||||
else:
|
||||
logger.debug(f"No secret to delete for {variable_name}")
|
||||
if callback:
|
||||
callback(True)
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to delete secret: {e.message}")
|
||||
if callback:
|
||||
callback(False)
|
||||
|
||||
if deleted:
|
||||
logger.info(f"Deleted secret for {variable_name}")
|
||||
else:
|
||||
logger.debug(f"No secret to delete for {variable_name}")
|
||||
# Delete the secret asynchronously
|
||||
Secret.password_clear(
|
||||
self.schema,
|
||||
attributes,
|
||||
None, # cancellable
|
||||
on_clear_complete,
|
||||
None # user_data
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to delete secret: {e.message}")
|
||||
return False
|
||||
|
||||
def delete_all_project_secrets(self, project_id: str) -> bool:
|
||||
def delete_all_project_secrets(self, project_id: str,
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Delete all secrets for a project (when project is deleted).
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
callback: Optional callback function called with success boolean
|
||||
"""
|
||||
attributes = {
|
||||
"project_id": project_id,
|
||||
}
|
||||
|
||||
try:
|
||||
# Delete all matching secrets
|
||||
deleted = Secret.password_clear_sync(
|
||||
self.schema,
|
||||
attributes,
|
||||
None
|
||||
)
|
||||
def on_clear_complete(source, result, user_data):
|
||||
try:
|
||||
Secret.password_clear_finish(result)
|
||||
logger.info(f"Deleted all secrets for project {project_id}")
|
||||
if callback:
|
||||
callback(True)
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to delete project secrets: {e.message}")
|
||||
if callback:
|
||||
callback(False)
|
||||
|
||||
logger.info(f"Deleted all secrets for project {project_id}")
|
||||
return True
|
||||
# Delete all matching secrets asynchronously
|
||||
Secret.password_clear(
|
||||
self.schema,
|
||||
attributes,
|
||||
None,
|
||||
on_clear_complete,
|
||||
None
|
||||
)
|
||||
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to delete project secrets: {e.message}")
|
||||
return False
|
||||
|
||||
def delete_all_environment_secrets(self, project_id: str, environment_id: str) -> bool:
|
||||
def delete_all_environment_secrets(self, project_id: str, environment_id: str,
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Delete all secrets for an environment (when environment is deleted).
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project
|
||||
environment_id: UUID of the environment
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
callback: Optional callback function called with success boolean
|
||||
"""
|
||||
attributes = {
|
||||
"project_id": project_id,
|
||||
"environment_id": environment_id,
|
||||
}
|
||||
|
||||
try:
|
||||
deleted = Secret.password_clear_sync(
|
||||
self.schema,
|
||||
attributes,
|
||||
None
|
||||
)
|
||||
def on_clear_complete(source, result, user_data):
|
||||
try:
|
||||
Secret.password_clear_finish(result)
|
||||
logger.info(f"Deleted all secrets for environment {environment_id}")
|
||||
if callback:
|
||||
callback(True)
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to delete environment secrets: {e.message}")
|
||||
if callback:
|
||||
callback(False)
|
||||
|
||||
logger.info(f"Deleted all secrets for environment {environment_id}")
|
||||
return True
|
||||
Secret.password_clear(
|
||||
self.schema,
|
||||
attributes,
|
||||
None,
|
||||
on_clear_complete,
|
||||
None
|
||||
)
|
||||
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to delete environment secrets: {e.message}")
|
||||
return False
|
||||
|
||||
def rename_variable_secrets(self, project_id: str, old_name: str, new_name: str) -> bool:
|
||||
def rename_variable_secrets(self, project_id: str, old_name: str, new_name: str,
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Rename a variable across all environments (updates secret keys).
|
||||
|
||||
@ -250,65 +274,174 @@ class SecretManager:
|
||||
project_id: UUID of the project
|
||||
old_name: Current variable name
|
||||
new_name: New variable name
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
callback: Optional callback function called with success boolean
|
||||
"""
|
||||
# Search for all secrets with the old variable name
|
||||
attributes = {
|
||||
"project_id": project_id,
|
||||
"variable_name": old_name,
|
||||
}
|
||||
|
||||
try:
|
||||
# Search for matching secrets
|
||||
# This returns a list of Secret.Item objects
|
||||
secrets = Secret.password_search_sync(
|
||||
self.schema,
|
||||
attributes,
|
||||
Secret.SearchFlags.ALL,
|
||||
None
|
||||
)
|
||||
def on_search_complete(source, result, user_data):
|
||||
try:
|
||||
secrets = Secret.password_search_finish(result)
|
||||
|
||||
if not secrets:
|
||||
logger.debug(f"No secrets found for variable {old_name}")
|
||||
return True
|
||||
if not secrets:
|
||||
logger.debug(f"No secrets found for variable {old_name}")
|
||||
if callback:
|
||||
callback(True)
|
||||
return
|
||||
|
||||
# For each secret, retrieve value, store with new name, delete old
|
||||
for item in secrets:
|
||||
# Get the secret value
|
||||
item.load_secret_sync(None)
|
||||
value = item.get_secret().get_text()
|
||||
# Track how many secrets we need to process
|
||||
pending_count = [len(secrets)]
|
||||
all_success = [True]
|
||||
|
||||
# Get environment_id from attributes
|
||||
def on_rename_done(success):
|
||||
pending_count[0] -= 1
|
||||
if not success:
|
||||
all_success[0] = False
|
||||
if pending_count[0] == 0:
|
||||
logger.info(f"Renamed variable secrets from {old_name} to {new_name}")
|
||||
if callback:
|
||||
callback(all_success[0])
|
||||
|
||||
# For each secret, retrieve value, store with new name, delete old
|
||||
for item in secrets:
|
||||
self._rename_single_secret(item, project_id, old_name, new_name, on_rename_done)
|
||||
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to search for variable secrets: {e.message}")
|
||||
if callback:
|
||||
callback(False)
|
||||
|
||||
# Search for matching secrets asynchronously
|
||||
Secret.password_search(
|
||||
self.schema,
|
||||
attributes,
|
||||
Secret.SearchFlags.ALL,
|
||||
None,
|
||||
on_search_complete,
|
||||
None
|
||||
)
|
||||
|
||||
def _rename_single_secret(self, item, project_id: str, old_name: str,
|
||||
new_name: str, callback: Callable[[bool], None]):
|
||||
"""Helper to rename a single secret item."""
|
||||
|
||||
def on_load_complete(item, result, user_data):
|
||||
try:
|
||||
item.load_secret_finish(result)
|
||||
secret = item.get_secret()
|
||||
if secret is None:
|
||||
callback(False)
|
||||
return
|
||||
|
||||
value = secret.get_text()
|
||||
attrs = item.get_attributes()
|
||||
environment_id = attrs.get("environment_id")
|
||||
|
||||
if not environment_id:
|
||||
logger.warning(f"Secret missing environment_id, skipping")
|
||||
continue
|
||||
logger.warning("Secret missing environment_id, skipping")
|
||||
callback(False)
|
||||
return
|
||||
|
||||
# Store with new name, then delete old
|
||||
def on_store_done(success):
|
||||
if success:
|
||||
self.delete_secret(project_id, environment_id, old_name, callback)
|
||||
else:
|
||||
callback(False)
|
||||
|
||||
# Store with new name
|
||||
self.store_secret(
|
||||
project_id=project_id,
|
||||
environment_id=environment_id,
|
||||
variable_name=new_name,
|
||||
value=value
|
||||
value=value,
|
||||
callback=on_store_done
|
||||
)
|
||||
|
||||
# Delete old secret
|
||||
self.delete_secret(
|
||||
project_id=project_id,
|
||||
environment_id=environment_id,
|
||||
variable_name=old_name
|
||||
)
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to load secret for rename: {e.message}")
|
||||
callback(False)
|
||||
|
||||
logger.info(f"Renamed variable secrets from {old_name} to {new_name}")
|
||||
return True
|
||||
item.load_secret(None, on_load_complete, None)
|
||||
|
||||
except GLib.Error as e:
|
||||
logger.error(f"Failed to rename variable secrets: {e.message}")
|
||||
return False
|
||||
# ========== Batch Operations ==========
|
||||
|
||||
def retrieve_multiple_secrets(self, project_id: str, environment_id: str,
|
||||
variable_names: list,
|
||||
callback: Callable[[dict], None]):
|
||||
"""
|
||||
Retrieve multiple secrets at once (async).
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project
|
||||
environment_id: UUID of the environment
|
||||
variable_names: List of variable names to retrieve
|
||||
callback: Callback function called with dict of {variable_name: value}
|
||||
"""
|
||||
if not variable_names:
|
||||
callback({})
|
||||
return
|
||||
|
||||
results = {}
|
||||
pending_count = [len(variable_names)]
|
||||
|
||||
def on_single_retrieve(var_name):
|
||||
def handler(value):
|
||||
results[var_name] = value
|
||||
pending_count[0] -= 1
|
||||
if pending_count[0] == 0:
|
||||
callback(results)
|
||||
return handler
|
||||
|
||||
for var_name in variable_names:
|
||||
self.retrieve_secret(
|
||||
project_id=project_id,
|
||||
environment_id=environment_id,
|
||||
variable_name=var_name,
|
||||
callback=on_single_retrieve(var_name)
|
||||
)
|
||||
|
||||
def store_multiple_secrets(self, project_id: str, environment_id: str,
|
||||
variables: dict, project_name: str = "",
|
||||
environment_name: str = "",
|
||||
callback: Optional[Callable[[bool], None]] = None):
|
||||
"""
|
||||
Store multiple secrets at once (async).
|
||||
|
||||
Args:
|
||||
project_id: UUID of the project
|
||||
environment_id: UUID of the environment
|
||||
variables: Dict of {variable_name: value} to store
|
||||
project_name: Optional project name for display
|
||||
environment_name: Optional environment name for display
|
||||
callback: Optional callback called with overall success boolean
|
||||
"""
|
||||
if not variables:
|
||||
if callback:
|
||||
callback(True)
|
||||
return
|
||||
|
||||
pending_count = [len(variables)]
|
||||
all_success = [True]
|
||||
|
||||
def on_single_store(success):
|
||||
if not success:
|
||||
all_success[0] = False
|
||||
pending_count[0] -= 1
|
||||
if pending_count[0] == 0 and callback:
|
||||
callback(all_success[0])
|
||||
|
||||
for var_name, value in variables.items():
|
||||
self.store_secret(
|
||||
project_id=project_id,
|
||||
environment_id=environment_id,
|
||||
variable_name=var_name,
|
||||
value=value,
|
||||
project_name=project_name,
|
||||
environment_name=environment_name,
|
||||
callback=on_single_store
|
||||
)
|
||||
|
||||
|
||||
# Singleton instance
|
||||
|
||||
@ -107,22 +107,12 @@ class VariableDataRow(Gtk.Box):
|
||||
entry.set_placeholder_text(env.name)
|
||||
entry.set_hexpand(True)
|
||||
|
||||
# Get value from appropriate storage (keyring or JSON)
|
||||
value = self.project_manager.get_variable_value(
|
||||
self.project.id,
|
||||
env.id,
|
||||
self.variable_name
|
||||
)
|
||||
entry.set_text(value)
|
||||
|
||||
# Configure entry for sensitive variables
|
||||
if is_sensitive:
|
||||
entry.set_visibility(False) # Show bullets instead of text
|
||||
entry.set_input_purpose(Gtk.InputPurpose.PASSWORD)
|
||||
entry.add_css_class("sensitive-variable")
|
||||
|
||||
entry.connect('changed', self._on_value_changed, env.id)
|
||||
|
||||
entry_box.append(entry)
|
||||
|
||||
# Add entry box to size group for alignment
|
||||
@ -132,17 +122,34 @@ class VariableDataRow(Gtk.Box):
|
||||
self.values_box.append(entry_box)
|
||||
self.value_entries[env.id] = entry
|
||||
|
||||
# Get value from appropriate storage (keyring or JSON) asynchronously
|
||||
def on_value_retrieved(value, entry=entry, env_id=env.id):
|
||||
entry.set_text(value)
|
||||
# Connect changed handler after setting initial value to avoid spurious signals
|
||||
entry.connect('changed', self._on_value_changed, env_id)
|
||||
|
||||
self.project_manager.get_variable_value(
|
||||
self.project.id,
|
||||
env.id,
|
||||
self.variable_name,
|
||||
on_value_retrieved
|
||||
)
|
||||
|
||||
def _on_value_changed(self, entry, env_id):
|
||||
"""Handle value entry changes."""
|
||||
value = entry.get_text()
|
||||
# Use project_manager to store value in appropriate location
|
||||
|
||||
# Emit signal first (value is being changed)
|
||||
self.emit('value-changed', env_id, value)
|
||||
|
||||
# Use project_manager to store value in appropriate location (async)
|
||||
self.project_manager.set_variable_value(
|
||||
self.project.id,
|
||||
env_id,
|
||||
self.variable_name,
|
||||
value
|
||||
value,
|
||||
callback=None # Fire and forget - errors are logged
|
||||
)
|
||||
self.emit('value-changed', env_id, value)
|
||||
|
||||
def _on_name_entry_focus_in(self, controller):
|
||||
"""Show edit buttons when entry gains focus."""
|
||||
|
||||
130
src/window.py
130
src/window.py
@ -535,10 +535,13 @@ class RosterWindow(Adw.ApplicationWindow):
|
||||
if preprocessing_result.modified_request:
|
||||
modified_request = preprocessing_result.modified_request
|
||||
|
||||
# Apply variable substitution to (possibly modified) request
|
||||
substituted_request = modified_request
|
||||
if widget.selected_environment_id:
|
||||
env = widget.get_selected_environment()
|
||||
# Disable send button during request
|
||||
widget.send_button.set_sensitive(False)
|
||||
|
||||
def proceed_with_request(env):
|
||||
"""Continue with request after environment is loaded."""
|
||||
# Apply variable substitution to (possibly modified) request
|
||||
substituted_request = modified_request
|
||||
if env:
|
||||
from .variable_substitution import VariableSubstitution
|
||||
substituted_request, undefined = VariableSubstitution.substitute_request(modified_request, env)
|
||||
@ -546,65 +549,60 @@ class RosterWindow(Adw.ApplicationWindow):
|
||||
if undefined:
|
||||
logger.warning(f"Undefined variables in request: {', '.join(undefined)}")
|
||||
|
||||
# Disable send button during request
|
||||
widget.send_button.set_sensitive(False)
|
||||
# Execute async
|
||||
def callback(response, error, user_data):
|
||||
"""Callback runs on main thread."""
|
||||
# Re-enable send button
|
||||
widget.send_button.set_sensitive(True)
|
||||
|
||||
# Execute async
|
||||
def callback(response, error, user_data):
|
||||
"""Callback runs on main thread."""
|
||||
# Re-enable send button
|
||||
widget.send_button.set_sensitive(True)
|
||||
# Update tab with response
|
||||
if response:
|
||||
widget.display_response(response)
|
||||
|
||||
# Update tab with response
|
||||
if response:
|
||||
widget.display_response(response)
|
||||
# Execute postprocessing script if exists
|
||||
scripts = widget.get_scripts()
|
||||
if scripts and scripts.postprocessing.strip():
|
||||
from .script_executor import ScriptExecutor, ScriptContext
|
||||
|
||||
# Execute postprocessing script if exists
|
||||
scripts = widget.get_scripts()
|
||||
if scripts and scripts.postprocessing.strip():
|
||||
from .script_executor import ScriptExecutor, ScriptContext
|
||||
# Create context for variable updates
|
||||
context = None
|
||||
if widget.project_id and widget.selected_environment_id:
|
||||
context = ScriptContext(
|
||||
project_id=widget.project_id,
|
||||
environment_id=widget.selected_environment_id,
|
||||
project_manager=self.project_manager
|
||||
)
|
||||
|
||||
# Create context for variable updates
|
||||
context = None
|
||||
if widget.project_id and widget.selected_environment_id:
|
||||
context = ScriptContext(
|
||||
project_id=widget.project_id,
|
||||
environment_id=widget.selected_environment_id,
|
||||
project_manager=self.project_manager
|
||||
# Execute script with context
|
||||
script_result = ScriptExecutor.execute_postprocessing_script(
|
||||
scripts.postprocessing,
|
||||
response,
|
||||
context
|
||||
)
|
||||
|
||||
# Execute script with context
|
||||
script_result = ScriptExecutor.execute_postprocessing_script(
|
||||
scripts.postprocessing,
|
||||
response,
|
||||
context
|
||||
)
|
||||
# Display script results
|
||||
widget.display_script_results(script_result)
|
||||
|
||||
# Display script results
|
||||
widget.display_script_results(script_result)
|
||||
|
||||
# Process variable updates
|
||||
self._process_variable_updates(script_result, widget, context)
|
||||
# Process variable updates
|
||||
self._process_variable_updates(script_result, widget, context)
|
||||
else:
|
||||
widget._clear_script_results()
|
||||
else:
|
||||
widget.display_error(error or "Unknown error")
|
||||
widget._clear_script_results()
|
||||
else:
|
||||
widget.display_error(error or "Unknown error")
|
||||
widget._clear_script_results()
|
||||
|
||||
# Save response to tab
|
||||
page = self.tab_view.get_selected_page()
|
||||
if page:
|
||||
tab = self.page_to_tab.get(page)
|
||||
if tab:
|
||||
tab.response = response
|
||||
# Save response to tab
|
||||
page = self.tab_view.get_selected_page()
|
||||
if page:
|
||||
tab = self.page_to_tab.get(page)
|
||||
if tab:
|
||||
tab.response = response
|
||||
|
||||
# Create history entry with redacted sensitive variables
|
||||
# Determine which request to save to history
|
||||
request_for_history = modified_request
|
||||
has_redacted = False
|
||||
# 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()
|
||||
@ -624,22 +622,28 @@ class RosterWindow(Adw.ApplicationWindow):
|
||||
# No sensitive variables defined, use fully substituted request
|
||||
request_for_history = substituted_request
|
||||
elif env:
|
||||
# Environment selected but no project - use fully substituted request
|
||||
# Environment loaded but no project - use fully substituted request
|
||||
request_for_history = substituted_request
|
||||
|
||||
entry = HistoryEntry(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
request=request_for_history,
|
||||
response=response,
|
||||
error=error,
|
||||
has_redacted_variables=has_redacted
|
||||
)
|
||||
self.history_manager.add_entry(entry)
|
||||
entry = HistoryEntry(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
request=request_for_history,
|
||||
response=response,
|
||||
error=error,
|
||||
has_redacted_variables=has_redacted
|
||||
)
|
||||
self.history_manager.add_entry(entry)
|
||||
|
||||
# Refresh history panel to show new entry
|
||||
self._load_history()
|
||||
# Refresh history panel to show new entry
|
||||
self._load_history()
|
||||
|
||||
self.http_client.execute_request_async(substituted_request, callback, None)
|
||||
self.http_client.execute_request_async(substituted_request, callback, None)
|
||||
|
||||
# Fetch environment asynchronously then proceed
|
||||
if widget.selected_environment_id:
|
||||
widget.get_selected_environment(proceed_with_request)
|
||||
else:
|
||||
proceed_with_request(None)
|
||||
|
||||
# History and Project Management
|
||||
def _load_history(self) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user