use std::io;
use std::path::Path;
use std::pin::Pin;
use std::task::{Context, Poll};
use anyhow::Result;
use pin_project_lite::pin_project;
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::UnixStream;
use tonic::transport::server::Connected;
use crate::control_plane::Role;
#[derive(Debug, Clone)]
pub struct UdsPeerInfo {
pub uid: u32,
pub _gid: u32,
pub pid: Option<i32>,
}
pin_project! {
pub struct UdsStream {
#[pin]
inner: UnixStream,
peer: UdsPeerInfo,
}
}
impl UdsStream {
pub fn new(stream: UnixStream, peer: UdsPeerInfo) -> Self {
Self {
inner: stream,
peer,
}
}
}
impl Connected for UdsStream {
type ConnectInfo = UdsPeerInfo;
fn connect_info(&self) -> Self::ConnectInfo {
self.peer.clone()
}
}
impl AsyncRead for UdsStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
self.project().inner.poll_read(cx, buf)
}
}
impl AsyncWrite for UdsStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.project().inner.poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.project().inner.poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.project().inner.poll_shutdown(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[io::IoSlice<'_>],
) -> Poll<io::Result<usize>> {
self.project().inner.poll_write_vectored(cx, bufs)
}
fn is_write_vectored(&self) -> bool {
self.inner.is_write_vectored()
}
}
pub fn validate_peer(stream: &UnixStream) -> io::Result<UdsPeerInfo> {
let cred = stream.peer_cred()?;
let peer = UdsPeerInfo {
uid: cred.uid(),
_gid: cred.gid(),
pid: cred.pid(),
};
#[allow(clippy::undocumented_unsafe_blocks)]
let my_uid = unsafe { libc::getuid() };
if peer.uid != my_uid {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"UDS peer UID {} does not match daemon UID {}",
peer.uid, my_uid
),
));
}
Ok(peer)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UdsAuthPolicy {
#[serde(default = "default_max_role")]
pub max_role: Role,
}
fn default_max_role() -> Role {
Role::Admin
}
impl Default for UdsAuthPolicy {
fn default() -> Self {
Self {
max_role: default_max_role(),
}
}
}
pub fn load_uds_policy(
data_dir: &Path,
control_plane_dir: Option<&Path>,
) -> Result<Option<UdsAuthPolicy>> {
let cp_dir = match control_plane_dir {
Some(d) => d.to_path_buf(),
None => data_dir.join("control-plane"),
};
let path = cp_dir.join("uds-policy.yaml");
if !path.exists() {
return Ok(None);
}
let raw = std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("failed to read {}: {e}", path.display()))?;
let policy: UdsAuthPolicy = serde_yaml::from_str(&raw)
.map_err(|e| anyhow::anyhow!("failed to parse {}: {e}", path.display()))?;
Ok(Some(policy))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_policy_allows_admin() {
let policy = UdsAuthPolicy::default();
assert_eq!(policy.max_role, Role::Admin);
assert!(policy.max_role.allows(Role::Admin));
}
#[test]
fn read_only_policy_denies_operator() {
let policy = UdsAuthPolicy {
max_role: Role::ReadOnly,
};
assert!(policy.max_role.allows(Role::ReadOnly));
assert!(!policy.max_role.allows(Role::Operator));
assert!(!policy.max_role.allows(Role::Admin));
}
#[test]
fn operator_policy_allows_operator_denies_admin() {
let policy = UdsAuthPolicy {
max_role: Role::Operator,
};
assert!(policy.max_role.allows(Role::ReadOnly));
assert!(policy.max_role.allows(Role::Operator));
assert!(!policy.max_role.allows(Role::Admin));
}
#[test]
fn absent_policy_file_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let result = load_uds_policy(tmp.path(), None).unwrap();
assert!(result.is_none());
}
#[test]
fn policy_file_round_trip() {
let tmp = tempfile::tempdir().unwrap();
let cp_dir = tmp.path().join("control-plane");
std::fs::create_dir_all(&cp_dir).unwrap();
let path = cp_dir.join("uds-policy.yaml");
std::fs::write(&path, "max_role: read_only\n").unwrap();
let policy = load_uds_policy(tmp.path(), None).unwrap().unwrap();
assert_eq!(policy.max_role, Role::ReadOnly);
}
}