use std::collections::HashMap;
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use futures::channel::mpsc;
use log::debug;
use zbus::Connection;
use crate::ConnectionError;
use crate::dbus::AgentManagerProxy;
use super::iface::SecretAgentInterface;
use super::request::{CancelReason, SecretAgentCapabilities, SecretRequest, SecretStoreEvent};
const DEFAULT_IDENTIFIER: &str = "com.system76.CosmicApplets.nmrs.secret_agent";
const DEFAULT_OBJECT_PATH: &str = "/org/freedesktop/NetworkManager/SecretAgent";
const DEFAULT_QUEUE_DEPTH: usize = 32;
const AGENT_MANAGER_ERROR_PREFIX: &str = "org.freedesktop.NetworkManager.AgentManager.";
#[derive(Debug, PartialEq, Eq)]
enum AgentManagerFailure {
AlreadyRegistered,
Registration,
NotRegistered,
Other,
}
fn classify_agent_manager_failure(name: &str, detail: Option<&str>) -> AgentManagerFailure {
if !name.starts_with(AGENT_MANAGER_ERROR_PREFIX) {
return AgentManagerFailure::Other;
}
match name.strip_prefix(AGENT_MANAGER_ERROR_PREFIX) {
Some("PermissionDenied")
if detail.is_some_and(|detail| {
detail.to_ascii_lowercase().contains("already registered")
}) =>
{
AgentManagerFailure::AlreadyRegistered
}
Some("NotRegistered") => AgentManagerFailure::NotRegistered,
_ => AgentManagerFailure::Registration,
}
}
fn classify_zbus_agent_manager_failure(error: &zbus::Error) -> AgentManagerFailure {
match error {
zbus::Error::MethodError(name, detail, _) => {
classify_agent_manager_failure(name.as_str(), detail.as_deref())
}
_ => AgentManagerFailure::Other,
}
}
fn registration_error(error: zbus::Error, operation: &str) -> ConnectionError {
match classify_zbus_agent_manager_failure(&error) {
AgentManagerFailure::AlreadyRegistered => ConnectionError::AgentAlreadyRegistered,
AgentManagerFailure::Registration | AgentManagerFailure::NotRegistered => {
ConnectionError::AgentRegistration {
context: format!("{operation}: {error}"),
}
}
AgentManagerFailure::Other => ConnectionError::DbusOperation {
context: operation.into(),
source: error,
},
}
}
fn unregistration_error(error: zbus::Error) -> ConnectionError {
if classify_zbus_agent_manager_failure(&error) == AgentManagerFailure::NotRegistered {
ConnectionError::AgentNotRegistered
} else {
ConnectionError::DbusOperation {
context: "unregistering secret agent".into(),
source: error,
}
}
}
pub struct SecretAgent;
impl SecretAgent {
#[must_use]
pub fn builder() -> SecretAgentBuilder {
SecretAgentBuilder::default()
}
}
#[derive(Debug)]
pub struct SecretAgentBuilder {
identifier: String,
capabilities: SecretAgentCapabilities,
object_path: String,
queue_depth: usize,
}
impl Default for SecretAgentBuilder {
fn default() -> Self {
Self {
identifier: DEFAULT_IDENTIFIER.into(),
capabilities: SecretAgentCapabilities::VPN_HINTS,
object_path: DEFAULT_OBJECT_PATH.into(),
queue_depth: DEFAULT_QUEUE_DEPTH,
}
}
}
impl SecretAgentBuilder {
#[must_use]
pub fn with_identifier(mut self, identifier: impl Into<String>) -> Self {
self.identifier = identifier.into();
self
}
#[must_use]
pub fn with_capabilities(mut self, capabilities: SecretAgentCapabilities) -> Self {
self.capabilities = capabilities;
self
}
#[must_use]
pub fn with_object_path(mut self, path: impl Into<String>) -> Self {
self.object_path = path.into();
self
}
#[must_use]
pub fn with_queue_depth(mut self, depth: usize) -> Self {
self.queue_depth = depth;
self
}
pub async fn register(
self,
) -> crate::Result<(SecretAgentHandle, mpsc::Receiver<SecretRequest>)> {
let (request_tx, request_rx) = mpsc::channel(self.queue_depth);
let (cancel_tx, cancel_rx) = mpsc::unbounded();
let (store_tx, store_rx) = mpsc::unbounded();
let iface = SecretAgentInterface {
request_tx,
cancel_tx,
store_tx,
pending: Arc::new(Mutex::new(HashMap::new())),
next_request_id: AtomicU64::new(1),
response_timeout: crate::types::constants::timeouts::secret_agent_response_timeout(),
};
let conn = Connection::system()
.await
.map_err(|e| ConnectionError::DbusOperation {
context: "connecting to system bus for secret agent".into(),
source: e,
})?;
conn.object_server()
.at(&*self.object_path, iface)
.await
.map_err(|e| ConnectionError::DbusOperation {
context: format!("serving SecretAgent interface at {}", self.object_path),
source: e,
})?;
debug!(
"Serving secret agent '{}' at '{}'",
self.identifier, self.object_path
);
let agent_proxy =
AgentManagerProxy::new(&conn)
.await
.map_err(|e| ConnectionError::DbusOperation {
context: "creating AgentManager proxy".into(),
source: e,
})?;
agent_proxy
.register_with_capabilities(&self.identifier, self.capabilities.bits())
.await
.map_err(|error| {
registration_error(error, "registering secret agent with NetworkManager")
})?;
debug!(
"Registered secret agent '{}' with capabilities {:?}",
self.identifier, self.capabilities
);
let handle = SecretAgentHandle {
conn,
identifier: self.identifier,
capabilities: self.capabilities,
object_path: self.object_path,
cancel_rx,
store_rx,
};
Ok((handle, request_rx))
}
}
pub struct SecretAgentHandle {
conn: Connection,
identifier: String,
capabilities: SecretAgentCapabilities,
object_path: String,
cancel_rx: mpsc::UnboundedReceiver<CancelReason>,
store_rx: mpsc::UnboundedReceiver<SecretStoreEvent>,
}
impl std::fmt::Debug for SecretAgentHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretAgentHandle")
.field("identifier", &self.identifier)
.field("object_path", &self.object_path)
.finish_non_exhaustive()
}
}
impl SecretAgentHandle {
pub async fn reregister(&self) -> crate::Result<()> {
let proxy = AgentManagerProxy::new(&self.conn).await.map_err(|e| {
ConnectionError::DbusOperation {
context: "creating AgentManager proxy for re-registration".into(),
source: e,
}
})?;
proxy
.register_with_capabilities(&self.identifier, self.capabilities.bits())
.await
.map_err(|error| {
registration_error(error, "re-registering secret agent with NetworkManager")
})?;
debug!("Re-registered secret agent '{}'", self.identifier);
Ok(())
}
pub async fn unregister(self) -> crate::Result<()> {
let unregistered = async {
let proxy = AgentManagerProxy::new(&self.conn).await.map_err(|e| {
ConnectionError::DbusOperation {
context: "creating AgentManager proxy for unregistration".into(),
source: e,
}
})?;
proxy.unregister().await.map_err(unregistration_error)
}
.await;
self.conn
.object_server()
.remove::<SecretAgentInterface, _>(&*self.object_path)
.await
.map_err(|e| ConnectionError::DbusOperation {
context: format!("removing SecretAgent interface at {}", self.object_path),
source: e,
})?;
unregistered?;
debug!("Unregistered secret agent '{}'", self.identifier);
Ok(())
}
pub fn cancellations(&mut self) -> &mut mpsc::UnboundedReceiver<CancelReason> {
&mut self.cancel_rx
}
pub fn store_events(&mut self) -> &mut mpsc::UnboundedReceiver<SecretStoreEvent> {
&mut self.store_rx
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_manager_errors_map_to_public_lifecycle_failures() {
let cases = [
(
"org.freedesktop.NetworkManager.AgentManager.PermissionDenied",
Some("An agent with this ID is already registered for this user."),
AgentManagerFailure::AlreadyRegistered,
),
(
"org.freedesktop.NetworkManager.AgentManager.PermissionDenied",
Some("Not authorized"),
AgentManagerFailure::Registration,
),
(
"org.freedesktop.NetworkManager.AgentManager.InvalidIdentifier",
Some("Identifier contains invalid character ':'"),
AgentManagerFailure::Registration,
),
(
"org.freedesktop.NetworkManager.AgentManager.NotRegistered",
None,
AgentManagerFailure::NotRegistered,
),
(
"org.freedesktop.DBus.Error.NoReply",
Some("timed out"),
AgentManagerFailure::Other,
),
];
for (name, detail, expected) in cases {
assert_eq!(classify_agent_manager_failure(name, detail), expected);
}
}
#[test]
fn defaults_match_networkmanager_secret_agent_contract() {
let builder = SecretAgentBuilder::default();
assert_eq!(builder.object_path, DEFAULT_OBJECT_PATH);
assert_eq!(builder.identifier, DEFAULT_IDENTIFIER);
assert_eq!(builder.capabilities, SecretAgentCapabilities::VPN_HINTS);
assert_eq!(builder.queue_depth, DEFAULT_QUEUE_DEPTH);
}
#[test]
fn builder_methods_replace_every_registration_option() {
let builder = SecretAgent::builder()
.with_identifier("org.example.nmrs.agent")
.with_capabilities(SecretAgentCapabilities::empty())
.with_object_path("/org/example/nmrs/Agent")
.with_queue_depth(7);
assert_eq!(builder.identifier, "org.example.nmrs.agent");
assert_eq!(builder.capabilities, SecretAgentCapabilities::empty());
assert_eq!(builder.object_path, "/org/example/nmrs/Agent");
assert_eq!(builder.queue_depth, 7);
}
async fn connection_pair() -> (Connection, Connection) {
let (agent_socket, peer_socket) =
std::os::unix::net::UnixStream::pair().expect("socketpair");
let peer = zbus::connection::Builder::unix_stream(peer_socket)
.server(zbus::Guid::generate())
.expect("p2p server guid")
.p2p()
.serve_at("/", StandInBus)
.expect("serving the stand-in bus")
.build();
let agent = zbus::connection::Builder::unix_stream(agent_socket)
.p2p()
.method_timeout(std::time::Duration::from_secs(5))
.build();
let (peer, agent) = futures::join!(peer, agent);
let peer = peer.expect("p2p server connection");
let agent = agent.expect("p2p client connection");
(agent, peer)
}
struct StandInBus;
#[zbus::interface(name = "org.freedesktop.nmrs.test.StandInBus")]
impl StandInBus {}
async fn serve_agent(conn: &Connection, object_path: &str) -> SecretAgentHandle {
let (request_tx, _request_rx) = mpsc::channel(DEFAULT_QUEUE_DEPTH);
let (cancel_tx, cancel_rx) = mpsc::unbounded();
let (store_tx, store_rx) = mpsc::unbounded();
conn.object_server()
.at(
object_path,
SecretAgentInterface {
request_tx,
cancel_tx,
store_tx,
pending: Arc::new(Mutex::new(HashMap::new())),
next_request_id: AtomicU64::new(1),
response_timeout:
crate::types::constants::timeouts::secret_agent_response_timeout(),
},
)
.await
.expect("serving the agent interface");
SecretAgentHandle {
conn: conn.clone(),
identifier: DEFAULT_IDENTIFIER.to_owned(),
capabilities: SecretAgentCapabilities::VPN_HINTS,
object_path: object_path.to_owned(),
cancel_rx,
store_rx,
}
}
async fn agent_served_at(conn: &Connection, path: &str) -> bool {
conn.object_server()
.interface::<_, SecretAgentInterface>(path)
.await
.is_ok()
}
async fn introspect(peer: &Connection, path: &str) -> String {
zbus::fdo::IntrospectableProxy::builder(peer)
.destination("org.freedesktop.NetworkManager.nmrs.Agent")
.expect("destination")
.path(path.to_owned())
.expect("object path")
.build()
.await
.expect("introspectable proxy")
.introspect()
.await
.unwrap_or_default()
}
async fn exported_nodes(peer: &Connection) -> Vec<String> {
introspect(peer, "/")
.await
.lines()
.filter(|line| line.trim_start().starts_with("<node name="))
.map(|line| line.trim().to_owned())
.collect()
}
#[tokio::test(flavor = "multi_thread")]
async fn unregister_releases_the_agent_object_path() {
let (conn, _peer) = connection_pair().await;
let handle = serve_agent(&conn, DEFAULT_OBJECT_PATH).await;
assert!(agent_served_at(&conn, DEFAULT_OBJECT_PATH).await);
assert!(handle.unregister().await.is_err());
assert!(!agent_served_at(&conn, DEFAULT_OBJECT_PATH).await);
}
#[ignore = "needs zbus 6: ObjectServer::remove prunes empty ancestor nodes"]
#[tokio::test(flavor = "multi_thread")]
async fn unregister_leaves_no_objects_exported_on_the_agent_connection() {
let (conn, peer) = connection_pair().await;
let handle = serve_agent(&conn, DEFAULT_OBJECT_PATH).await;
assert!(handle.unregister().await.is_err());
assert_eq!(exported_nodes(&peer).await, Vec::<String>::new());
}
#[tokio::test(flavor = "multi_thread")]
async fn unregister_releases_a_custom_object_path() {
let (conn, _peer) = connection_pair().await;
let handle = serve_agent(&conn, "/org/example/nmrs/Agent").await;
assert!(agent_served_at(&conn, "/org/example/nmrs/Agent").await);
assert!(handle.unregister().await.is_err());
assert!(!agent_served_at(&conn, "/org/example/nmrs/Agent").await);
}
}