Apply recent changes
This commit is contained in:
@@ -13,10 +13,23 @@ pub struct ConsentRequest {
|
||||
pub tool_name: String,
|
||||
}
|
||||
|
||||
/// Scope of consent grant
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ConsentScope {
|
||||
/// Grant only for this single operation
|
||||
Once,
|
||||
/// Grant for the duration of the current session
|
||||
Session,
|
||||
/// Grant permanently (persisted across sessions)
|
||||
Permanent,
|
||||
/// Explicitly denied
|
||||
Denied,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ConsentRecord {
|
||||
pub tool_name: String,
|
||||
pub granted: bool,
|
||||
pub scope: ConsentScope,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub data_types: Vec<String>,
|
||||
pub external_endpoints: Vec<String>,
|
||||
@@ -24,7 +37,17 @@ pub struct ConsentRecord {
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct ConsentManager {
|
||||
records: HashMap<String, ConsentRecord>,
|
||||
/// Permanent consent records (persisted to vault)
|
||||
permanent_records: HashMap<String, ConsentRecord>,
|
||||
/// Session-scoped consent (cleared on manager drop or explicit clear)
|
||||
#[serde(skip)]
|
||||
session_records: HashMap<String, ConsentRecord>,
|
||||
/// Once-scoped consent (used once then cleared)
|
||||
#[serde(skip)]
|
||||
once_records: HashMap<String, ConsentRecord>,
|
||||
/// Pending consent requests (to prevent duplicate prompts)
|
||||
#[serde(skip)]
|
||||
pending_requests: HashMap<String, ()>,
|
||||
}
|
||||
|
||||
impl ConsentManager {
|
||||
@@ -36,19 +59,24 @@ impl ConsentManager {
|
||||
pub fn from_vault(vault: &Arc<std::sync::Mutex<VaultHandle>>) -> Self {
|
||||
let guard = vault.lock().expect("Vault mutex poisoned");
|
||||
if let Some(consent_data) = guard.settings().get("consent_records") {
|
||||
if let Ok(records) =
|
||||
if let Ok(permanent_records) =
|
||||
serde_json::from_value::<HashMap<String, ConsentRecord>>(consent_data.clone())
|
||||
{
|
||||
return Self { records };
|
||||
return Self {
|
||||
permanent_records,
|
||||
session_records: HashMap::new(),
|
||||
once_records: HashMap::new(),
|
||||
pending_requests: HashMap::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Persist consent records to vault storage
|
||||
/// Persist permanent consent records to vault storage
|
||||
pub fn persist_to_vault(&self, vault: &Arc<std::sync::Mutex<VaultHandle>>) -> Result<()> {
|
||||
let mut guard = vault.lock().expect("Vault mutex poisoned");
|
||||
let consent_json = serde_json::to_value(&self.records)?;
|
||||
let consent_json = serde_json::to_value(&self.permanent_records)?;
|
||||
guard
|
||||
.settings_mut()
|
||||
.insert("consent_records".to_string(), consent_json);
|
||||
@@ -61,24 +89,60 @@ impl ConsentManager {
|
||||
tool_name: &str,
|
||||
data_types: Vec<String>,
|
||||
endpoints: Vec<String>,
|
||||
) -> Result<bool> {
|
||||
if let Some(existing) = self.records.get(tool_name) {
|
||||
return Ok(existing.granted);
|
||||
) -> Result<ConsentScope> {
|
||||
// Check if already granted permanently
|
||||
if let Some(existing) = self.permanent_records.get(tool_name) {
|
||||
if existing.scope == ConsentScope::Permanent {
|
||||
return Ok(ConsentScope::Permanent);
|
||||
}
|
||||
}
|
||||
|
||||
let consent = self.show_consent_dialog(tool_name, &data_types, &endpoints)?;
|
||||
// Check if granted for session
|
||||
if let Some(existing) = self.session_records.get(tool_name) {
|
||||
if existing.scope == ConsentScope::Session {
|
||||
return Ok(ConsentScope::Session);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if request is already pending (prevent duplicate prompts)
|
||||
if self.pending_requests.contains_key(tool_name) {
|
||||
// Wait for the other prompt to complete by returning denied temporarily
|
||||
// The caller should retry after a short delay
|
||||
return Ok(ConsentScope::Denied);
|
||||
}
|
||||
|
||||
// Mark as pending
|
||||
self.pending_requests.insert(tool_name.to_string(), ());
|
||||
|
||||
// Show consent dialog and get scope
|
||||
let scope = self.show_consent_dialog(tool_name, &data_types, &endpoints)?;
|
||||
|
||||
// Remove from pending
|
||||
self.pending_requests.remove(tool_name);
|
||||
|
||||
// Create record based on scope
|
||||
let record = ConsentRecord {
|
||||
tool_name: tool_name.to_string(),
|
||||
granted: consent,
|
||||
scope: scope.clone(),
|
||||
timestamp: Utc::now(),
|
||||
data_types,
|
||||
external_endpoints: endpoints,
|
||||
};
|
||||
|
||||
self.records.insert(tool_name.to_string(), record);
|
||||
// Note: Caller should persist to vault after this call
|
||||
Ok(consent)
|
||||
// Store in appropriate location
|
||||
match scope {
|
||||
ConsentScope::Permanent => {
|
||||
self.permanent_records.insert(tool_name.to_string(), record);
|
||||
}
|
||||
ConsentScope::Session => {
|
||||
self.session_records.insert(tool_name.to_string(), record);
|
||||
}
|
||||
ConsentScope::Once | ConsentScope::Denied => {
|
||||
// Don't store, just return the decision
|
||||
}
|
||||
}
|
||||
|
||||
Ok(scope)
|
||||
}
|
||||
|
||||
/// Grant consent programmatically (for TUI or automated flows)
|
||||
@@ -87,15 +151,38 @@ impl ConsentManager {
|
||||
tool_name: &str,
|
||||
data_types: Vec<String>,
|
||||
endpoints: Vec<String>,
|
||||
) {
|
||||
self.grant_consent_with_scope(tool_name, data_types, endpoints, ConsentScope::Permanent);
|
||||
}
|
||||
|
||||
/// Grant consent with specific scope
|
||||
pub fn grant_consent_with_scope(
|
||||
&mut self,
|
||||
tool_name: &str,
|
||||
data_types: Vec<String>,
|
||||
endpoints: Vec<String>,
|
||||
scope: ConsentScope,
|
||||
) {
|
||||
let record = ConsentRecord {
|
||||
tool_name: tool_name.to_string(),
|
||||
granted: true,
|
||||
scope: scope.clone(),
|
||||
timestamp: Utc::now(),
|
||||
data_types,
|
||||
external_endpoints: endpoints,
|
||||
};
|
||||
self.records.insert(tool_name.to_string(), record);
|
||||
|
||||
match scope {
|
||||
ConsentScope::Permanent => {
|
||||
self.permanent_records.insert(tool_name.to_string(), record);
|
||||
}
|
||||
ConsentScope::Session => {
|
||||
self.session_records.insert(tool_name.to_string(), record);
|
||||
}
|
||||
ConsentScope::Once => {
|
||||
self.once_records.insert(tool_name.to_string(), record);
|
||||
}
|
||||
ConsentScope::Denied => {} // Denied is not stored
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if consent is needed (returns None if already granted, Some(info) if needed)
|
||||
@@ -110,21 +197,44 @@ impl ConsentManager {
|
||||
}
|
||||
|
||||
pub fn has_consent(&self, tool_name: &str) -> bool {
|
||||
self.records
|
||||
// Check permanent first, then session, then once
|
||||
self.permanent_records
|
||||
.get(tool_name)
|
||||
.map(|record| record.granted)
|
||||
.map(|r| r.scope == ConsentScope::Permanent)
|
||||
.or_else(|| {
|
||||
self.session_records
|
||||
.get(tool_name)
|
||||
.map(|r| r.scope == ConsentScope::Session)
|
||||
})
|
||||
.or_else(|| {
|
||||
self.once_records
|
||||
.get(tool_name)
|
||||
.map(|r| r.scope == ConsentScope::Once)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Consume "once" consent for a tool (clears it after first use)
|
||||
pub fn consume_once_consent(&mut self, tool_name: &str) {
|
||||
self.once_records.remove(tool_name);
|
||||
}
|
||||
|
||||
pub fn revoke_consent(&mut self, tool_name: &str) {
|
||||
if let Some(record) = self.records.get_mut(tool_name) {
|
||||
record.granted = false;
|
||||
record.timestamp = Utc::now();
|
||||
}
|
||||
self.permanent_records.remove(tool_name);
|
||||
self.session_records.remove(tool_name);
|
||||
self.once_records.remove(tool_name);
|
||||
}
|
||||
|
||||
pub fn clear_all_consent(&mut self) {
|
||||
self.records.clear();
|
||||
self.permanent_records.clear();
|
||||
self.session_records.clear();
|
||||
self.once_records.clear();
|
||||
}
|
||||
|
||||
/// Clear only session-scoped consent (useful when starting new session)
|
||||
pub fn clear_session_consent(&mut self) {
|
||||
self.session_records.clear();
|
||||
self.once_records.clear(); // Also clear once consent on session clear
|
||||
}
|
||||
|
||||
/// Check if consent is needed for a tool (non-blocking)
|
||||
@@ -146,27 +256,40 @@ impl ConsentManager {
|
||||
tool_name: &str,
|
||||
data_types: &[String],
|
||||
endpoints: &[String],
|
||||
) -> Result<bool> {
|
||||
// TEMPORARY: Auto-grant consent when not in a proper terminal (TUI mode)
|
||||
) -> Result<ConsentScope> {
|
||||
// TEMPORARY: Auto-grant session consent when not in a proper terminal (TUI mode)
|
||||
// TODO: Integrate consent UI into the TUI event loop
|
||||
use std::io::IsTerminal;
|
||||
if !io::stdin().is_terminal() || std::env::var("OWLEN_AUTO_CONSENT").is_ok() {
|
||||
eprintln!("Auto-granting consent for {} (TUI mode)", tool_name);
|
||||
return Ok(true);
|
||||
eprintln!("Auto-granting session consent for {} (TUI mode)", tool_name);
|
||||
return Ok(ConsentScope::Session);
|
||||
}
|
||||
|
||||
println!("=== PRIVACY CONSENT REQUIRED ===");
|
||||
println!("Tool: {}", tool_name);
|
||||
println!("Data to be sent: {}", data_types.join(", "));
|
||||
println!("External endpoints: {}", endpoints.join(", "));
|
||||
println!("Do you consent to this data transmission? (y/N)");
|
||||
|
||||
print!("> ");
|
||||
println!("\n╔══════════════════════════════════════════════════╗");
|
||||
println!("║ 🔒 PRIVACY CONSENT REQUIRED 🔒 ║");
|
||||
println!("╚══════════════════════════════════════════════════╝");
|
||||
println!();
|
||||
println!("Tool: {}", tool_name);
|
||||
println!("Data: {}", data_types.join(", "));
|
||||
println!("Endpoints: {}", endpoints.join(", "));
|
||||
println!();
|
||||
println!("Choose consent scope:");
|
||||
println!(" [1] Allow once - Grant only for this operation");
|
||||
println!(" [2] Allow session - Grant for current session");
|
||||
println!(" [3] Allow always - Grant permanently");
|
||||
println!(" [4] Deny - Reject this operation");
|
||||
println!();
|
||||
print!("Enter choice (1-4) [default: 4]: ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
Ok(matches!(input.trim().to_lowercase().as_str(), "y" | "yes"))
|
||||
match input.trim() {
|
||||
"1" => Ok(ConsentScope::Once),
|
||||
"2" => Ok(ConsentScope::Session),
|
||||
"3" => Ok(ConsentScope::Permanent),
|
||||
_ => Ok(ConsentScope::Denied),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user