diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0b09fda..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: rust - -rust: - - 1.28.0 # minimum supported toolchain - - 1.31.0 # pinned toolchain for clippy - - stable - - beta - - nightly - -matrix: - allow_failures: - - rust: nightly - -env: - global: - - CLIPPY_RUST_VERSION=1.31.0 - -before_script: - - bash -c 'if [[ "$TRAVIS_RUST_VERSION" == "$CLIPPY_RUST_VERSION" ]]; then - rustup component add clippy; - fi' - -script: - - cargo test - - bash -c 'if [[ "$TRAVIS_RUST_VERSION" == "$CLIPPY_RUST_VERSION" ]]; then - cargo clippy -- -D warnings; - fi' diff --git a/Cargo.lock b/Cargo.lock index a4bcde2..bd3b992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,18 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "block-buffer" version = "0.10.4" @@ -139,6 +151,16 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -148,6 +170,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "fs2" version = "0.4.3" @@ -168,12 +196,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + [[package]] name = "log" version = "0.4.20" @@ -221,6 +261,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "sha2" version = "0.10.8" @@ -249,6 +311,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -288,7 +363,9 @@ dependencies = [ "clap", "error-chain", "fs2", + "lazy_static", "openssh-keys", + "tempfile", "uzers", ] diff --git a/Cargo.toml b/Cargo.toml index 49109c2..1142346 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/coreos/update-ssh-keys" documentation = "https://docs.rs/update-ssh-keys" description = "A tool for managing authorized SSH keys" version = "0.4.2-alpha.0" +edition = "2021" [dependencies] fs2 = "0.4" @@ -15,6 +16,8 @@ error-chain = { version = "0.12", default-features = false } clap = { version = "4.4.6", features = ["cargo"] } uzers = "0.11.3" openssh-keys = "0.6.2" +lazy_static = "1.4.0" +tempfile = "3.8.0" [[bin]] name = "update-ssh-keys" diff --git a/README.md b/README.md index fbdae4a..64d8b56 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # update-ssh-keys -[![Build Status](https://travis-ci.org/coreos/update-ssh-keys.svg?branch=master)](https://travis-ci.org/coreos/update-ssh-keys) -![minimum rust 1.28](https://img.shields.io/badge/rust-1.28%2B-orange.svg) +[![Github CI](https://github.com/flatcar/update-ssh-keys/actions/workflows/rust.yml/badge.svg)](https://github.com/flatcar/update-ssh-keys/actions) +![minimum rust 1.60](https://img.shields.io/badge/rust-1.60%2B-orange.svg) `update-ssh-keys` is a command line tool and a library for managing openssh authorized public keys. It keeps track of sets of keys with names, allows for @@ -15,6 +15,6 @@ non-Container Linux machine, you can build the project with `cargo build --release`. The rust toolchain is required to build it. You can install `rustup` to manage your rust toolchain - https://www.rustup.rs. -`test/test_update_ssh_keys.py` is a python script which tests the functionality +`test/test_update_ssh_keys.rs` is a Rust program which tests the functionality of the `update-ssh-keys` command line tool. If changes are made to `update-ssh-keys`, that script should be run. diff --git a/src/lib.rs b/src/lib.rs index d4e4ae7..2dcf316 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -506,6 +506,11 @@ impl AuthorizedKeys { }) } + /// close file lock to release resources for other processes + pub fn close(&self) -> Result<()> { + self.lock.unlock() + } + /// get_keys gets the authorized keyset with the provided name pub fn get_keys(&self, name: &str) -> Option<&AuthorizedKeySet> { self.keys.get(name) diff --git a/tests/compat_python.rs b/tests/compat_python.rs deleted file mode 100644 index a319ea1..0000000 --- a/tests/compat_python.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::env; -use std::process::Command; - -// This runs the old python integration test-suite to ensure -// retro-compatibility. -#[test] -fn test_compat_python_suite() { - let pytests = env::current_dir() - .unwrap() - .join("tests") - .join("test_update_ssh_keys.py"); - let result = Command::new(pytests).output().unwrap(); - if !result.status.success() { - panic!( - "\nstdout: {}\nstderr: {}", - String::from_utf8_lossy(&result.stdout), - String::from_utf8_lossy(&result.stderr) - ); - }; - assert!(result.status.success()); -} diff --git a/tests/test_update_ssh_keys.py b/tests/test_update_ssh_keys.py deleted file mode 100755 index 353d00b..0000000 --- a/tests/test_update_ssh_keys.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env python2 -# Copyright 2017 CoreOS, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import pwd -import shutil -import subprocess -import tempfile -import unittest - -script_path = os.path.abspath('%s/../../target/debug/update-ssh-keys' % __file__) - -test_keys = { - 'valid1': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDULTftpWMj4nD+7Ps' - 'B8itam2T6Aqm9Z+ursQG1SRiK4ie5rHGJoteGnbH91Uix/HDE5GC3Hz' - 'ICQVOnQay4hwJUKRfEUEWj1Sncer/BL2igDquABlcXNl2dgOlfJ8a3q' - '6IZnQpdEe6Vrqg/Ui082UxuZ08pNV94M/5IhR2fx0EbY66PQ97o+ywH' - 'sB7oXDO8p/+mGL+h7cxFY7hILXTa5/3TGBEgcA65Rrmq22eiRt97RGh' - 'DjfzIqTqb8gwuhTSNN7FWDLrEyRwJMbaTgDSoMIZdLtndVrGEqFHUO+' - 'WzinSiEQCs2MDDnTk29bleHAEktu1x68GYhg9S7O/gZq8/swAV ' - 'core@valid1', - 'valid2': 'command="echo \\"test\\"" ssh-dss AAAAB3NzaC1kc3MAAACBAJA94Sqw80BSKjVTNZD6570nXIN' - 'hP8R2UhbBuydT+GI6CfA9Dw7O0udJQUfrqARFcRQR/syc72CO6jaKNE' - '3/A5E+8uVmRZt7s9VtA47s1qxqHswth74m1Nb86n2OTB0HcW63FsXo2' - 'cJF+r+l6F3IcRPi4z/eaEKG7uhAS59TjH2tAAAAFQC0I9kL3oceMT1O' - '44WPe6NZ8w8CMwAAAIABGm2Yg8nGFZbo/W8njuM79w0W2P1NBVNWzBH' - 'WQqVbr4i1bWTSSc9X+itQUpeF6zAUDsUoprhNise2NLrMYCLFo9JxhE' - 'iYAcEJ/YbKEnjtJzaAmQNpyh3rCWuOcGPTevjAZIkl+zEc+/N7tCW1e' - 'uDYm6IXZ8LEQyTUQUdU4pZ2OgAAAIABk1ZA3+TiCMaoAafNVUZ7zwqk' - '888yVOgsJ7HGGDGRMo5ytr2SUJB7QWsLX6Un/Zbu32nXsAqtqagxd6F' - 'Ies98TSekMh/hAv9uK92mEsXSINXOeIMKRedqOyPgk5IEOsFpxAUO4T' - 'xpYToeuM8HRemecxw2eIFHnax+mQqCsi7FgQ== core@valid2', - 'valid3': 'sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9w' - 'ZW5zc2guY29tAAAAIEX/dQ0v4127bEo8eeG1EV0ApO2lWbSnN6RWusn' - '/NjqIAAAABHNzaDo= demos@siril', - 'bad': 'ssh-bad this-not-a-key core@bad', -} - -fingerprints = { - 'valid1': 'SHA256:yZ+o48h6quk9c+JVgJ/Zq4S5u4LUk6TSpneHKkmM9KY', - 'valid2': 'SHA256:RP5k1AybZ1kollIAnpUavr1v1nfZ0yloKvI46AMDPkM ', - 'valid3': 'SHA256:U8IKRkIHed6vFMTflwweA3HhIf2DWgZ8EFTm9fgwOUk', -} - -class UpdateSshKeysTestCase(unittest.TestCase): - - def setUp(self): - user_info = pwd.getpwuid(os.getuid()) - self.user = user_info.pw_name - self.ssh_dir = tempfile.mkdtemp(prefix='test_update_ssh_keys') - self.env = os.environ.copy() - self.pub_files = {} - - for name, text in test_keys.iteritems(): - pub_path = '%s/%s.pub' % (self.ssh_dir, name) - self.pub_files[name] = pub_path - with open(pub_path, 'w') as pub_fd: - pub_fd.write('%s\n' % text) - - def tearDown(self): - shutil.rmtree(self.ssh_dir) - - def assertHasKeys(self, *keys): - with open('%s/authorized_keys' % self.ssh_dir, 'r') as fd: - text = fd.read() - self.assertTrue(text.startswith('# auto-generated')) - for key in keys: - self.assertIn(test_keys[key], text) - for key in test_keys: - if key in keys: - continue - self.assertNotIn(test_keys[key], text) - - def run_script(self, *args, **kwargs): - cmd = [script_path, '-u', self.user, '--ssh-dir', self.ssh_dir] - cmd.extend(args) - return subprocess.Popen(cmd, env=self.env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - **kwargs) - - def test_usage(self): - proc = self.run_script('-h') - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertTrue(out.startswith('Usage: ')) - self.assertEquals(err, '') - - def test_no_keys(self): - proc = self.run_script() - out, err = proc.communicate() - self.assertEquals(proc.returncode, 1) - self.assertEquals(out, '') - self.assertIn('no keys found', err) - self.assertTrue(os.path.isdir('%s/authorized_keys.d' % self.ssh_dir)) - self.assertFalse(os.path.exists('%s/authorized_keys' % self.ssh_dir)) - - def test_first_run(self): - with open('%s/authorized_keys' % self.ssh_dir, 'w') as fd: - fd.write('%s\n' % test_keys['valid1']) - fd.write('%s\n' % test_keys['valid2']) - fd.write('%s\n' % test_keys['valid3']) - proc = self.run_script() - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertTrue(out.startswith('Updated ')) - self.assertEquals(err, '') - self.assertTrue(os.path.exists( - '%s/authorized_keys.d/old_authorized_keys' % self.ssh_dir)) - self.assertHasKeys('valid1', 'valid2', 'valid3') - - def test_add_one_file(self): - proc = self.run_script('-a', 'one', self.pub_files['valid1']) - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertTrue(out.startswith('Adding')) - self.assertIn(fingerprints['valid1'], out) - self.assertIn('\nUpdated ', out) - self.assertEquals(err, '') - self.assertTrue(os.path.exists( - '%s/authorized_keys.d/one' % self.ssh_dir)) - self.assertHasKeys('valid1') - - def test_add_one_stdin(self): - proc = self.run_script('-a', 'one', stdin=subprocess.PIPE) - out, err = proc.communicate(test_keys['valid1']) - self.assertEquals(proc.returncode, 0) - self.assertTrue(out.startswith('Adding')) - self.assertIn(fingerprints['valid1'], out) - self.assertIn('\nUpdated ', out) - self.assertEquals(err, '') - self.assertTrue(os.path.exists( - '%s/authorized_keys.d/one' % self.ssh_dir)) - self.assertHasKeys('valid1') - - def test_replace_one(self): - self.test_add_one_file() - proc = self.run_script('-a', 'one', self.pub_files['valid2']) - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertIn(fingerprints['valid2'], out) - self.assertEquals(err, '') - self.assertHasKeys('valid2') - - def test_no_replace(self): - self.test_add_one_file() - proc = self.run_script('-n', '-a', 'one', self.pub_files['valid2']) - out, err = proc.communicate() - self.assertTrue(out.startswith('Skipping')) - self.assertEquals(proc.returncode, 0) - self.assertEquals(err, '') - self.assertHasKeys('valid1') - - proc = self.run_script('-n', '-A', 'one', self.pub_files['valid2']) - out, err = proc.communicate() - self.assertTrue(out.startswith('Skipping')) - self.assertEquals(proc.returncode, 0) - self.assertEquals(err, '') - self.assertHasKeys('valid1') - - def test_add_two(self): - self.test_add_one_file() - proc = self.run_script('-a', 'two', self.pub_files['valid2']) - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertIn(fingerprints['valid2'], out) - self.assertEquals(err, '') - self.assertHasKeys('valid1', 'valid2') - - def test_del_one(self): - self.test_add_one_file() - proc = self.run_script('-d', 'one') - out, err = proc.communicate() - self.assertEquals(proc.returncode, 1) - self.assertIn(fingerprints['valid1'], out) - self.assertIn('no keys found', err) - # Removed from authorized_keys.d but not authorized_keys - self.assertFalse(os.path.exists( - '%s/authorized_keys.d/one' % self.ssh_dir)) - self.assertHasKeys('valid1') - - def test_del_two(self): - self.test_add_two() - proc = self.run_script('-d', 'two') - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertIn(fingerprints['valid2'], out) - self.assertEquals(err, '') - self.assertHasKeys('valid1') - - def test_disable(self): - self.test_add_two() - proc = self.run_script('-D', 'two') - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertTrue(out.startswith('Disabling')) - self.assertIn(fingerprints['valid2'], out) - self.assertEquals(err, '') - self.assertHasKeys('valid1') - - proc = self.run_script('-a', 'two', self.pub_files['valid2']) - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertTrue(out.startswith('Skipping')) - self.assertEquals(err, '') - self.assertHasKeys('valid1') - - def test_enable(self): - self.test_disable() - proc = self.run_script('-A', 'two', self.pub_files['valid2']) - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertTrue(out.startswith('Adding')) - self.assertEquals(err, '') - self.assertHasKeys('valid1', 'valid2') - - def test_add_bad(self): - self.test_add_one_file() - proc = self.run_script('-a', 'bad', self.pub_files['bad']) - out, err = proc.communicate() - self.assertEquals(proc.returncode, 0) - self.assertIn('warning', out) - self.assertIn('failed to parse public key', out) - self.assertHasKeys('valid1') - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_update_ssh_keys.rs b/tests/test_update_ssh_keys.rs new file mode 100644 index 0000000..b785a1c --- /dev/null +++ b/tests/test_update_ssh_keys.rs @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2017-2023 Flatcar Authors + +extern crate update_ssh_keys; + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::fs; + use std::fs::File; + use std::io::{Read, Write}; + use std::path::PathBuf; + use tempfile; + + use uzers; + + use update_ssh_keys::errors::{Error, ErrorKind}; + use update_ssh_keys::{AuthorizedKeyEntry, AuthorizedKeys}; + + // As Rust does not support global variables by default, + // it is necessary to make use of lazy_static, so the variables + // could be accessed in multiple tests. + lazy_static::lazy_static! { + static ref TEST_KEYS: HashMap<&'static str, &'static str> = + [ + ( + "valid1", + "\ + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDULTftpWMj4nD+7Ps\ + B8itam2T6Aqm9Z+ursQG1SRiK4ie5rHGJoteGnbH91Uix/HDE5GC3Hz\ + ICQVOnQay4hwJUKRfEUEWj1Sncer/BL2igDquABlcXNl2dgOlfJ8a3q\ + 6IZnQpdEe6Vrqg/Ui082UxuZ08pNV94M/5IhR2fx0EbY66PQ97o+ywH\ + sB7oXDO8p/+mGL+h7cxFY7hILXTa5/3TGBEgcA65Rrmq22eiRt97RGh\ + DjfzIqTqb8gwuhTSNN7FWDLrEyRwJMbaTgDSoMIZdLtndVrGEqFHUO+\ + WzinSiEQCs2MDDnTk29bleHAEktu1x68GYhg9S7O/gZq8/swAV", + ), + ( + "valid2", + "\ + command=\"echo \\\"test\\\"\" ssh-dss AAAAB3NzaC1kc3MAAACBAJA94Sqw80BSKjVTNZD6570nXIN\ + hP8R2UhbBuydT+GI6CfA9Dw7O0udJQUfrqARFcRQR/syc72CO6jaKNE\ + 3/A5E+8uVmRZt7s9VtA47s1qxqHswth74m1Nb86n2OTB0HcW63FsXo2\ + cJF+r+l6F3IcRPi4z/eaEKG7uhAS59TjH2tAAAAFQC0I9kL3oceMT1O\ + 44WPe6NZ8w8CMwAAAIABGm2Yg8nGFZbo/W8njuM79w0W2P1NBVNWzBH\ + WQqVbr4i1bWTSSc9X+itQUpeF6zAUDsUoprhNise2NLrMYCLFo9JxhE\ + iYAcEJ/YbKEnjtJzaAmQNpyh3rCWuOcGPTevjAZIkl+zEc+/N7tCW1e\ + uDYm6IXZ8LEQyTUQUdU4pZ2OgAAAIABk1ZA3+TiCMaoAafNVUZ7zwqk\ + 888yVOgsJ7HGGDGRMo5ytr2SUJB7QWsLX6Un/Zbu32nXsAqtqagxd6F\ + Ies98TSekMh/hAv9uK92mEsXSINXOeIMKRedqOyPgk5IEOsFpxAUO4T\ + xpYToeuM8HRemecxw2eIFHnax+mQqCsi7FgQ== core@valid2", + ), + ( + "valid3", + "\ + sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9w\ + ZW5zc2guY29tAAAAIEX/dQ0v4127bEo8eeG1EV0ApO2lWbSnN6RWusn\ + /NjqIAAAABHNzaDo= demos@siril", + ), + ( + "bad", + "ssh-bad this-not-a-key core@bad", + ), + ].iter().cloned().collect(); + + static ref FINGERPRINTS: HashMap<&'static str, &'static str> = + [ + ( + "valid1", + "SHA256:yZ+o48h6quk9c+JVgJ/Zq4S5u4LUk6TSpneHKkmM9KY", + ), + ( + "valid2", + "SHA256:RP5k1AybZ1kollIAnpUavr1v1nfZ0yloKvI46AMDPkM", + ), + ( + "valid3", + "SHA256:U8IKRkIHed6vFMTflwweA3HhIf2DWgZ8EFTm9fgwOUk", + ), + ].iter().cloned().collect(); + } + + // TestContext holds path to a temporary directory used by the current test. + struct TestContext { + ssh_dir: PathBuf, + aks: AuthorizedKeys, + } + + // Automatically clean up ssh_dir when each test finished. + impl Drop for TestContext { + fn drop(&mut self) { + _ = fs::remove_dir_all(PathBuf::from(self.ssh_dir.clone())); + } + } + + // ssh_dir: path to ssh directory + // keys: Vec of key strings + fn assert_has_keys(ssh_dir: &str, keys: Vec<&str>) { + let authkeyspath: PathBuf = PathBuf::from(format!("{}/authorized_keys", ssh_dir)); + let authkeysfile = File::open(authkeyspath.clone()); + assert!(authkeysfile.is_ok()); + let mut authkeystext = String::new(); + authkeysfile + .unwrap() + .read_to_string(&mut authkeystext) + .expect( + format!( + "unable to read a file {}.", + authkeyspath.to_str().unwrap_or_default() + ) + .as_str(), + ); + assert!(authkeystext.starts_with("# auto-generated")); + + for key in &keys { + assert!(authkeystext.contains(&*TEST_KEYS[key])); + } + + for (key, _) in &*TEST_KEYS { + if keys.contains(&key) { + continue; + } + assert!(!authkeystext.contains(&*TEST_KEYS[key])); + } + } + + fn add_key_check_results( + ssh_dir: &str, + keys: Vec, + pubkeyname: &str, + testname: &str, + assert_keys: Vec<&str>, + ) { + for key in &keys { + if let AuthorizedKeyEntry::Valid { ref key } = *key { + assert!(key + .to_fingerprint_string() + .contains(&*FINGERPRINTS[pubkeyname])); + + let authkeyone: PathBuf = + PathBuf::from(format!("{}/authorized_keys.d/{}", ssh_dir, testname)); + + assert!(authkeyone.exists()); + + assert_has_keys(ssh_dir, assert_keys.clone()); + } + } + } + + fn open_authorized_keys(ssh_dir: &str) -> AuthorizedKeys { + let ssh_dir: PathBuf = PathBuf::from(ssh_dir); + let unameosstr = uzers::get_current_username().unwrap_or_default(); + let unamestr = unameosstr.to_str().unwrap_or_default(); + let user = uzers::get_user_by_name(&unamestr) + .ok_or_else(|| format!("failed to find user with name '{}'", unamestr)) + .expect("failed to resolve user"); + + AuthorizedKeys::open(user, true, Some(ssh_dir)).expect( + format!( + "failed to open authorized_keys directory for user '{}'", + unamestr + ) + .as_str(), + ) + } + + // A wrapper for adding an ssh key. + // + // pubkeyname: name of ssh public key, like "valid1", "bad" + // testname: key name given as cmdline args, like "one", "two" + // is_force: whether to force adding a key via "--add-force" + // is_replace: whether to adding a key by replacing an existing one, unless "--no-replace". + // assert_keys: Vec of keys to be asserted after add_keys() succeeded. + fn add_one_ssh_key( + ssh_dir: &str, + aks: &mut AuthorizedKeys, + pubkeyname: &str, + testname: &str, + is_force: bool, + is_replace: bool, + ) -> Result, ErrorKind> { + let keyfiles = [format!("{}/{}.pub", ssh_dir, pubkeyname)]; + + let mut keys = vec![]; + for keyfile in keyfiles { + let file = File::open(&keyfile) + .expect(format!("failed to open keyfile '{:?}'", keyfile).as_str()); + keys.append(&mut AuthorizedKeys::read_keys(file).unwrap_or_default()); + } + + let res = aks.add_keys(testname, keys.clone(), is_replace, is_force); + + match res { + Ok(_) => {} + Err(Error(ErrorKind::KeysDisabled(name), _)) => { + println!("Skipping add {}, disabled.", name); + } + Err(Error(ErrorKind::KeysExist(_), _)) => { + println!("Skipping add {}, already exists.", testname); + } + Err(err) => { + return Err(err.into()); + } + } + + match aks.write() { + Ok(_) => {} + Err(err) => return Err(err.into()), + } + + match aks.sync() { + Ok(_) => {} + Err(err) => return Err(err.into()), + } + + Ok(keys) + } + + // A wrapper for deleting an ssh key. + // + // testname: key name given as cmdline args, like "one", "two" + fn del_one_ssh_key( + aks: &mut AuthorizedKeys, + testname: &str, + ) -> Result, ErrorKind> { + let mut akes: Vec = Vec::new(); + + for key in aks.remove_keys(testname) { + if let AuthorizedKeyEntry::Invalid { key: _ } = key { + return Err(ErrorKind::KeysExist(testname.to_string())); + } + + akes.push(key); + } + + match aks.write() { + Ok(_) => {} + Err(err) => return Err(err.into()), + } + + match aks.sync() { + Ok(_) => {} + Err(err) => return Err(err.into()), + } + + Ok(akes) + } + + // Create a common TestContext to be used for all tests. + // Note: it is not possible for TestContext to have AuthorizedKeys, due to + // mutability issues. + fn setup_tests() -> TestContext { + let ssh_dir_path: PathBuf = tempfile::tempdir().unwrap().into_path(); + + for (name, text) in &*TEST_KEYS { + let pub_path = format!("{}/{}.pub", ssh_dir_path.to_str().unwrap(), name); + + let mut pubfile = File::create(pub_path.clone()) + .expect(format!("unable to create a file {}", pub_path.as_str()).as_str()); + let _ = pubfile.write_all(text.as_bytes()); + } + + let aksm = open_authorized_keys(ssh_dir_path.to_str().unwrap()); + + TestContext { + ssh_dir: ssh_dir_path.clone(), + aks: aksm, + } + } + + #[test] + fn test_no_keys() { + let ctx = setup_tests(); + let ssh_dir = ctx.ssh_dir.to_str().unwrap(); + + ctx.aks + .write() + .expect("failed to update authorized keys directory"); + assert!(format!("{}", ctx.aks.sync().unwrap_err().kind()).contains("no keys found")); + + let authkeysdir: PathBuf = PathBuf::from(format!("{}/authorized_keys.d", ssh_dir)); + assert!(authkeysdir.is_dir()); + + let authkeys: PathBuf = PathBuf::from(format!("{}/authorized_keys", ssh_dir)); + assert!(!authkeys.exists()); + } + + #[test] + fn test_first_run() { + let mut ctx = setup_tests(); + let ssh_dir = ctx.ssh_dir.to_str().unwrap(); + + let authkeys: PathBuf = PathBuf::from(format!("{}/authorized_keys", ssh_dir)); + let mut authkeysfile = File::options() + .create(true) + .append(true) + .write(true) + .open(authkeys.clone()) + .expect( + format!( + "unable to create a file {}", + authkeys.to_str().unwrap_or_default() + ) + .as_str(), + ); + for (_, text) in &*TEST_KEYS { + _ = authkeysfile.write_all(format!("{}\n", text).as_bytes()); + } + + // Since aks has been already opened by setup_tests(), we have to close and reopen + // to re-read the contents (opening twice would make it hang forever). + let _ = ctx.aks.close(); + ctx.aks = open_authorized_keys(ctx.ssh_dir.to_str().unwrap()); + + // equivalent of running "update-ssh-keys" without args + ctx.aks + .write() + .expect("failed to update authorized keys directory"); + ctx.aks.sync().expect("failed to update authorized keys"); + + let authkeys: PathBuf = + PathBuf::from(format!("{}/authorized_keys.d/old_authorized_keys", ssh_dir)); + assert!(authkeys.exists()); + assert_has_keys(ssh_dir, ["valid1", "valid2", "valid3"].to_vec()); + } + + #[test] + fn test_add_one_file() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + run_test_add_one_file(&ctx.ssh_dir, aks); + } + + fn run_test_add_one_file(ssh_dir: &PathBuf, aks: &mut AuthorizedKeys) { + let ssh_dir = ssh_dir.to_str().unwrap(); + let pubkeyvalid1 = "valid1"; + let testnameone = "one"; + let assert_keys = ["valid1"].to_vec(); + + // "update-ssh-keys --add one valid1.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid1, // pubkeyname + testnameone, // testname + false, // is_force + true, // is_replace + ) { + Ok(keys) => { + add_key_check_results(ssh_dir, keys, pubkeyvalid1, testnameone, assert_keys) + } + Err(err) => panic!("update_ssh_keys --add failed {}", err), + } + } + + #[test] + fn test_replace_one() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + let ssh_dir = ctx.ssh_dir.to_str().unwrap(); + let pubkeyvalid1 = "valid1"; + let pubkeyvalid2 = "valid2"; + let testnameone = "one"; + let assert_keys = ["valid1"].to_vec(); + + // "update-ssh-keys --add one valid1.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid1, // pubkeyname + testnameone, // testname + false, // is_force + true, // is_replace + ) { + Ok(keys) => { + add_key_check_results(ssh_dir, keys, pubkeyvalid1, testnameone, assert_keys) + } + Err(err) => panic!("update_ssh_keys --add failed {}", err), + } + + let assert_keys = ["valid2"].to_vec(); + + // "update-ssh-keys --add one valid2.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid2, // pubkeyname + testnameone, // testname + false, // is_force + true, // is_replace + ) { + Ok(keys) => { + add_key_check_results(ssh_dir, keys, pubkeyvalid2, testnameone, assert_keys) + } + Err(err) => panic!("update_ssh_keys --add failed {}", err), + } + } + + #[test] + fn test_no_replace() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + let ssh_dir = ctx.ssh_dir.to_str().unwrap(); + let pubkeyvalid1 = "valid1"; + let pubkeyvalid2 = "valid2"; + let testnameone = "one"; + let assert_keys = ["valid1"].to_vec(); + + // "update-ssh-keys --add one valid1.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid1, // pubkeyname + testnameone, // testname + false, // is_force + true, // is_replace + ) { + Ok(keys) => add_key_check_results( + ssh_dir, + keys, + pubkeyvalid1, + testnameone, + assert_keys.clone(), + ), + Err(err) => panic!("update_ssh_keys --add failed {}", err), + } + + // "update-ssh-keys --no-replace --add one valid2.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid2, // pubkeyname + testnameone, // testname + false, // is_force + false, // is_replace + ) { + Ok(keys) => add_key_check_results( + ssh_dir, + keys, + pubkeyvalid2, + testnameone, + assert_keys.clone(), + ), + Err(err) => panic!("update_ssh_keys --no-replace --add failed {}", err), + } + + // "update-ssh-keys --no-replace --add-force one valid2.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid2, // pubkeyname + testnameone, // testname + true, // is_force + false, // is_replace + ) { + Ok(keys) => { + add_key_check_results(ssh_dir, keys, pubkeyvalid2, testnameone, assert_keys) + } + Err(err) => panic!("update_ssh_keys --no-replace --add-force failed {}", err), + } + } + + #[test] + fn test_add_two() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + run_test_add_two(&ctx.ssh_dir, aks); + } + + fn run_test_add_two(ssh_dir: &PathBuf, aks: &mut AuthorizedKeys) { + let ssh_dir = ssh_dir.to_str().unwrap(); + let pubkeyvalid1 = "valid1"; + let pubkeyvalid2 = "valid2"; + let testnameone = "one"; + let testnametwo = "two"; + let assert_keys = ["valid1"].to_vec(); + + // "update-ssh-keys --add one valid1.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid1, // pubkeyname + testnameone, // testname + false, // is_force + true, // is_replace + ) { + Ok(keys) => { + add_key_check_results(ssh_dir, keys, pubkeyvalid1, testnameone, assert_keys) + } + Err(err) => panic!("update_ssh_keys --add failed {}", err), + } + + let assert_keys = ["valid1", "valid2"].to_vec(); + + // "update-ssh-keys --add two valid2.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid2, // pubkeyname + testnametwo, // testname + false, // is_force + true, // is_replace + ) { + Ok(keys) => { + add_key_check_results(ssh_dir, keys, pubkeyvalid2, testnametwo, assert_keys) + } + Err(err) => panic!("update_ssh_keys --add failed {}", err), + } + } + + #[test] + fn test_del_one() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + run_test_add_one_file(&ctx.ssh_dir, aks); + + let ssh_dir = ctx.ssh_dir.to_str().unwrap(); + let pubkeyvalid1 = "valid1"; + let testnameone = "one"; + + // "update-ssh-keys --delete one valid1.pub" + match del_one_ssh_key(aks, testnameone) { + Ok(_) => panic!("unexpected test success"), + Err(err) => { + println!("update_ssh_keys --delete failed"); + + assert!(format!("{}", err).contains("no keys found")); + + // NOTE: it is not possible to check for fingerprint, as key is not available in + // the context. + // assert!(key.to_fingerprint_string().contains(&*FINGERPRINTS[pubkeyvalid1])); + + let authkeyone: PathBuf = + PathBuf::from(format!("{}/authorized_keys.d/{}", ssh_dir, testnameone)); + + assert!(!authkeyone.exists()); + + let mut svec: Vec<&str> = Vec::new(); + svec.push(pubkeyvalid1); + assert_has_keys(ssh_dir, svec); + } + } + } + + #[test] + fn test_del_two() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + run_test_add_two(&ctx.ssh_dir, aks); + + // "update-ssh-keys --delete two valid2.pub" + let ssh_dir = ctx.ssh_dir.to_str().unwrap(); + let pubkeyvalid1 = "valid1"; + let pubkeyvalid2 = "valid2"; + let testnametwo = "two"; + + match del_one_ssh_key(aks, testnametwo) { + Ok(ake) => { + println!("update_ssh_keys --delete passed"); + + for key in ake { + if let AuthorizedKeyEntry::Valid { ref key } = key { + assert!(key + .to_fingerprint_string() + .contains(&*FINGERPRINTS[pubkeyvalid2])); + + let authkeyone: PathBuf = + PathBuf::from(format!("{}/authorized_keys.d/{}", ssh_dir, testnametwo)); + + assert!(!authkeyone.exists()); + + let mut svec: Vec<&str> = Vec::new(); + svec.push(pubkeyvalid1); + assert_has_keys(ssh_dir, svec); + } + } + } + Err(err) => panic!("update_ssh_keys --delete failed {}", err), + } + } + + fn run_test_disable(ssh_dir_input: &PathBuf, aks: &mut AuthorizedKeys) { + let ssh_dir = ssh_dir_input.to_str().unwrap(); + + run_test_add_two(ssh_dir_input, aks); + + // "update-ssh-keys --disable two" + let pubkeyvalid1 = "valid1"; + let pubkeyvalid2 = "valid2"; + let testnametwo = "two"; + + let keyfiles = [format!("{}/{}.pub", ssh_dir, pubkeyvalid2)]; + + let mut keys = vec![]; + for keyfile in keyfiles { + let file = File::open(&keyfile) + .expect(format!("failed to open keyfile '{:?}'", keyfile).as_str()); + keys.append(&mut AuthorizedKeys::read_keys(file).unwrap_or_default()); + } + + for key in aks.disable_keys("two") { + if let AuthorizedKeyEntry::Valid { ref key } = key { + assert!(key + .to_fingerprint_string() + .contains(&*FINGERPRINTS[pubkeyvalid2])); + + aks.write() + .expect("failed to update authorized keys directory"); + + aks.sync().expect("failed to update authorized keys"); + + let authkeyone: PathBuf = + PathBuf::from(format!("{}/authorized_keys.d/{}", ssh_dir, "two")); + assert!(authkeyone.exists()); + + let mut svec: Vec<&str> = Vec::new(); + svec.push(pubkeyvalid1); + assert_has_keys(ssh_dir, svec); + } + } + + // add two again + // "update-ssh-keys --add two valid2.pub" + let assert_keys = ["valid1"].to_vec(); + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid2, // pubkeyname + testnametwo, // testname + false, // is_force + true, // is_replace + ) { + Ok(keys) => { + add_key_check_results(ssh_dir, keys, pubkeyvalid2, testnametwo, assert_keys); + } + Err(err) => panic!("update_ssh_keys --add failed {}", err), + } + } + + #[test] + fn test_disable() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + run_test_disable(&ctx.ssh_dir, aks); + } + + #[test] + fn test_enable() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + let ssh_dir_path = &ctx.ssh_dir; + let ssh_dir = ssh_dir_path.to_str().unwrap(); + let pubkeyvalid2 = "valid2"; + let testnametwo = "two"; + let assert_keys = ["valid1", "valid2"].to_vec(); + + // "update-ssh-keys --disable two" + run_test_disable(ssh_dir_path, aks); + + // "update-ssh-keys --add-force two valid2.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeyvalid2, // pubkeyname + testnametwo, // testname + true, // is_force + true, // is_replace + ) { + Ok(keys) => { + add_key_check_results(ssh_dir, keys, pubkeyvalid2, testnametwo, assert_keys) + } + Err(err) => panic!("update_ssh_keys --add-force failed {}", err), + } + } + + #[test] + fn test_add_bad() { + let mut ctx = setup_tests(); + let aks = &mut ctx.aks; + + let ssh_dir_path = &ctx.ssh_dir; + let ssh_dir = ssh_dir_path.to_str().unwrap(); + let pubkeybad = "bad"; + let testnamebad = "bad"; + let assert_keys = ["valid1"].to_vec(); + + run_test_add_one_file(&ctx.ssh_dir, aks); + + // "update-ssh-keys --add bad bad.pub" + match add_one_ssh_key( + ssh_dir, // ssh_dir + aks, // AuthorizedKeys + pubkeybad, // pubkeyname + testnamebad, // testname + false, // is_force + false, // is_replace + ) { + Ok(_) => assert_has_keys(ctx.ssh_dir.to_str().unwrap(), assert_keys), + Err(err) => panic!("update_ssh_keys --add failed {}", err), + } + } +}