Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ad720afc8 | |||
| 0720649036 | |||
| 198bb9ad59 |
@ -9,8 +9,7 @@
|
|||||||
"--share=ipc",
|
"--share=ipc",
|
||||||
"--socket=fallback-x11",
|
"--socket=fallback-x11",
|
||||||
"--device=dri",
|
"--device=dri",
|
||||||
"--socket=wayland",
|
"--socket=wayland"
|
||||||
"--talk-name=org.freedesktop.secrets"
|
|
||||||
],
|
],
|
||||||
"cleanup": [
|
"cleanup": [
|
||||||
"/include",
|
"/include",
|
||||||
@ -32,11 +31,8 @@
|
|||||||
{
|
{
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.bugsy.cz/beval/roster.git",
|
"url": "https://git.bugsy.cz/beval/roster.git",
|
||||||
"tag": "v0.7.0"
|
"tag": "v0.8.2"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"config-opts": [
|
|
||||||
"--libdir=lib"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -9,8 +9,7 @@
|
|||||||
"--share=ipc",
|
"--share=ipc",
|
||||||
"--socket=fallback-x11",
|
"--socket=fallback-x11",
|
||||||
"--device=dri",
|
"--device=dri",
|
||||||
"--socket=wayland",
|
"--socket=wayland"
|
||||||
"--talk-name=org.freedesktop.secrets"
|
|
||||||
],
|
],
|
||||||
"cleanup" : [
|
"cleanup" : [
|
||||||
"/include",
|
"/include",
|
||||||
|
|||||||
@ -25,6 +25,11 @@
|
|||||||
<!-- Use the OARS website (https://hughsie.github.io/oars/generate.html) to generate these and make sure to use oars-1.1 -->
|
<!-- Use the OARS website (https://hughsie.github.io/oars/generate.html) to generate these and make sure to use oars-1.1 -->
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
|
|
||||||
|
<branding>
|
||||||
|
<color type="primary" scheme_preference="light">#8b9dbc</color>
|
||||||
|
<color type="primary" scheme_preference="dark">#173c7a</color>
|
||||||
|
</branding>
|
||||||
|
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://git.bugsy.cz/beval/roster/raw/branch/master/screenshots/main.png</image>
|
<image>https://git.bugsy.cz/beval/roster/raw/branch/master/screenshots/main.png</image>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
project('roster',
|
project('roster',
|
||||||
version: '0.8.1',
|
version: '0.8.2',
|
||||||
meson_version: '>= 1.0.0',
|
meson_version: '>= 1.0.0',
|
||||||
default_options: [ 'warning_level=2', 'werror=false', ],
|
default_options: [ 'warning_level=2', 'werror=false', ],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -169,10 +169,12 @@ class EnvironmentsDialog(Adw.Dialog):
|
|||||||
|
|
||||||
def on_response(dlg, response):
|
def on_response(dlg, response):
|
||||||
if response == "delete":
|
if response == "delete":
|
||||||
self.project_manager.delete_variable(self.project.id, var_name)
|
def on_delete_complete():
|
||||||
self._reload_project()
|
self._reload_project()
|
||||||
self._populate_table()
|
self._populate_table()
|
||||||
self.emit('environments-updated')
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
self.project_manager.delete_variable(self.project.id, var_name, on_delete_complete)
|
||||||
|
|
||||||
dialog.connect("response", on_response)
|
dialog.connect("response", on_response)
|
||||||
dialog.present(self)
|
dialog.present(self)
|
||||||
@ -181,10 +183,12 @@ class EnvironmentsDialog(Adw.Dialog):
|
|||||||
"""Handle variable name change."""
|
"""Handle variable name change."""
|
||||||
new_name = row.get_variable_name()
|
new_name = row.get_variable_name()
|
||||||
if new_name and new_name != old_name and new_name not in self.project.variable_names:
|
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)
|
def on_rename_complete():
|
||||||
self._reload_project()
|
self._reload_project()
|
||||||
self._populate_table()
|
self._populate_table()
|
||||||
self.emit('environments-updated')
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
self.project_manager.rename_variable(self.project.id, old_name, new_name, on_rename_complete)
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def on_add_environment_clicked(self, button):
|
def on_add_environment_clicked(self, button):
|
||||||
@ -267,10 +271,12 @@ class EnvironmentsDialog(Adw.Dialog):
|
|||||||
|
|
||||||
def on_response(dlg, response):
|
def on_response(dlg, response):
|
||||||
if response == "delete":
|
if response == "delete":
|
||||||
self.project_manager.delete_environment(self.project.id, environment.id)
|
def on_delete_complete():
|
||||||
self._reload_project()
|
self._reload_project()
|
||||||
self._populate_table()
|
self._populate_table()
|
||||||
self.emit('environments-updated')
|
self.emit('environments-updated')
|
||||||
|
|
||||||
|
self.project_manager.delete_environment(self.project.id, environment.id, on_delete_complete)
|
||||||
|
|
||||||
dialog.connect("response", on_response)
|
dialog.connect("response", on_response)
|
||||||
dialog.present(self)
|
dialog.present(self)
|
||||||
@ -283,22 +289,19 @@ class EnvironmentsDialog(Adw.Dialog):
|
|||||||
|
|
||||||
def _on_sensitivity_changed(self, widget, is_sensitive, var_name):
|
def _on_sensitivity_changed(self, widget, is_sensitive, var_name):
|
||||||
"""Handle variable sensitivity toggle."""
|
"""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:
|
if is_sensitive:
|
||||||
# Mark as sensitive (move to keyring)
|
# Mark as sensitive (move to keyring)
|
||||||
success = self.project_manager.mark_variable_as_sensitive(self.project.id, var_name)
|
self.project_manager.mark_variable_as_sensitive(self.project.id, var_name, on_complete)
|
||||||
if success:
|
|
||||||
# Reload and refresh UI
|
|
||||||
self._reload_project()
|
|
||||||
self._populate_table()
|
|
||||||
self.emit('environments-updated')
|
|
||||||
else:
|
else:
|
||||||
# Mark as non-sensitive (move to JSON)
|
# Mark as non-sensitive (move to JSON)
|
||||||
success = self.project_manager.mark_variable_as_nonsensitive(self.project.id, var_name)
|
self.project_manager.mark_variable_as_nonsensitive(self.project.id, var_name, on_complete)
|
||||||
if success:
|
|
||||||
# Reload and refresh UI
|
|
||||||
self._reload_project()
|
|
||||||
self._populate_table()
|
|
||||||
self.emit('environments-updated')
|
|
||||||
|
|
||||||
def _reload_project(self):
|
def _reload_project(self):
|
||||||
"""Reload project data from manager."""
|
"""Reload project data from manager."""
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import json
|
|||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Callable
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import gi
|
import gi
|
||||||
gi.require_version('GLib', '2.0')
|
gi.require_version('GLib', '2.0')
|
||||||
@ -132,18 +132,23 @@ class ProjectManager:
|
|||||||
break
|
break
|
||||||
self.save_projects(projects)
|
self.save_projects(projects)
|
||||||
|
|
||||||
def delete_project(self, project_id: str):
|
def delete_project(self, project_id: str,
|
||||||
"""Delete a project and all its requests."""
|
callback: Optional[Callable[[], None]] = None):
|
||||||
|
"""Delete a project and all its requests (async for secret cleanup)."""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
secret_manager = get_secret_manager()
|
secret_manager = get_secret_manager()
|
||||||
|
|
||||||
# Delete all secrets for this project
|
# Remove project from list and save immediately
|
||||||
secret_manager.delete_all_project_secrets(project_id)
|
|
||||||
|
|
||||||
# Remove project from list
|
|
||||||
projects = [p for p in projects if p.id != project_id]
|
projects = [p for p in projects if p.id != project_id]
|
||||||
self.save_projects(projects)
|
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:
|
def add_request(self, project_id: str, name: str, request: HttpRequest, scripts=None) -> SavedRequest:
|
||||||
"""Add request to a project."""
|
"""Add request to a project."""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
@ -239,20 +244,27 @@ class ProjectManager:
|
|||||||
break
|
break
|
||||||
self.save_projects(projects)
|
self.save_projects(projects)
|
||||||
|
|
||||||
def delete_environment(self, project_id: str, env_id: str):
|
def delete_environment(self, project_id: str, env_id: str,
|
||||||
"""Delete an environment."""
|
callback: Optional[Callable[[], None]] = None):
|
||||||
|
"""Delete an environment (async for secret cleanup)."""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
secret_manager = get_secret_manager()
|
secret_manager = get_secret_manager()
|
||||||
|
|
||||||
for p in projects:
|
for p in projects:
|
||||||
if p.id == project_id:
|
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
|
# Remove environment from list
|
||||||
p.environments = [e for e in p.environments if e.id != env_id]
|
p.environments = [e for e in p.environments if e.id != env_id]
|
||||||
break
|
break
|
||||||
|
|
||||||
self.save_projects(projects)
|
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):
|
def add_variable(self, project_id: str, variable_name: str):
|
||||||
"""Add a variable to the project and all environments."""
|
"""Add a variable to the project and all environments."""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
@ -266,10 +278,12 @@ class ProjectManager:
|
|||||||
break
|
break
|
||||||
self.save_projects(projects)
|
self.save_projects(projects)
|
||||||
|
|
||||||
def rename_variable(self, project_id: str, old_name: str, new_name: str):
|
def rename_variable(self, project_id: str, old_name: str, new_name: str,
|
||||||
"""Rename a variable in the project and all environments."""
|
callback: Optional[Callable[[], None]] = None):
|
||||||
|
"""Rename a variable in the project and all environments (async for secrets)."""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
secret_manager = get_secret_manager()
|
secret_manager = get_secret_manager()
|
||||||
|
is_sensitive = False
|
||||||
|
|
||||||
for p in projects:
|
for p in projects:
|
||||||
if p.id == project_id:
|
if p.id == project_id:
|
||||||
@ -278,24 +292,33 @@ class ProjectManager:
|
|||||||
idx = p.variable_names.index(old_name)
|
idx = p.variable_names.index(old_name)
|
||||||
p.variable_names[idx] = new_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:
|
if old_name in p.sensitive_variables:
|
||||||
|
is_sensitive = True
|
||||||
idx_sensitive = p.sensitive_variables.index(old_name)
|
idx_sensitive = p.sensitive_variables.index(old_name)
|
||||||
p.sensitive_variables[idx_sensitive] = new_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
|
# Update all environments
|
||||||
for env in p.environments:
|
for env in p.environments:
|
||||||
if old_name in env.variables:
|
if old_name in env.variables:
|
||||||
env.variables[new_name] = env.variables.pop(old_name)
|
env.variables[new_name] = env.variables.pop(old_name)
|
||||||
break
|
break
|
||||||
|
|
||||||
self.save_projects(projects)
|
self.save_projects(projects)
|
||||||
|
|
||||||
def delete_variable(self, project_id: str, variable_name: str):
|
# Rename secrets in keyring if sensitive
|
||||||
"""Delete a variable from the project and all environments."""
|
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()
|
projects = self.load_projects()
|
||||||
secret_manager = get_secret_manager()
|
secret_manager = get_secret_manager()
|
||||||
|
is_sensitive = False
|
||||||
|
environments_to_cleanup = []
|
||||||
|
|
||||||
for p in projects:
|
for p in projects:
|
||||||
if p.id == project_id:
|
if p.id == project_id:
|
||||||
@ -303,19 +326,33 @@ class ProjectManager:
|
|||||||
if variable_name in p.variable_names:
|
if variable_name in p.variable_names:
|
||||||
p.variable_names.remove(variable_name)
|
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:
|
if variable_name in p.sensitive_variables:
|
||||||
|
is_sensitive = True
|
||||||
p.sensitive_variables.remove(variable_name)
|
p.sensitive_variables.remove(variable_name)
|
||||||
# Delete all secrets for this variable
|
environments_to_cleanup = [env.id for env in p.environments]
|
||||||
for env in p.environments:
|
|
||||||
secret_manager.delete_secret(project_id, env.id, variable_name)
|
|
||||||
|
|
||||||
# Remove from all environments
|
# Remove from all environments
|
||||||
for env in p.environments:
|
for env in p.environments:
|
||||||
env.variables.pop(variable_name, None)
|
env.variables.pop(variable_name, None)
|
||||||
break
|
break
|
||||||
|
|
||||||
self.save_projects(projects)
|
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):
|
def update_environment_variable(self, project_id: str, env_id: str, variable_name: str, value: str):
|
||||||
"""Update a single variable value in an environment."""
|
"""Update a single variable value in an environment."""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
@ -351,9 +388,10 @@ class ProjectManager:
|
|||||||
|
|
||||||
# ========== Sensitive Variable Management ==========
|
# ========== 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:
|
This will:
|
||||||
1. Add variable to sensitive_variables list
|
1. Add variable to sensitive_variables list
|
||||||
@ -363,12 +401,11 @@ class ProjectManager:
|
|||||||
Args:
|
Args:
|
||||||
project_id: Project ID
|
project_id: Project ID
|
||||||
variable_name: Variable name to mark as sensitive
|
variable_name: Variable name to mark as sensitive
|
||||||
|
callback: Optional callback called with success boolean
|
||||||
Returns:
|
|
||||||
True if successful
|
|
||||||
"""
|
"""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
secret_manager = get_secret_manager()
|
secret_manager = get_secret_manager()
|
||||||
|
secrets_to_store = []
|
||||||
|
|
||||||
for p in projects:
|
for p in projects:
|
||||||
if p.id == project_id:
|
if p.id == project_id:
|
||||||
@ -376,30 +413,56 @@ class ProjectManager:
|
|||||||
if variable_name not in p.sensitive_variables:
|
if variable_name not in p.sensitive_variables:
|
||||||
p.sensitive_variables.append(variable_name)
|
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:
|
for env in p.environments:
|
||||||
if variable_name in env.variables:
|
if variable_name in env.variables:
|
||||||
value = env.variables[variable_name]
|
value = env.variables[variable_name]
|
||||||
# Store in keyring
|
secrets_to_store.append({
|
||||||
secret_manager.store_secret(
|
'environment_id': env.id,
|
||||||
project_id=project_id,
|
'environment_name': env.name,
|
||||||
environment_id=env.id,
|
'value': value,
|
||||||
variable_name=variable_name,
|
'project_name': p.name
|
||||||
value=value,
|
})
|
||||||
project_name=p.name,
|
|
||||||
environment_name=env.name
|
|
||||||
)
|
|
||||||
# Clear from JSON (keep empty placeholder)
|
# Clear from JSON (keep empty placeholder)
|
||||||
env.variables[variable_name] = ""
|
env.variables[variable_name] = ""
|
||||||
|
|
||||||
self.save_projects(projects)
|
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:
|
This will:
|
||||||
1. Remove variable from sensitive_variables list
|
1. Remove variable from sensitive_variables list
|
||||||
@ -409,40 +472,62 @@ class ProjectManager:
|
|||||||
Args:
|
Args:
|
||||||
project_id: Project ID
|
project_id: Project ID
|
||||||
variable_name: Variable name to mark as non-sensitive
|
variable_name: Variable name to mark as non-sensitive
|
||||||
|
callback: Optional callback called with success boolean
|
||||||
Returns:
|
|
||||||
True if successful
|
|
||||||
"""
|
"""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
secret_manager = get_secret_manager()
|
secret_manager = get_secret_manager()
|
||||||
|
|
||||||
|
target_project = None
|
||||||
|
environments_to_migrate = []
|
||||||
|
|
||||||
for p in projects:
|
for p in projects:
|
||||||
if p.id == project_id:
|
if p.id == project_id:
|
||||||
|
target_project = p
|
||||||
# Remove from sensitive list
|
# Remove from sensitive list
|
||||||
if variable_name in p.sensitive_variables:
|
if variable_name in p.sensitive_variables:
|
||||||
p.sensitive_variables.remove(variable_name)
|
p.sensitive_variables.remove(variable_name)
|
||||||
|
|
||||||
# Move all environment values from keyring to JSON
|
environments_to_migrate = list(p.environments)
|
||||||
for env in p.environments:
|
break
|
||||||
# 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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
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)
|
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:
|
def is_variable_sensitive(self, project_id: str, variable_name: str) -> bool:
|
||||||
"""Check if a variable is marked as sensitive."""
|
"""Check if a variable is marked as sensitive."""
|
||||||
@ -452,9 +537,10 @@ class ProjectManager:
|
|||||||
return variable_name in p.sensitive_variables
|
return variable_name in p.sensitive_variables
|
||||||
return False
|
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.
|
This is a convenience method that handles both sensitive and non-sensitive variables.
|
||||||
|
|
||||||
@ -462,9 +548,7 @@ class ProjectManager:
|
|||||||
project_id: Project ID
|
project_id: Project ID
|
||||||
env_id: Environment ID
|
env_id: Environment ID
|
||||||
variable_name: Variable name
|
variable_name: Variable name
|
||||||
|
callback: Callback called with variable value (empty string if not found)
|
||||||
Returns:
|
|
||||||
Variable value (empty string if not found)
|
|
||||||
"""
|
"""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
|
|
||||||
@ -472,25 +556,32 @@ class ProjectManager:
|
|||||||
if p.id == project_id:
|
if p.id == project_id:
|
||||||
# Check if sensitive
|
# Check if sensitive
|
||||||
if variable_name in p.sensitive_variables:
|
if variable_name in p.sensitive_variables:
|
||||||
# Retrieve from keyring
|
# Retrieve from keyring asynchronously
|
||||||
secret_manager = get_secret_manager()
|
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,
|
project_id=project_id,
|
||||||
environment_id=env_id,
|
environment_id=env_id,
|
||||||
variable_name=variable_name
|
variable_name=variable_name,
|
||||||
|
callback=on_retrieve
|
||||||
)
|
)
|
||||||
return value or ""
|
return
|
||||||
else:
|
else:
|
||||||
# Retrieve from JSON
|
# Retrieve from JSON synchronously
|
||||||
for env in p.environments:
|
for env in p.environments:
|
||||||
if env.id == env_id:
|
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.
|
This is a convenience method that handles both sensitive and non-sensitive variables.
|
||||||
|
|
||||||
@ -499,6 +590,7 @@ class ProjectManager:
|
|||||||
env_id: Environment ID
|
env_id: Environment ID
|
||||||
variable_name: Variable name
|
variable_name: Variable name
|
||||||
value: Value to set
|
value: Value to set
|
||||||
|
callback: Optional callback called with success boolean
|
||||||
"""
|
"""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
secret_manager = get_secret_manager()
|
secret_manager = get_secret_manager()
|
||||||
@ -507,7 +599,7 @@ class ProjectManager:
|
|||||||
if p.id == project_id:
|
if p.id == project_id:
|
||||||
# Check if sensitive
|
# Check if sensitive
|
||||||
if variable_name in p.sensitive_variables:
|
if variable_name in p.sensitive_variables:
|
||||||
# Store in keyring
|
# Store in keyring asynchronously
|
||||||
for env in p.environments:
|
for env in p.environments:
|
||||||
if env.id == env_id:
|
if env.id == env_id:
|
||||||
secret_manager.store_secret(
|
secret_manager.store_secret(
|
||||||
@ -516,24 +608,32 @@ class ProjectManager:
|
|||||||
variable_name=variable_name,
|
variable_name=variable_name,
|
||||||
value=value,
|
value=value,
|
||||||
project_name=p.name,
|
project_name=p.name,
|
||||||
environment_name=env.name
|
environment_name=env.name,
|
||||||
|
callback=callback
|
||||||
)
|
)
|
||||||
# Keep empty placeholder in JSON
|
# Keep empty placeholder in JSON
|
||||||
env.variables[variable_name] = ""
|
env.variables[variable_name] = ""
|
||||||
break
|
self.save_projects(projects)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
# Store in JSON
|
# Store in JSON synchronously
|
||||||
for env in p.environments:
|
for env in p.environments:
|
||||||
if env.id == env_id:
|
if env.id == env_id:
|
||||||
env.variables[variable_name] = value
|
env.variables[variable_name] = value
|
||||||
break
|
break
|
||||||
|
|
||||||
self.save_projects(projects)
|
self.save_projects(projects)
|
||||||
return
|
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
|
This is useful for variable substitution - it returns a complete Environment
|
||||||
object with all values populated from both JSON and keyring.
|
object with all values populated from both JSON and keyring.
|
||||||
@ -541,9 +641,7 @@ class ProjectManager:
|
|||||||
Args:
|
Args:
|
||||||
project_id: Project ID
|
project_id: Project ID
|
||||||
env_id: Environment ID
|
env_id: Environment ID
|
||||||
|
callback: Callback called with Environment object (or None if not found)
|
||||||
Returns:
|
|
||||||
Environment object with all values, or None if not found
|
|
||||||
"""
|
"""
|
||||||
projects = self.load_projects()
|
projects = self.load_projects()
|
||||||
secret_manager = get_secret_manager()
|
secret_manager = get_secret_manager()
|
||||||
@ -560,16 +658,24 @@ class ProjectManager:
|
|||||||
created_at=env.created_at
|
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
|
# Fill in sensitive variable values from keyring
|
||||||
for var_name in p.sensitive_variables:
|
def on_secrets_retrieved(secrets_dict):
|
||||||
value = secret_manager.retrieve_secret(
|
for var_name, value in secrets_dict.items():
|
||||||
project_id=project_id,
|
if value is not None:
|
||||||
environment_id=env_id,
|
complete_env.variables[var_name] = value
|
||||||
variable_name=var_name
|
callback(complete_env)
|
||||||
)
|
|
||||||
if value is not None:
|
|
||||||
complete_env.variables[var_name] = value
|
|
||||||
|
|
||||||
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
|
# Always update visual indicators when environment changes
|
||||||
self._update_variable_indicators()
|
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.
|
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:
|
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)
|
# Get environment with secrets (includes values from keyring) - async
|
||||||
return self.project_manager.get_environment_with_secrets(
|
self.project_manager.get_environment_with_secrets(
|
||||||
self.project_id,
|
self.project_id,
|
||||||
self.selected_environment_id
|
self.selected_environment_id,
|
||||||
|
callback
|
||||||
)
|
)
|
||||||
|
|
||||||
def _detect_undefined_variables(self):
|
def _detect_undefined_variables(self, callback):
|
||||||
"""Detect undefined variables in the current request. Returns set of undefined variable names."""
|
"""Detect undefined variables in the current request (async).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback: Function called with set of undefined variable names
|
||||||
|
"""
|
||||||
from .variable_substitution import VariableSubstitution
|
from .variable_substitution import VariableSubstitution
|
||||||
|
|
||||||
# Get current request from UI
|
# Get current request from UI
|
||||||
request = self.get_request()
|
request = self.get_request()
|
||||||
|
|
||||||
# Get selected environment
|
def on_environment_loaded(env):
|
||||||
env = self.get_selected_environment()
|
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:
|
# Get selected environment asynchronously
|
||||||
# No environment selected - ALL variables are undefined
|
self.get_selected_environment(on_environment_loaded)
|
||||||
# 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
|
|
||||||
|
|
||||||
def _schedule_indicator_update(self):
|
def _schedule_indicator_update(self):
|
||||||
"""Schedule an indicator update with debouncing."""
|
"""Schedule an indicator update with debouncing."""
|
||||||
@ -1374,22 +1384,26 @@ class RequestTabWidget(Gtk.Box):
|
|||||||
return False # Don't repeat
|
return False # Don't repeat
|
||||||
|
|
||||||
def _update_variable_indicators(self):
|
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)
|
# Only update if we have a project (variables only make sense in project context)
|
||||||
if not self.project_id:
|
if not self.project_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Detect undefined variables
|
def on_undefined_detected(undefined_vars):
|
||||||
self.undefined_variables = self._detect_undefined_variables()
|
# Store detected undefined variables
|
||||||
|
self.undefined_variables = undefined_vars
|
||||||
|
|
||||||
# Update URL entry
|
# Update URL entry
|
||||||
self._update_url_indicator()
|
self._update_url_indicator()
|
||||||
|
|
||||||
# Update headers
|
# Update headers
|
||||||
self._update_header_indicators()
|
self._update_header_indicators()
|
||||||
|
|
||||||
# Update body
|
# Update body
|
||||||
self._update_body_indicators()
|
self._update_body_indicators()
|
||||||
|
|
||||||
|
# Detect undefined variables asynchronously
|
||||||
|
self._detect_undefined_variables(on_undefined_detected)
|
||||||
|
|
||||||
def _update_url_indicator(self):
|
def _update_url_indicator(self):
|
||||||
"""Update warning indicator on URL entry."""
|
"""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
|
This module provides a clean interface to libsecret for storing environment
|
||||||
variable values that contain sensitive data (API keys, passwords, tokens).
|
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:
|
Each secret is identified by:
|
||||||
- project_id: UUID of the project
|
- project_id: UUID of the project
|
||||||
- environment_id: UUID of the environment
|
- environment_id: UUID of the environment
|
||||||
@ -31,8 +34,9 @@ Each secret is identified by:
|
|||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version('Secret', '1')
|
gi.require_version('Secret', '1')
|
||||||
from gi.repository import Secret, GLib
|
from gi.repository import Secret, GLib, Gio
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable, Optional, Any
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -50,7 +54,10 @@ ROSTER_SCHEMA = Secret.Schema.new(
|
|||||||
|
|
||||||
|
|
||||||
class SecretManager:
|
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):
|
def __init__(self):
|
||||||
"""Initialize the secret manager."""
|
"""Initialize the secret manager."""
|
||||||
@ -58,9 +65,10 @@ class SecretManager:
|
|||||||
|
|
||||||
def store_secret(self, project_id: str, environment_id: str,
|
def store_secret(self, project_id: str, environment_id: str,
|
||||||
variable_name: str, value: str, project_name: 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:
|
Args:
|
||||||
project_id: UUID of the project
|
project_id: UUID of the project
|
||||||
@ -69,9 +77,7 @@ class SecretManager:
|
|||||||
value: The secret value to store
|
value: The secret value to store
|
||||||
project_name: Optional human-readable project name (for display in Seahorse)
|
project_name: Optional human-readable project name (for display in Seahorse)
|
||||||
environment_name: Optional human-readable environment name (for display)
|
environment_name: Optional human-readable environment name (for display)
|
||||||
|
callback: Optional callback function called with success boolean
|
||||||
Returns:
|
|
||||||
True if successful, False otherwise
|
|
||||||
"""
|
"""
|
||||||
# Create a descriptive label for the keyring UI
|
# Create a descriptive label for the keyring UI
|
||||||
label = f"Roster: {project_name}/{environment_name}/{variable_name}" if project_name else \
|
label = f"Roster: {project_name}/{environment_name}/{variable_name}" if project_name else \
|
||||||
@ -84,36 +90,40 @@ class SecretManager:
|
|||||||
"variable_name": variable_name,
|
"variable_name": variable_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
def on_store_complete(source, result, user_data):
|
||||||
# Store the secret synchronously
|
try:
|
||||||
# Using PASSWORD collection (default keyring)
|
Secret.password_store_finish(result)
|
||||||
Secret.password_store_sync(
|
logger.info(f"Stored secret for {variable_name} in {environment_id}")
|
||||||
self.schema,
|
if callback:
|
||||||
attributes,
|
callback(True)
|
||||||
Secret.COLLECTION_DEFAULT,
|
except GLib.Error as e:
|
||||||
label,
|
logger.error(f"Failed to store secret: {e.message}")
|
||||||
value,
|
if callback:
|
||||||
None # cancellable
|
callback(False)
|
||||||
)
|
|
||||||
logger.info(f"Stored secret for {variable_name} in {environment_id}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except GLib.Error as e:
|
# Store the secret asynchronously
|
||||||
logger.error(f"Failed to store secret: {e.message}")
|
Secret.password_store(
|
||||||
return False
|
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,
|
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:
|
Args:
|
||||||
project_id: UUID of the project
|
project_id: UUID of the project
|
||||||
environment_id: UUID of the environment
|
environment_id: UUID of the environment
|
||||||
variable_name: Name of the variable
|
variable_name: Name of the variable
|
||||||
|
callback: Callback function called with the secret value (or None if not found)
|
||||||
Returns:
|
|
||||||
The secret value if found, None otherwise
|
|
||||||
"""
|
"""
|
||||||
attributes = {
|
attributes = {
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
@ -121,37 +131,38 @@ class SecretManager:
|
|||||||
"variable_name": variable_name,
|
"variable_name": variable_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
def on_lookup_complete(source, result, user_data):
|
||||||
# Retrieve the secret synchronously
|
try:
|
||||||
value = Secret.password_lookup_sync(
|
value = Secret.password_lookup_finish(result)
|
||||||
self.schema,
|
if value is None:
|
||||||
attributes,
|
logger.debug(f"No secret found for {variable_name}")
|
||||||
None # cancellable
|
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:
|
# Retrieve the secret asynchronously
|
||||||
logger.debug(f"No secret found for {variable_name}")
|
Secret.password_lookup(
|
||||||
else:
|
self.schema,
|
||||||
logger.debug(f"Retrieved secret for {variable_name}")
|
attributes,
|
||||||
|
None, # cancellable
|
||||||
return value
|
on_lookup_complete,
|
||||||
|
None # user_data
|
||||||
except GLib.Error as e:
|
)
|
||||||
logger.error(f"Failed to retrieve secret: {e.message}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def delete_secret(self, project_id: str, environment_id: str,
|
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:
|
Args:
|
||||||
project_id: UUID of the project
|
project_id: UUID of the project
|
||||||
environment_id: UUID of the environment
|
environment_id: UUID of the environment
|
||||||
variable_name: Name of the variable
|
variable_name: Name of the variable
|
||||||
|
callback: Optional callback function called with success boolean
|
||||||
Returns:
|
|
||||||
True if deleted (or didn't exist), False on error
|
|
||||||
"""
|
"""
|
||||||
attributes = {
|
attributes = {
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
@ -159,85 +170,98 @@ class SecretManager:
|
|||||||
"variable_name": variable_name,
|
"variable_name": variable_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
def on_clear_complete(source, result, user_data):
|
||||||
# Delete the secret synchronously
|
try:
|
||||||
deleted = Secret.password_clear_sync(
|
deleted = Secret.password_clear_finish(result)
|
||||||
self.schema,
|
if deleted:
|
||||||
attributes,
|
logger.info(f"Deleted secret for {variable_name}")
|
||||||
None # cancellable
|
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:
|
# Delete the secret asynchronously
|
||||||
logger.info(f"Deleted secret for {variable_name}")
|
Secret.password_clear(
|
||||||
else:
|
self.schema,
|
||||||
logger.debug(f"No secret to delete for {variable_name}")
|
attributes,
|
||||||
|
None, # cancellable
|
||||||
|
on_clear_complete,
|
||||||
|
None # user_data
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
def delete_all_project_secrets(self, project_id: str,
|
||||||
|
callback: Optional[Callable[[bool], None]] = None):
|
||||||
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:
|
|
||||||
"""
|
"""
|
||||||
Delete all secrets for a project (when project is deleted).
|
Delete all secrets for a project (when project is deleted).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_id: UUID of the project
|
project_id: UUID of the project
|
||||||
|
callback: Optional callback function called with success boolean
|
||||||
Returns:
|
|
||||||
True if successful, False otherwise
|
|
||||||
"""
|
"""
|
||||||
attributes = {
|
attributes = {
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
def on_clear_complete(source, result, user_data):
|
||||||
# Delete all matching secrets
|
try:
|
||||||
deleted = Secret.password_clear_sync(
|
Secret.password_clear_finish(result)
|
||||||
self.schema,
|
logger.info(f"Deleted all secrets for project {project_id}")
|
||||||
attributes,
|
if callback:
|
||||||
None
|
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}")
|
# Delete all matching secrets asynchronously
|
||||||
return True
|
Secret.password_clear(
|
||||||
|
self.schema,
|
||||||
|
attributes,
|
||||||
|
None,
|
||||||
|
on_clear_complete,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
except GLib.Error as e:
|
def delete_all_environment_secrets(self, project_id: str, environment_id: str,
|
||||||
logger.error(f"Failed to delete project secrets: {e.message}")
|
callback: Optional[Callable[[bool], None]] = None):
|
||||||
return False
|
|
||||||
|
|
||||||
def delete_all_environment_secrets(self, project_id: str, environment_id: str) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Delete all secrets for an environment (when environment is deleted).
|
Delete all secrets for an environment (when environment is deleted).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_id: UUID of the project
|
project_id: UUID of the project
|
||||||
environment_id: UUID of the environment
|
environment_id: UUID of the environment
|
||||||
|
callback: Optional callback function called with success boolean
|
||||||
Returns:
|
|
||||||
True if successful, False otherwise
|
|
||||||
"""
|
"""
|
||||||
attributes = {
|
attributes = {
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"environment_id": environment_id,
|
"environment_id": environment_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
def on_clear_complete(source, result, user_data):
|
||||||
deleted = Secret.password_clear_sync(
|
try:
|
||||||
self.schema,
|
Secret.password_clear_finish(result)
|
||||||
attributes,
|
logger.info(f"Deleted all secrets for environment {environment_id}")
|
||||||
None
|
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}")
|
Secret.password_clear(
|
||||||
return True
|
self.schema,
|
||||||
|
attributes,
|
||||||
|
None,
|
||||||
|
on_clear_complete,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
except GLib.Error as e:
|
def rename_variable_secrets(self, project_id: str, old_name: str, new_name: str,
|
||||||
logger.error(f"Failed to delete environment secrets: {e.message}")
|
callback: Optional[Callable[[bool], None]] = None):
|
||||||
return False
|
|
||||||
|
|
||||||
def rename_variable_secrets(self, project_id: str, old_name: str, new_name: str) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Rename a variable across all environments (updates secret keys).
|
Rename a variable across all environments (updates secret keys).
|
||||||
|
|
||||||
@ -250,65 +274,174 @@ class SecretManager:
|
|||||||
project_id: UUID of the project
|
project_id: UUID of the project
|
||||||
old_name: Current variable name
|
old_name: Current variable name
|
||||||
new_name: New variable name
|
new_name: New variable name
|
||||||
|
callback: Optional callback function called with success boolean
|
||||||
Returns:
|
|
||||||
True if successful, False otherwise
|
|
||||||
"""
|
"""
|
||||||
# Search for all secrets with the old variable name
|
|
||||||
attributes = {
|
attributes = {
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"variable_name": old_name,
|
"variable_name": old_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
def on_search_complete(source, result, user_data):
|
||||||
# Search for matching secrets
|
try:
|
||||||
# This returns a list of Secret.Item objects
|
secrets = Secret.password_search_finish(result)
|
||||||
secrets = Secret.password_search_sync(
|
|
||||||
self.schema,
|
|
||||||
attributes,
|
|
||||||
Secret.SearchFlags.ALL,
|
|
||||||
None
|
|
||||||
)
|
|
||||||
|
|
||||||
if not secrets:
|
if not secrets:
|
||||||
logger.debug(f"No secrets found for variable {old_name}")
|
logger.debug(f"No secrets found for variable {old_name}")
|
||||||
return True
|
if callback:
|
||||||
|
callback(True)
|
||||||
|
return
|
||||||
|
|
||||||
# For each secret, retrieve value, store with new name, delete old
|
# Track how many secrets we need to process
|
||||||
for item in secrets:
|
pending_count = [len(secrets)]
|
||||||
# Get the secret value
|
all_success = [True]
|
||||||
item.load_secret_sync(None)
|
|
||||||
value = item.get_secret().get_text()
|
|
||||||
|
|
||||||
# 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()
|
attrs = item.get_attributes()
|
||||||
environment_id = attrs.get("environment_id")
|
environment_id = attrs.get("environment_id")
|
||||||
|
|
||||||
if not environment_id:
|
if not environment_id:
|
||||||
logger.warning(f"Secret missing environment_id, skipping")
|
logger.warning("Secret missing environment_id, skipping")
|
||||||
continue
|
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(
|
self.store_secret(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
environment_id=environment_id,
|
environment_id=environment_id,
|
||||||
variable_name=new_name,
|
variable_name=new_name,
|
||||||
value=value
|
value=value,
|
||||||
|
callback=on_store_done
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete old secret
|
except GLib.Error as e:
|
||||||
self.delete_secret(
|
logger.error(f"Failed to load secret for rename: {e.message}")
|
||||||
project_id=project_id,
|
callback(False)
|
||||||
environment_id=environment_id,
|
|
||||||
variable_name=old_name
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Renamed variable secrets from {old_name} to {new_name}")
|
item.load_secret(None, on_load_complete, None)
|
||||||
return True
|
|
||||||
|
|
||||||
except GLib.Error as e:
|
# ========== Batch Operations ==========
|
||||||
logger.error(f"Failed to rename variable secrets: {e.message}")
|
|
||||||
return False
|
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
|
# Singleton instance
|
||||||
|
|||||||
@ -107,22 +107,12 @@ class VariableDataRow(Gtk.Box):
|
|||||||
entry.set_placeholder_text(env.name)
|
entry.set_placeholder_text(env.name)
|
||||||
entry.set_hexpand(True)
|
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
|
# Configure entry for sensitive variables
|
||||||
if is_sensitive:
|
if is_sensitive:
|
||||||
entry.set_visibility(False) # Show bullets instead of text
|
entry.set_visibility(False) # Show bullets instead of text
|
||||||
entry.set_input_purpose(Gtk.InputPurpose.PASSWORD)
|
entry.set_input_purpose(Gtk.InputPurpose.PASSWORD)
|
||||||
entry.add_css_class("sensitive-variable")
|
entry.add_css_class("sensitive-variable")
|
||||||
|
|
||||||
entry.connect('changed', self._on_value_changed, env.id)
|
|
||||||
|
|
||||||
entry_box.append(entry)
|
entry_box.append(entry)
|
||||||
|
|
||||||
# Add entry box to size group for alignment
|
# Add entry box to size group for alignment
|
||||||
@ -132,17 +122,34 @@ class VariableDataRow(Gtk.Box):
|
|||||||
self.values_box.append(entry_box)
|
self.values_box.append(entry_box)
|
||||||
self.value_entries[env.id] = entry
|
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):
|
def _on_value_changed(self, entry, env_id):
|
||||||
"""Handle value entry changes."""
|
"""Handle value entry changes."""
|
||||||
value = entry.get_text()
|
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_manager.set_variable_value(
|
||||||
self.project.id,
|
self.project.id,
|
||||||
env_id,
|
env_id,
|
||||||
self.variable_name,
|
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):
|
def _on_name_entry_focus_in(self, controller):
|
||||||
"""Show edit buttons when entry gains focus."""
|
"""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:
|
if preprocessing_result.modified_request:
|
||||||
modified_request = preprocessing_result.modified_request
|
modified_request = preprocessing_result.modified_request
|
||||||
|
|
||||||
# Apply variable substitution to (possibly modified) request
|
# Disable send button during request
|
||||||
substituted_request = modified_request
|
widget.send_button.set_sensitive(False)
|
||||||
if widget.selected_environment_id:
|
|
||||||
env = widget.get_selected_environment()
|
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:
|
if env:
|
||||||
from .variable_substitution import VariableSubstitution
|
from .variable_substitution import VariableSubstitution
|
||||||
substituted_request, undefined = VariableSubstitution.substitute_request(modified_request, env)
|
substituted_request, undefined = VariableSubstitution.substitute_request(modified_request, env)
|
||||||
@ -546,65 +549,60 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
if undefined:
|
if undefined:
|
||||||
logger.warning(f"Undefined variables in request: {', '.join(undefined)}")
|
logger.warning(f"Undefined variables in request: {', '.join(undefined)}")
|
||||||
|
|
||||||
# Disable send button during request
|
# Execute async
|
||||||
widget.send_button.set_sensitive(False)
|
def callback(response, error, user_data):
|
||||||
|
"""Callback runs on main thread."""
|
||||||
|
# Re-enable send button
|
||||||
|
widget.send_button.set_sensitive(True)
|
||||||
|
|
||||||
# Execute async
|
# Update tab with response
|
||||||
def callback(response, error, user_data):
|
if response:
|
||||||
"""Callback runs on main thread."""
|
widget.display_response(response)
|
||||||
# Re-enable send button
|
|
||||||
widget.send_button.set_sensitive(True)
|
|
||||||
|
|
||||||
# Update tab with response
|
# Execute postprocessing script if exists
|
||||||
if response:
|
scripts = widget.get_scripts()
|
||||||
widget.display_response(response)
|
if scripts and scripts.postprocessing.strip():
|
||||||
|
from .script_executor import ScriptExecutor, ScriptContext
|
||||||
|
|
||||||
# Execute postprocessing script if exists
|
# Create context for variable updates
|
||||||
scripts = widget.get_scripts()
|
context = None
|
||||||
if scripts and scripts.postprocessing.strip():
|
if widget.project_id and widget.selected_environment_id:
|
||||||
from .script_executor import ScriptExecutor, ScriptContext
|
context = ScriptContext(
|
||||||
|
project_id=widget.project_id,
|
||||||
|
environment_id=widget.selected_environment_id,
|
||||||
|
project_manager=self.project_manager
|
||||||
|
)
|
||||||
|
|
||||||
# Create context for variable updates
|
# Execute script with context
|
||||||
context = None
|
script_result = ScriptExecutor.execute_postprocessing_script(
|
||||||
if widget.project_id and widget.selected_environment_id:
|
scripts.postprocessing,
|
||||||
context = ScriptContext(
|
response,
|
||||||
project_id=widget.project_id,
|
context
|
||||||
environment_id=widget.selected_environment_id,
|
|
||||||
project_manager=self.project_manager
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute script with context
|
# Display script results
|
||||||
script_result = ScriptExecutor.execute_postprocessing_script(
|
widget.display_script_results(script_result)
|
||||||
scripts.postprocessing,
|
|
||||||
response,
|
|
||||||
context
|
|
||||||
)
|
|
||||||
|
|
||||||
# Display script results
|
# Process variable updates
|
||||||
widget.display_script_results(script_result)
|
self._process_variable_updates(script_result, widget, context)
|
||||||
|
else:
|
||||||
# Process variable updates
|
widget._clear_script_results()
|
||||||
self._process_variable_updates(script_result, widget, context)
|
|
||||||
else:
|
else:
|
||||||
|
widget.display_error(error or "Unknown error")
|
||||||
widget._clear_script_results()
|
widget._clear_script_results()
|
||||||
else:
|
|
||||||
widget.display_error(error or "Unknown error")
|
|
||||||
widget._clear_script_results()
|
|
||||||
|
|
||||||
# Save response to tab
|
# Save response to tab
|
||||||
page = self.tab_view.get_selected_page()
|
page = self.tab_view.get_selected_page()
|
||||||
if page:
|
if page:
|
||||||
tab = self.page_to_tab.get(page)
|
tab = self.page_to_tab.get(page)
|
||||||
if tab:
|
if tab:
|
||||||
tab.response = response
|
tab.response = response
|
||||||
|
|
||||||
# Create history entry with redacted sensitive variables
|
# Create history entry with redacted sensitive variables
|
||||||
# Determine which request to save to history
|
# Determine which request to save to history
|
||||||
request_for_history = modified_request
|
request_for_history = modified_request
|
||||||
has_redacted = False
|
has_redacted = False
|
||||||
|
|
||||||
if widget.selected_environment_id:
|
|
||||||
env = widget.get_selected_environment()
|
|
||||||
if env and widget.project_id:
|
if env and widget.project_id:
|
||||||
# Get the project's sensitive variables list
|
# Get the project's sensitive variables list
|
||||||
projects = self.project_manager.load_projects()
|
projects = self.project_manager.load_projects()
|
||||||
@ -624,22 +622,28 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
# No sensitive variables defined, use fully substituted request
|
# No sensitive variables defined, use fully substituted request
|
||||||
request_for_history = substituted_request
|
request_for_history = substituted_request
|
||||||
elif env:
|
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
|
request_for_history = substituted_request
|
||||||
|
|
||||||
entry = HistoryEntry(
|
entry = HistoryEntry(
|
||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
request=request_for_history,
|
request=request_for_history,
|
||||||
response=response,
|
response=response,
|
||||||
error=error,
|
error=error,
|
||||||
has_redacted_variables=has_redacted
|
has_redacted_variables=has_redacted
|
||||||
)
|
)
|
||||||
self.history_manager.add_entry(entry)
|
self.history_manager.add_entry(entry)
|
||||||
|
|
||||||
# Refresh history panel to show new entry
|
# Refresh history panel to show new entry
|
||||||
self._load_history()
|
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
|
# History and Project Management
|
||||||
def _load_history(self) -> None:
|
def _load_history(self) -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user