#![allow(deprecated)]
#[macro_use]
extern crate error_chain;
extern crate fs2;
extern crate openssh_keys;
extern crate users;
pub mod errors {
error_chain! {
links {
ParseError(::openssh_keys::errors::Error, ::openssh_keys::errors::ErrorKind);
}
foreign_links {
Io(::std::io::Error);
}
errors {
KeysDisabled(name: String) {
description("keys are disabled")
display("keys with name '{}' are disabled", name)
}
KeysExist(name: String) {
description("keys already exist")
display("keys with name '{}' already exist", name)
}
NoKeysFound(ssh_dir: String) {
description("no keys found")
display("update-ssh-keys: no keys found in {}", ssh_dir)
}
}
}
}
use errors::*;
use fs2::FileExt;
use openssh_keys::PublicKey;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use users::os::unix::UserExt;
use users::{switch, User};
const SSH_DIR: &str = ".ssh";
const AUTHORIZED_KEYS_DIR: &str = "authorized_keys.d";
const AUTHORIZED_KEYS_FILE: &str = "authorized_keys";
const PRESERVED_KEYS_FILE: &str = "old_authorized_keys";
const LOCK_FILE: &str = ".authorized_keys.d.lock";
const STAGE_FILE: &str = ".authorized_keys.d.stage_file";
const STAGE_DIR: &str = ".authorized_keys.d.stage_dir";
const STAGE_OLD_DIR: &str = ".authorized_keys.d.old";
fn lock_file(user: &User) -> PathBuf {
user.home_dir().join(LOCK_FILE)
}
fn default_ssh_dir(user: &User) -> PathBuf {
user.home_dir().join(SSH_DIR)
}
fn authorized_keys_dir<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
ssh_dir.as_ref().join(AUTHORIZED_KEYS_DIR)
}
fn authorized_keys_file<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
ssh_dir.as_ref().join(AUTHORIZED_KEYS_FILE)
}
fn stage_dir<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
ssh_dir.as_ref().join(STAGE_DIR)
}
fn stage_old_dir<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
ssh_dir.as_ref().join(STAGE_OLD_DIR)
}
fn stage_file<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
ssh_dir.as_ref().join(STAGE_FILE)
}
fn switch_user(user: &User) -> Result<switch::SwitchUserGuard> {
switch::switch_user_group(user.uid(), user.primary_group_id())
.chain_err(|| "failed to switch user/group")
}
#[derive(Debug)]
struct FileLock {
pub lock: File,
}
impl Drop for FileLock {
fn drop(&mut self) {
self.unlock().unwrap();
}
}
impl FileLock {
fn try_new(path: &Path) -> Result<Self> {
Ok(FileLock {
lock: File::create(path)
.chain_err(|| format!("failed to create lock file: {:?}", path))?,
})
}
fn lock(&self) -> Result<()> {
self.lock
.lock_exclusive()
.chain_err(|| "failed to lock file")
}
fn unlock(&self) -> Result<()> {
self.lock.unlock().chain_err(|| "failed to unlock file")
}
}
#[derive(Debug)]
pub struct AuthorizedKeys {
pub ssh_dir: PathBuf,
pub keys: HashMap<String, AuthorizedKeySet>,
pub user: User,
lock: FileLock,
}
impl Drop for AuthorizedKeys {
fn drop(&mut self) {}
}
#[derive(Clone, Debug, Default)]
pub struct AuthorizedKeySet {
pub filename: String,
pub disabled: bool,
pub keys: Vec<AuthorizedKeyEntry>,
}
#[derive(Clone, Debug)]
pub enum AuthorizedKeyEntry {
Valid { key: PublicKey },
Invalid { key: String },
}
fn truncate_dir<P: AsRef<Path>>(dir: P) -> Result<()> {
let dir = dir.as_ref();
if dir.exists() {
if dir.is_dir() {
fs::remove_dir_all(dir)
.chain_err(|| format!("failed to remove existing directory '{:?}'", dir))?;
} else if dir.is_file() {
fs::remove_file(dir)
.chain_err(|| format!("failed to remove existing file '{:?}'", dir))?;
} else {
return Err(format!(
"failed to remove existing path '{:?}': not a file or directory",
dir
)
.into());
}
}
fs::create_dir_all(dir).chain_err(|| format!("failed to create directory '{:?}'", dir))
}
fn replace_dir<P: AsRef<Path>>(old: P, new: P, stage: P) -> Result<()> {
let old = old.as_ref();
let new = new.as_ref();
let stage = stage.as_ref();
if old.exists() && old.is_dir() {
let old_as_file = File::open(old)
.chain_err(|| format!("failed to open old dir '{}' for syncing", old.display()))?;
old_as_file
.sync_all()
.chain_err(|| format!("failed to sync old dir '{}'", old.display()))?;
truncate_dir(stage)?;
if new.exists() {
fs::rename(new, stage)
.chain_err(|| format!("failed to move '{:?}' to '{:?}'", new, stage))?;
}
fs::rename(old, new).chain_err(|| format!("failed to move '{:?}' to '{:?}'", old, new))?;
let parent_path = new
.parent()
.ok_or_else(|| format!("failed to sync parent directory of '{}'", new.display()))?;
let parent_dir = File::open(parent_path)
.chain_err(|| format!("failed to open dir '{}' for syncing", parent_path.display()))?;
parent_dir
.sync_all()
.chain_err(|| format!("failed to sync dir '{}'", parent_path.display()))?;
truncate_dir(stage)?;
}
Ok(())
}
impl AuthorizedKeys {
pub fn authorized_keys_dir(&self) -> PathBuf {
authorized_keys_dir(&self.ssh_dir)
}
pub fn authorized_keys_file(&self) -> PathBuf {
authorized_keys_file(&self.ssh_dir)
}
pub fn stage_dir(&self) -> PathBuf {
stage_dir(&self.ssh_dir)
}
fn stage_old_dir(&self) -> PathBuf {
stage_old_dir(&self.ssh_dir)
}
pub fn stage_file(&self) -> PathBuf {
stage_file(&self.ssh_dir)
}
pub fn write(&self) -> Result<()> {
let _guard = switch_user(&self.user)?;
let stage_dir = self.stage_dir();
truncate_dir(&stage_dir).chain_err(|| {
format!(
"failed to create staging directory '{}'",
stage_dir.display()
)
})?;
for keyset in self.keys.values() {
let keyfilename = stage_dir.join(&keyset.filename);
let mut keyfile = File::create(&keyfilename)
.chain_err(|| format!("failed to create file '{:?}'", keyfilename))?;
if keyset.disabled {
continue;
}
for key in &keyset.keys {
match *key {
AuthorizedKeyEntry::Valid { ref key } => writeln!(keyfile, "{}", key)
.chain_err(|| format!("failed to write to file '{:?}'", keyfilename))?,
AuthorizedKeyEntry::Invalid { ref key } => writeln!(keyfile, "{}", key)
.chain_err(|| format!("failed to write to file '{:?}'", keyfilename))?,
}
}
keyfile
.sync_all()
.chain_err(|| format!("failed to sync file '{:?}'", keyfilename))?;
}
replace_dir(
&stage_dir,
&self.authorized_keys_dir(),
&self.stage_old_dir(),
)
}
pub fn sync(&self) -> Result<()> {
if self.keys.is_empty() {
return Err(ErrorKind::NoKeysFound(format!("{:?}", self.authorized_keys_dir())).into());
}
let _guard = switch_user(&self.user)?;
let stage_filename = self.stage_file();
let mut stage_file = File::create(&stage_filename).chain_err(|| {
format!(
"failed to create or truncate staging file '{:?}'",
stage_filename
)
})?;
writeln!(stage_file, "# auto-generated by update-ssh-keys")
.chain_err(|| format!("failed to write to file '{:?}'", stage_filename))?;
for keyset in self.keys.values() {
if keyset.disabled {
continue;
}
for key in &keyset.keys {
if let AuthorizedKeyEntry::Valid { ref key } = *key {
writeln!(stage_file, "{}", key)
.chain_err(|| format!("failed to write to file '{:?}'", stage_filename))?;
}
}
}
stage_file
.sync_all()
.chain_err(|| format!("failed to sync file '{:?}'", stage_filename))?;
drop(stage_file);
fs::rename(&stage_filename, &self.authorized_keys_file()).chain_err(|| {
format!(
"failed to move '{:?}' to '{:?}'",
stage_filename,
self.authorized_keys_file()
)
})?;
let parent_path = stage_filename.parent().ok_or_else(|| {
format!(
"failed to sync parent directory of '{}'",
stage_filename.display()
)
})?;
let parent_dir_file = File::open(parent_path)
.chain_err(|| format!("failed to open '{}' for syncing", parent_path.display()))?;
parent_dir_file
.sync_all()
.chain_err(|| format!("failed to sync '{}'", parent_path.display()))?;
Ok(())
}
fn read_all_keys(dir: &Path) -> Result<HashMap<String, AuthorizedKeySet>> {
let dir_contents =
fs::read_dir(&dir).chain_err(|| format!("failed to read from directory {:?}", dir))?;
let mut keys = HashMap::new();
for entry in dir_contents {
let entry =
entry.chain_err(|| format!("failed to read entry in directory {:?}", dir))?;
let path = entry.path();
if path.is_dir() {
return Err(format!("'{:?}' is a directory", path).into());
} else {
let name = path
.file_name()
.ok_or_else(|| format!("failed to get filename for '{:?}'", path))?
.to_str()
.ok_or_else(|| format!("failed to convert filename '{:?}' to string", path))?;
let from =
File::open(&path).chain_err(|| format!("failed to open file {:?}", path))?;
let keyset = AuthorizedKeys::read_keys(from)?;
keys.insert(
name.to_string(),
AuthorizedKeySet {
filename: name.to_string(),
disabled: keyset.is_empty(),
keys: keyset,
},
);
}
}
Ok(keys)
}
pub fn read_keys<R>(r: R) -> Result<Vec<AuthorizedKeyEntry>>
where
R: Read,
{
let keybuf = BufReader::new(r);
let mut keys = vec![];
for key in keybuf.lines() {
let key = key.chain_err(|| "failed to read public key")?;
if !key.is_empty() && !(key.trim().starts_with('#')) {
match PublicKey::parse(&key) {
Ok(pkey) => keys.push(AuthorizedKeyEntry::Valid { key: pkey }),
Err(e) => {
println!("warning: failed to parse public key \"{}\": {}, omitting from authorized_keys", key, e);
keys.push(AuthorizedKeyEntry::Invalid { key })
}
};
}
}
Ok(keys)
}
pub fn open(user: User, create: bool, ssh_dir: Option<PathBuf>) -> Result<Self> {
let _guard = switch_user(&user)?;
let lock = FileLock::try_new(&lock_file(&user))?;
lock.lock()?;
let ssh_dir = ssh_dir.unwrap_or_else(|| default_ssh_dir(&user));
let akd = authorized_keys_dir(&ssh_dir);
let keys = if akd.is_dir() {
AuthorizedKeys::read_all_keys(&akd)?
} else if !akd.exists() && create {
let filename = authorized_keys_file(&ssh_dir);
if filename.exists() {
let file = File::open(&filename).chain_err(|| {
format!("failed to open authorized keys file: '{:?}'", filename)
})?;
let mut keys = HashMap::new();
keys.insert(
PRESERVED_KEYS_FILE.to_string(),
AuthorizedKeySet {
filename: PRESERVED_KEYS_FILE.to_string(),
disabled: false,
keys: AuthorizedKeys::read_keys(file)?,
},
);
keys
} else {
HashMap::new()
}
} else {
return Err(format!("'{:?}' doesn't exist or is not a directory", akd).into());
};
Ok(AuthorizedKeys {
ssh_dir,
user,
keys,
lock,
})
}
pub fn get_keys(&self, name: &str) -> Option<&AuthorizedKeySet> {
self.keys.get(name)
}
pub fn get_all_keys(&self) -> &HashMap<String, AuthorizedKeySet> {
&self.keys
}
pub fn add_keys(
&mut self,
name: &str,
keys: Vec<AuthorizedKeyEntry>,
replace: bool,
force: bool,
) -> Result<Vec<AuthorizedKeyEntry>> {
if keys.is_empty() {
return Ok(vec![]);
}
if let Some(keyset) = self.keys.get(name) {
if keyset.disabled && !force {
return Err(ErrorKind::KeysDisabled(name.to_string()).into());
} else if !replace {
return Err(ErrorKind::KeysExist(name.to_string()).into());
}
}
self.keys.insert(
name.to_string(),
AuthorizedKeySet {
filename: name.to_string(),
disabled: false,
keys: keys.clone(),
},
);
Ok(keys)
}
pub fn remove_keys(&mut self, name: &str) -> Vec<AuthorizedKeyEntry> {
self.keys.remove(name).unwrap_or_default().keys
}
pub fn disable_keys(&mut self, name: &str) -> Vec<AuthorizedKeyEntry> {
if let Some(keyset) = self.keys.get_mut(name) {
let keys = keyset.keys.clone();
keyset.disabled = true;
keyset.keys = vec![];
return keys;
}
self.keys.insert(
name.to_string(),
AuthorizedKeySet {
filename: name.to_string(),
disabled: true,
keys: vec![],
},
);
vec![]
}
}