#![warn(bad_style)]
#![warn(future_incompatible)]
#![warn(nonstandard_style)]
#![warn(rust_2018_compatibility)]
#![warn(rust_2018_idioms)]
#![warn(rustdoc)]
#![warn(unused)]
#![warn(bare_trait_objects)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
#![warn(missing_docs)]
#![warn(single_use_lifetimes)]
#![warn(trivial_casts)]
#![warn(trivial_numeric_casts)]
#![warn(unreachable_pub)]
#![warn(unstable_features)]
#![warn(unused_import_braces)]
#![warn(unused_lifetimes)]
#![warn(unused_qualifications)]
#![warn(unused_results)]
#![warn(variant_size_differences)]
#![allow(unsafe_code)]
#![cfg_attr(feature="cargo-clippy", warn(clippy::all))]
mod errors;
mod template;
mod socket;
use crate::errors::*;
use crate::template::Spec;
use crate::socket::Socket;
use std::collections::HashSet;
use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use libc::{gid_t, mode_t, uid_t};
use failure::ResultExt;
use sudo_plugin::*;
const DEFAULT_BINARY_PATH : &str = "/usr/bin/sudo_approve";
const DEFAULT_USER_PROMPT_PATH : &str = "/etc/sudo_pair.prompt.user";
const DEFAULT_PAIR_PROMPT_PATH : &str = "/etc/sudo_pair.prompt.pair";
const DEFAULT_SOCKET_DIR : &str = "/var/run/sudo_pair";
const DEFAULT_GIDS_ENFORCED : [gid_t; 1] = [0];
const DEFAULT_USER_PROMPT : &[u8] = b"%B '%p %u'\n";
const DEFAULT_PAIR_PROMPT : &[u8] = b"%U@%h:%d$ %C\ny/n? [n]: ";
sudo_io_plugin! {
sudo_pair: SudoPair {
close: close,
log_ttyout: log_ttyout,
log_stdin: log_disabled,
log_stdout: log_stdout,
log_stderr: log_stderr,
}
}
struct SudoPair {
plugin: &'static Plugin,
options: PluginOptions,
socket: Option<Socket>,
}
impl SudoPair {
fn open(plugin: &'static Plugin) -> Result<Self> {
let mut pair = Self {
plugin,
options: PluginOptions::from(&plugin.plugin_options),
socket: None,
};
if pair.is_exempt() {
return Ok(pair)
}
if pair.is_sudoing_to_user_and_group() {
return Err(ErrorKind::SudoToUserAndGroup.into());
}
let template_spec = pair.template_spec();
pair.local_pair_prompt(&template_spec);
pair.remote_pair_connect()?;
pair.remote_pair_prompt(&template_spec)?;
Ok(pair)
}
fn close(&mut self, _: i64, _: i64) {
let _ = self.socket.as_mut().map(Socket::close);
}
fn log_ttyout(&mut self, log: &[u8]) -> Result<()> {
if !self.plugin.command_info.iolog_ttyout {
return Ok(())
}
self.log_output(log)
}
fn log_stdout(&mut self, log: &[u8]) -> Result<()> {
if !self.plugin.command_info.iolog_stdout {
return Ok(())
}
self.log_output(log)
}
fn log_stderr(&mut self, log: &[u8]) -> Result<()> {
if !self.plugin.command_info.iolog_stderr {
return Ok(())
}
self.log_output(log)
}
fn log_output(&mut self, log: &[u8]) -> Result<()> {
self.socket.as_mut().map_or(Ok(()), |socket| {
socket.write_all(log)
}).context(ErrorKind::SessionTerminated)?;
Ok(())
}
fn log_disabled(&mut self, _: &[u8]) -> Result<()> {
if self.is_exempt() {
return Ok(());
}
Err(ErrorKind::StdinRedirected.into())
}
fn local_pair_prompt(&self, template_spec: &Spec) {
let template : Vec<u8> = File::open(&self.options.user_prompt_path)
.and_then(|file| file.bytes().collect() )
.unwrap_or_else(|_| DEFAULT_USER_PROMPT.into() );
let prompt = template_spec.expand(&template[..]);
let _ = self.plugin.tty().as_mut()
.and_then(|tty| tty.write_all(&prompt).ok() )
.ok_or_else(||self.plugin.stderr().write_all(&prompt));
}
fn remote_pair_connect(&mut self) -> Result<()> {
if self.socket.is_some() {
return Ok(());
}
let socket = Socket::open(
self.socket_path(),
self.socket_uid(),
self.socket_gid(),
self.socket_mode(),
).context(ErrorKind::CommunicationError)?;
self.socket = Some(socket);
Ok(())
}
fn remote_pair_prompt(&mut self, template_spec: &Spec) -> Result<()> {
let template : Vec<u8> = File::open(&self.options.pair_prompt_path)
.and_then(|file| file.bytes().collect() )
.unwrap_or_else(|_| DEFAULT_PAIR_PROMPT.into() );
let prompt = template_spec.expand(&template[..]);
let socket = self.socket
.as_mut()
.ok_or(ErrorKind::CommunicationError)?;
socket.write_all(&prompt[..])
.context(ErrorKind::CommunicationError)?;
socket.flush()
.context(ErrorKind::CommunicationError)?;
let mut response : [u8; 1] = [b'n'];
let _ = socket.read(&mut response)
.context(ErrorKind::SessionDeclined)?;
let _ = socket.write_all(&response[..]);
let _ = socket.write_all(b"\n");
match &response {
b"y" | b"Y" => Ok(()),
_ => Err(ErrorKind::SessionDeclined.into()),
}
}
fn is_exempt(&self) -> bool {
if self.is_sudoing_from_root() {
return true;
}
if self.is_sudoing_to_themselves() {
return true;
}
if self.is_sudoing_approval_command() {
return true;
}
if self.is_exempted_from_logging() {
return true;
}
if self.is_sudoing_from_exempted_gid() {
return true;
}
if !self.is_sudoing_to_enforced_gid() {
return true;
}
false
}
fn is_sudoing_from_root(&self) -> bool {
self.plugin.user_info.uid == self.plugin.user_info.euid
}
fn is_sudoing_to_themselves(&self) -> bool {
if !self.is_sudoing_to_user() && !self.is_sudoing_to_group() {
debug_assert_eq!(
self.plugin.runas_gids(),
self.plugin.user_info.groups.iter().cloned().collect()
);
return true;
}
false
}
fn is_sudoing_approval_command(&self) -> bool {
self.plugin.command_info.command == self.options.binary_path
}
fn is_exempted_from_logging(&self) -> bool {
if
!self.plugin.command_info.iolog_ttyout &&
!self.plugin.command_info.iolog_stdout &&
!self.plugin.command_info.iolog_stderr
{
return true;
}
false
}
fn is_sudoing_to_user_and_group(&self) -> bool {
if self.is_sudoing_to_user() && self.is_sudoing_to_explicit_group() {
return true
}
false
}
fn is_sudoing_from_exempted_gid(&self) -> bool {
!self.options.gids_exempted.is_disjoint(
&self.plugin.user_info.groups.iter().cloned().collect()
)
}
fn is_sudoing_to_enforced_gid(&self) -> bool {
!self.options.gids_enforced.is_disjoint(
&self.plugin.runas_gids()
)
}
fn is_sudoing_to_user(&self) -> bool {
self.plugin.user_info.uid != self.plugin.command_info.runas_euid
}
fn is_sudoing_to_group(&self) -> bool {
self.plugin.user_info.gid != self.plugin.command_info.runas_egid
}
fn is_sudoing_to_explicit_group(&self) -> bool {
self.plugin.settings.runas_group.is_some()
}
fn socket_path(&self) -> PathBuf {
self.options.socket_dir.join(
format!(
"{}.{}.sock",
self.plugin.user_info.uid,
self.plugin.user_info.pid,
)
)
}
fn socket_uid(&self) -> uid_t {
if self.is_sudoing_to_user() {
self.plugin.command_info.runas_euid
} else {
uid_t::max_value()
}
}
fn socket_gid(&self) -> gid_t {
if self.is_sudoing_to_explicit_group() {
self.plugin.command_info.runas_egid
} else {
gid_t::max_value()
}
}
fn socket_mode(&self) -> mode_t {
if self.is_sudoing_to_user() {
return libc::S_IWUSR; }
if self.is_sudoing_to_explicit_group() {
return libc::S_IWGRP; }
unreachable!("cannot determine if we're sudoing to a user or group")
}
fn template_spec(&self) -> Spec {
let mut spec = Spec::with_escape(b'%');
spec.replace(b'b', self.options.binary_name());
spec.replace(b'B', self.options.binary_path.as_os_str().as_bytes());
spec.replace(b'C', self.plugin.invocation());
spec.replace(b'd', self.plugin.cwd().as_os_str().as_bytes());
spec.replace(b'h', self.plugin.user_info.host.as_bytes());
spec.replace(b'H', self.plugin.user_info.lines.to_string());
spec.replace(b'g', self.plugin.user_info.gid.to_string());
spec.replace(b'p', self.plugin.user_info.pid.to_string());
spec.replace(b'u', self.plugin.user_info.uid.to_string());
spec.replace(b'U', self.plugin.user_info.user.as_bytes());
spec.replace(b'W', self.plugin.user_info.cols.to_string());
spec
}
}
#[derive(Debug)]
struct PluginOptions {
binary_path: PathBuf,
user_prompt_path: PathBuf,
pair_prompt_path: PathBuf,
socket_dir: PathBuf,
gids_enforced: HashSet<gid_t>,
gids_exempted: HashSet<gid_t>,
}
impl PluginOptions {
fn binary_name(&self) -> &[u8] {
self.binary_path.file_name().unwrap_or_else(||
self.binary_path.as_os_str()
).as_bytes()
}
}
#[allow(single_use_lifetimes)]
impl<'a> From<&'a OptionMap> for PluginOptions {
fn from(map: &'a OptionMap) -> Self {
Self {
binary_path: map.get("binary_path")
.unwrap_or_else(|_| DEFAULT_BINARY_PATH.into()),
user_prompt_path: map.get("user_prompt_path")
.unwrap_or_else(|_| DEFAULT_USER_PROMPT_PATH.into()),
pair_prompt_path: map.get("pair_prompt_path")
.unwrap_or_else(|_| DEFAULT_PAIR_PROMPT_PATH.into()),
socket_dir: map.get("socket_dir")
.unwrap_or_else(|_| DEFAULT_SOCKET_DIR.into()),
gids_enforced: map.get("gids_enforced")
.unwrap_or_else(|_| DEFAULT_GIDS_ENFORCED.iter().cloned().collect()),
gids_exempted: map.get("gids_exempted")
.unwrap_or_default(),
}
}
}