1#![allow(deprecated)]
30
31#[macro_use]
32extern crate error_chain;
33extern crate fs2;
34extern crate openssh_keys;
35extern crate users;
36
37pub mod errors {
38 error_chain! {
39 links {
40 ParseError(::openssh_keys::errors::Error, ::openssh_keys::errors::ErrorKind);
41 }
42 foreign_links {
43 Io(::std::io::Error);
44 }
45 errors {
46 KeysDisabled(name: String) {
47 description("keys are disabled")
48 display("keys with name '{}' are disabled", name)
49 }
50 KeysExist(name: String) {
51 description("keys already exist")
52 display("keys with name '{}' already exist", name)
53 }
54 NoKeysFound(ssh_dir: String) {
55 description("no keys found")
56 display("update-ssh-keys: no keys found in {}", ssh_dir)
57 }
58 }
59 }
60}
61
62use errors::*;
63use fs2::FileExt;
64use openssh_keys::PublicKey;
65use std::collections::HashMap;
66use std::fs::{self, File};
67use std::io::{BufRead, BufReader, Read, Write};
68use std::path::{Path, PathBuf};
69use users::os::unix::UserExt;
70use users::{switch, User};
71
72const SSH_DIR: &str = ".ssh";
73const AUTHORIZED_KEYS_DIR: &str = "authorized_keys.d";
74const AUTHORIZED_KEYS_FILE: &str = "authorized_keys";
75const PRESERVED_KEYS_FILE: &str = "old_authorized_keys";
76const LOCK_FILE: &str = ".authorized_keys.d.lock";
77const STAGE_FILE: &str = ".authorized_keys.d.stage_file";
78const STAGE_DIR: &str = ".authorized_keys.d.stage_dir";
79const STAGE_OLD_DIR: &str = ".authorized_keys.d.old";
80
81fn lock_file(user: &User) -> PathBuf {
82 user.home_dir().join(LOCK_FILE)
83}
84
85fn default_ssh_dir(user: &User) -> PathBuf {
86 user.home_dir().join(SSH_DIR)
87}
88
89fn authorized_keys_dir<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
90 ssh_dir.as_ref().join(AUTHORIZED_KEYS_DIR)
91}
92
93fn authorized_keys_file<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
94 ssh_dir.as_ref().join(AUTHORIZED_KEYS_FILE)
95}
96
97fn stage_dir<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
98 ssh_dir.as_ref().join(STAGE_DIR)
99}
100
101fn stage_old_dir<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
102 ssh_dir.as_ref().join(STAGE_OLD_DIR)
103}
104
105fn stage_file<P: AsRef<Path>>(ssh_dir: P) -> PathBuf {
106 ssh_dir.as_ref().join(STAGE_FILE)
107}
108
109fn switch_user(user: &User) -> Result<switch::SwitchUserGuard> {
110 switch::switch_user_group(user.uid(), user.primary_group_id())
111 .chain_err(|| "failed to switch user/group")
112}
113
114#[derive(Debug)]
115struct FileLock {
116 pub lock: File,
117}
118
119impl Drop for FileLock {
120 fn drop(&mut self) {
121 self.unlock().unwrap();
122 }
123}
124
125impl FileLock {
126 fn try_new(path: &Path) -> Result<Self> {
127 Ok(FileLock {
128 lock: File::create(path)
129 .chain_err(|| format!("failed to create lock file: {:?}", path))?,
130 })
131 }
132
133 fn lock(&self) -> Result<()> {
134 self.lock
135 .lock_exclusive()
136 .chain_err(|| "failed to lock file")
137 }
138
139 fn unlock(&self) -> Result<()> {
140 self.lock.unlock().chain_err(|| "failed to unlock file")
141 }
142}
143
144#[derive(Debug)]
145pub struct AuthorizedKeys {
146 pub ssh_dir: PathBuf,
147 pub keys: HashMap<String, AuthorizedKeySet>,
148 pub user: User,
149 lock: FileLock,
150}
151
152impl Drop for AuthorizedKeys {
153 fn drop(&mut self) {}
154}
155
156#[derive(Clone, Debug, Default)]
157pub struct AuthorizedKeySet {
158 pub filename: String,
159 pub disabled: bool,
160 pub keys: Vec<AuthorizedKeyEntry>,
161}
162
163#[derive(Clone, Debug)]
164pub enum AuthorizedKeyEntry {
165 Valid { key: PublicKey },
166 Invalid { key: String },
167}
168
169fn truncate_dir<P: AsRef<Path>>(dir: P) -> Result<()> {
173 let dir = dir.as_ref();
174
175 if dir.exists() {
176 if dir.is_dir() {
177 fs::remove_dir_all(dir)
178 .chain_err(|| format!("failed to remove existing directory '{:?}'", dir))?;
179 } else if dir.is_file() {
180 fs::remove_file(dir)
181 .chain_err(|| format!("failed to remove existing file '{:?}'", dir))?;
182 } else {
183 return Err(format!(
184 "failed to remove existing path '{:?}': not a file or directory",
185 dir
186 )
187 .into());
188 }
189 }
190
191 fs::create_dir_all(dir).chain_err(|| format!("failed to create directory '{:?}'", dir))
192}
193
194fn replace_dir<P: AsRef<Path>>(old: P, new: P, stage: P) -> Result<()> {
205 let old = old.as_ref();
206 let new = new.as_ref();
207 let stage = stage.as_ref();
208
209 if old.exists() && old.is_dir() {
210 let old_as_file = File::open(old)
212 .chain_err(|| format!("failed to open old dir '{}' for syncing", old.display()))?;
213 old_as_file
214 .sync_all()
215 .chain_err(|| format!("failed to sync old dir '{}'", old.display()))?;
216
217 truncate_dir(stage)?;
218 if new.exists() {
219 fs::rename(new, stage)
220 .chain_err(|| format!("failed to move '{:?}' to '{:?}'", new, stage))?;
221 }
222 fs::rename(old, new).chain_err(|| format!("failed to move '{:?}' to '{:?}'", old, new))?;
223
224 let parent_path = new
225 .parent()
226 .ok_or_else(|| format!("failed to sync parent directory of '{}'", new.display()))?;
227 let parent_dir = File::open(parent_path)
228 .chain_err(|| format!("failed to open dir '{}' for syncing", parent_path.display()))?;
229 parent_dir
230 .sync_all()
231 .chain_err(|| format!("failed to sync dir '{}'", parent_path.display()))?;
232
233 truncate_dir(stage)?;
234 }
235
236 Ok(())
237}
238
239impl AuthorizedKeys {
240 pub fn authorized_keys_dir(&self) -> PathBuf {
241 authorized_keys_dir(&self.ssh_dir)
242 }
243
244 pub fn authorized_keys_file(&self) -> PathBuf {
245 authorized_keys_file(&self.ssh_dir)
246 }
247
248 pub fn stage_dir(&self) -> PathBuf {
249 stage_dir(&self.ssh_dir)
250 }
251
252 fn stage_old_dir(&self) -> PathBuf {
253 stage_old_dir(&self.ssh_dir)
254 }
255
256 pub fn stage_file(&self) -> PathBuf {
257 stage_file(&self.ssh_dir)
258 }
259
260 pub fn write(&self) -> Result<()> {
264 let _guard = switch_user(&self.user)?;
266
267 let stage_dir = self.stage_dir();
269 truncate_dir(&stage_dir).chain_err(|| {
270 format!(
271 "failed to create staging directory '{}'",
272 stage_dir.display()
273 )
274 })?;
275
276 for keyset in self.keys.values() {
278 let keyfilename = stage_dir.join(&keyset.filename);
279 let mut keyfile = File::create(&keyfilename)
280 .chain_err(|| format!("failed to create file '{:?}'", keyfilename))?;
281 if keyset.disabled {
285 continue;
286 }
287 for key in &keyset.keys {
288 match *key {
289 AuthorizedKeyEntry::Valid { ref key } => writeln!(keyfile, "{}", key)
290 .chain_err(|| format!("failed to write to file '{:?}'", keyfilename))?,
291 AuthorizedKeyEntry::Invalid { ref key } => writeln!(keyfile, "{}", key)
292 .chain_err(|| format!("failed to write to file '{:?}'", keyfilename))?,
293 }
294 }
295
296 keyfile
297 .sync_all()
298 .chain_err(|| format!("failed to sync file '{:?}'", keyfilename))?;
299 }
300
301 replace_dir(
302 &stage_dir,
303 &self.authorized_keys_dir(),
304 &self.stage_old_dir(),
305 )
306 }
307
308 pub fn sync(&self) -> Result<()> {
312 if self.keys.is_empty() {
315 return Err(ErrorKind::NoKeysFound(format!("{:?}", self.authorized_keys_dir())).into());
316 }
317
318 let _guard = switch_user(&self.user)?;
320
321 let stage_filename = self.stage_file();
323 let mut stage_file = File::create(&stage_filename).chain_err(|| {
324 format!(
325 "failed to create or truncate staging file '{:?}'",
326 stage_filename
327 )
328 })?;
329
330 writeln!(stage_file, "# auto-generated by update-ssh-keys")
332 .chain_err(|| format!("failed to write to file '{:?}'", stage_filename))?;
333
334 for keyset in self.keys.values() {
336 if keyset.disabled {
338 continue;
339 }
340 for key in &keyset.keys {
341 if let AuthorizedKeyEntry::Valid { ref key } = *key {
343 writeln!(stage_file, "{}", key)
344 .chain_err(|| format!("failed to write to file '{:?}'", stage_filename))?;
345 }
346 }
347 }
348
349 stage_file
350 .sync_all()
351 .chain_err(|| format!("failed to sync file '{:?}'", stage_filename))?;
352 drop(stage_file);
353
354 fs::rename(&stage_filename, &self.authorized_keys_file()).chain_err(|| {
357 format!(
358 "failed to move '{:?}' to '{:?}'",
359 stage_filename,
360 self.authorized_keys_file()
361 )
362 })?;
363
364 let parent_path = stage_filename.parent().ok_or_else(|| {
365 format!(
366 "failed to sync parent directory of '{}'",
367 stage_filename.display()
368 )
369 })?;
370 let parent_dir_file = File::open(parent_path)
371 .chain_err(|| format!("failed to open '{}' for syncing", parent_path.display()))?;
372 parent_dir_file
373 .sync_all()
374 .chain_err(|| format!("failed to sync '{}'", parent_path.display()))?;
375
376 Ok(())
377 }
378
379 fn read_all_keys(dir: &Path) -> Result<HashMap<String, AuthorizedKeySet>> {
384 let dir_contents =
385 fs::read_dir(&dir).chain_err(|| format!("failed to read from directory {:?}", dir))?;
386 let mut keys = HashMap::new();
387 for entry in dir_contents {
388 let entry =
389 entry.chain_err(|| format!("failed to read entry in directory {:?}", dir))?;
390 let path = entry.path();
391 if path.is_dir() {
392 return Err(format!("'{:?}' is a directory", path).into());
394 } else {
395 let name = path
396 .file_name()
397 .ok_or_else(|| format!("failed to get filename for '{:?}'", path))?
398 .to_str()
399 .ok_or_else(|| format!("failed to convert filename '{:?}' to string", path))?;
400 let from =
401 File::open(&path).chain_err(|| format!("failed to open file {:?}", path))?;
402 let keyset = AuthorizedKeys::read_keys(from)?;
403 keys.insert(
404 name.to_string(),
405 AuthorizedKeySet {
406 filename: name.to_string(),
407 disabled: keyset.is_empty(),
408 keys: keyset,
409 },
410 );
411 }
412 }
413 Ok(keys)
414 }
415
416 pub fn read_keys<R>(r: R) -> Result<Vec<AuthorizedKeyEntry>>
420 where
421 R: Read,
422 {
423 let keybuf = BufReader::new(r);
424 let mut keys = vec![];
426 for key in keybuf.lines() {
427 let key = key.chain_err(|| "failed to read public key")?;
428 if !key.is_empty() && !(key.trim().starts_with('#')) {
430 match PublicKey::parse(&key) {
431 Ok(pkey) => keys.push(AuthorizedKeyEntry::Valid { key: pkey }),
432 Err(e) => {
433 println!("warning: failed to parse public key \"{}\": {}, omitting from authorized_keys", key, e);
434 keys.push(AuthorizedKeyEntry::Invalid { key })
435 }
436 };
437 }
438 }
439 Ok(keys)
440 }
441
442 pub fn open(user: User, create: bool, ssh_dir: Option<PathBuf>) -> Result<Self> {
462 let _guard = switch_user(&user)?;
464 let lock = FileLock::try_new(&lock_file(&user))?;
466 lock.lock()?;
467
468 let ssh_dir = ssh_dir.unwrap_or_else(|| default_ssh_dir(&user));
469 let akd = authorized_keys_dir(&ssh_dir);
470
471 let keys = if akd.is_dir() {
472 AuthorizedKeys::read_all_keys(&akd)?
474 } else if !akd.exists() && create {
475 let filename = authorized_keys_file(&ssh_dir);
477 if filename.exists() {
478 let file = File::open(&filename).chain_err(|| {
479 format!("failed to open authorized keys file: '{:?}'", filename)
480 })?;
481 let mut keys = HashMap::new();
482 keys.insert(
483 PRESERVED_KEYS_FILE.to_string(),
484 AuthorizedKeySet {
485 filename: PRESERVED_KEYS_FILE.to_string(),
486 disabled: false,
487 keys: AuthorizedKeys::read_keys(file)?,
488 },
489 );
490 keys
491 } else {
492 HashMap::new()
495 }
496 } else {
497 return Err(format!("'{:?}' doesn't exist or is not a directory", akd).into());
500 };
501
502 Ok(AuthorizedKeys {
503 ssh_dir,
504 user,
505 keys,
506 lock,
507 })
508 }
509
510 pub fn get_keys(&self, name: &str) -> Option<&AuthorizedKeySet> {
512 self.keys.get(name)
513 }
514
515 pub fn get_all_keys(&self) -> &HashMap<String, AuthorizedKeySet> {
518 &self.keys
519 }
520
521 pub fn add_keys(
531 &mut self,
532 name: &str,
533 keys: Vec<AuthorizedKeyEntry>,
534 replace: bool,
535 force: bool,
536 ) -> Result<Vec<AuthorizedKeyEntry>> {
537 if keys.is_empty() {
539 return Ok(vec![]);
540 }
541
542 if let Some(keyset) = self.keys.get(name) {
543 if keyset.disabled && !force {
544 return Err(ErrorKind::KeysDisabled(name.to_string()).into());
545 } else if !replace {
546 return Err(ErrorKind::KeysExist(name.to_string()).into());
547 }
548 }
549 self.keys.insert(
550 name.to_string(),
551 AuthorizedKeySet {
552 filename: name.to_string(),
553 disabled: false,
554 keys: keys.clone(),
555 },
556 );
557 Ok(keys)
558 }
559
560 pub fn remove_keys(&mut self, name: &str) -> Vec<AuthorizedKeyEntry> {
562 self.keys.remove(name).unwrap_or_default().keys
563 }
564
565 pub fn disable_keys(&mut self, name: &str) -> Vec<AuthorizedKeyEntry> {
569 if let Some(keyset) = self.keys.get_mut(name) {
570 let keys = keyset.keys.clone();
571 keyset.disabled = true;
572 keyset.keys = vec![];
573 return keys;
574 }
575 self.keys.insert(
576 name.to_string(),
577 AuthorizedKeySet {
578 filename: name.to_string(),
579 disabled: true,
580 keys: vec![],
581 },
582 );
583 vec![]
584 }
585}