"""Session-based permission management with time-based expiry."""
import subprocess
import time
from dataclasses import dataclass
from typing import Optional
from .config import load_config
# === DATA STRUCTURES === #
@dataclass
class Permission:
"""A granted permission for a secret."""
secret_name: str
granted_at: float # Kept for audit trail
expires_at: float
def is_expired(self) -> bool:
return time.time() > self.expires_at
# === PUBLIC API === #
class PermissionManager:
"""Manages session permissions for secret access."""
def __init__(self, timeout_seconds: Optional[int] = None):
config = load_config()
self.timeout = timeout_seconds or config.get("session_timeout", 3600)
self._permissions: dict[str, Permission] = {}
def is_granted(self, secret_name: str) -> bool:
"""Check if permission is granted and not expired."""
perm = self._permissions.get(secret_name)
if perm is None:
return False
if perm.is_expired():
del self._permissions[secret_name]
return False
return True
def grant(self, secret_name: str) -> None:
"""Grant permission for a secret."""
now = time.time()
self._permissions[secret_name] = Permission(
secret_name=secret_name,
granted_at=now,
expires_at=now + self.timeout,
)
def revoke(self, secret_name: str) -> None:
"""Revoke permission for a secret."""
self._permissions.pop(secret_name, None)
def revoke_all(self) -> None:
"""Revoke all permissions."""
self._permissions.clear()
def get_status(self) -> list[dict]:
"""Get status of all permissions."""
now = time.time()
result = []
expired = []
for name, perm in self._permissions.items():
if perm.is_expired():
expired.append(name)
else:
result.append({
"name": name,
"status": "granted",
"expires_in": int(perm.expires_at - now),
})
for name in expired:
del self._permissions[name]
return result
def get_pending_secrets(self, requested: list[str]) -> list[str]:
"""Get list of secrets that need permission from requested list."""
return [name for name in requested if not self.is_granted(name)]
# === MODULE HELPERS === #
def request_permission(secret_name: str, description: str, command: str) -> bool:
"""Request permission via native macOS dialog. Blocks until user responds."""
def escape_js(s: str) -> str:
return s.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'").replace('\n', '\\n')
desc_line = f'"{escape_js(description)}"' if description else '""'
cmd_line = f'"{escape_js(command)}"' if command else '""'
script = f'''
ObjC.import('Cocoa');
var app = $.NSApplication.sharedApplication;
app.setActivationPolicy($.NSApplicationActivationPolicyAccessory);
var alert = $.NSAlert.alloc.init;
alert.messageText = $("Allow Secret Access?");
var info = "{escape_js(secret_name)}";
var desc = {desc_line};
var cmd = {cmd_line};
if (desc.length > 0) info += "\\n" + desc;
if (cmd.length > 0) info += "\\n\\nCommand:\\n" + cmd;
alert.informativeText = $(info);
alert.addButtonWithTitle($("Allow"));
alert.addButtonWithTitle($("Deny"));
alert.alertStyle = $.NSAlertStyleWarning;
app.activateIgnoringOtherApps(true);
var response = alert.runModal;
response === $.NSAlertFirstButtonReturn;
'''
try:
result = subprocess.run(
["osascript", "-l", "JavaScript", "-e", script],
capture_output=True,
text=True,
timeout=120
)
return result.returncode == 0 and result.stdout.strip() == "true"
except subprocess.TimeoutExpired:
return False
except Exception:
return False