From fe5500d5e0593ec75729adc51e11b3bd442f6faf Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Tue, 21 Nov 2023 16:19:22 +0100 Subject: [PATCH] tests: migrate unit tests to Rust To avoid issues like python2 not available on distros, we should simply migrate unit tests to Rust. That would make more sense, as the main code is already written in Rust. Adding edition in Cargo.toml. Without edition = 2021, build fails due to a missing "extern crate ...". Specifying an edition, Build passes without having to add it. 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. --- Cargo.lock | 77 ++++ Cargo.toml | 3 + src/lib.rs | 5 + tests/compat_python.rs | 21 - tests/test_update_ssh_keys.py | 240 ------------ tests/test_update_ssh_keys.rs | 710 ++++++++++++++++++++++++++++++++++ 6 files changed, 795 insertions(+), 261 deletions(-) delete mode 100644 tests/compat_python.rs delete mode 100755 tests/test_update_ssh_keys.py create mode 100644 tests/test_update_ssh_keys.rs 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/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), + } + } +}