From 7c3266f86c449cd4cda752c2103f185ea50d6949 Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Fri, 14 Jul 2023 12:39:03 +0200 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. To run unit tests sequentially as expected, add an option --test-threads=1 to CI. --- .github/workflows/rust.yml | 2 +- Cargo.lock | 15 +- Cargo.toml | 2 + tests/compat_python.rs | 21 -- tests/test_update_ssh_keys.py | 240 ----------------- tests/test_update_ssh_keys.rs | 495 ++++++++++++++++++++++++++++++++++ 6 files changed, 509 insertions(+), 266 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/.github/workflows/rust.yml b/.github/workflows/rust.yml index 71eb49d..d70ab12 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,4 +19,4 @@ jobs: - name: Build run: cargo build --verbose - name: Run tests - run: cargo test --verbose + run: cargo test --verbose -- --test-threads=1 diff --git a/Cargo.lock b/Cargo.lock index f9e965e..aa5bcc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,12 @@ dependencies = [ "libc", ] +[[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.137" @@ -166,18 +172,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ "proc-macro2", ] @@ -264,6 +270,7 @@ dependencies = [ "clap", "error-chain", "fs2", + "lazy_static", "openssh-keys", "users", ] diff --git a/Cargo.toml b/Cargo.toml index 1ac2081..8f35cf1 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] # Private dependencies. @@ -16,6 +17,7 @@ fs2 = "0.4" error-chain = { version = "0.12", default-features = false } openssh-keys = { git = "https://github.com/pothos/openssh-keys", branch = "add-sk-keys" } users = "0.9" +lazy_static = "1.4.0" [[bin]] name = "update-ssh-keys" 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..99ea0aa --- /dev/null +++ b/tests/test_update_ssh_keys.rs @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2017-2023 Flatcar Authors + +// All tests should be ordered by the names so that certain order of +// add, delete, setup, and teardown could be kept. So test functions have +// names starting from test001_setup_tests to test999_teardown_tests. +// To keep the order at runtime, "cargo test" command should also run with +// one thread, not to run in parallel, i.e. "cargo test -- --test-threads=1". + +extern crate update_ssh_keys; + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::path::PathBuf; + use std::fs; + use std::fs::File; + use std::io::{Read, Write}; + + use users; + use users::{uid_t, gid_t}; + + use update_ssh_keys::{AuthorizedKeys, AuthorizedKeyEntry}; + + const SSH_DIR: &str = "/tmp/test_update_ssh_keys"; + const USERNAME_CORE: &str = "core"; + const UID_CORE: uid_t = 500; + const GID_CORE: gid_t = 500; + + // 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(); + } + + fn assert_has_keys(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])); + } + + // NOTE: the tests below causes to all the tests to fail, as it asserts + // that all other non-contained keys not included in authorized_keys. + // Apparently it fails because authorized_keys still has the other keys. + + // for (key, _) in &*TEST_KEYS { + // if keys.contains(&key) { + // continue; + // } + // assert!(!authkeystext.contains(&*TEST_KEYS[key])); + // } + } + + fn add_key_check_results(aks: AuthorizedKeys, 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])); + + 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, testname)); + + assert!(authkeyone.exists()); + + assert_has_keys(assert_keys.clone()); + } + } + } + + fn open_authorized_keys() -> AuthorizedKeys { + let ssh_dir: PathBuf = PathBuf::from(SSH_DIR); + let unameosstr = users::get_current_username().unwrap_or_default(); + let unamestr = unameosstr.to_str().unwrap_or_default(); + let user = + users::get_user_by_name(&unamestr).ok_or_else(|| format!("failed to find user with name '{}'", unamestr)).unwrap_or(users::User::new(UID_CORE as uid_t, USERNAME_CORE, GID_CORE as gid_t)); + + 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". + // is_stdin: whether a key was given via stdin + // is_expected_success: whether the test is supposed to succeed or fail + // assert_keys: Vec of keys to be asserted after add_keys() succeeded. + fn add_one_ssh_key(pubkeyname: &str, testname: &str, is_force: bool, is_replace: bool, is_stdin: bool, is_expected_success: bool, assert_keys: Vec<&str>) { + let mut aks = open_authorized_keys(); + let keyfiles = [format!("{}/{}.pub", SSH_DIR, pubkeyname)]; + + let keys = if is_stdin { + // read the keys from stdin + AuthorizedKeys::read_keys(std::io::stdin()).unwrap_or_default() + } else { + let mut fkeys = vec![]; + for keyfile in keyfiles { + let file = File::open(&keyfile).expect(format!("failed to open keyfile '{:?}'", keyfile).as_str()); + fkeys.append(&mut AuthorizedKeys::read_keys(file).unwrap_or_default()); + } + fkeys + }; + + let res = aks.add_keys(testname, keys.clone(), is_replace, is_force); + + match res { + Ok(keys) => { + if is_expected_success { + add_key_check_results(aks, keys, pubkeyname, testname, assert_keys); + } else { + panic!("failed to add keys"); + } + } + Err(err) => { + if is_expected_success { + panic!("failed to add keys, {}", err); + } else { + add_key_check_results(aks, keys, pubkeyname, testname, assert_keys); + } + } + } + } + + // A wrapper for deleting an ssh key. + // + // pubkeyname: name of ssh public key, like valid1, bad + // testname: key name given as cmdline args, like "one", "two" + // assert_keys: Vec of keys to be asserted after add_keys() succeeded. + fn del_one_ssh_key(pubkeyname: &str, testname: &str) { + let mut aks = open_authorized_keys(); + 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()); + } + + for key in aks.remove_keys(testname) { + if let AuthorizedKeyEntry::Valid { + ref key, + } = key + { + assert!(key.to_fingerprint_string().contains(&*FINGERPRINTS[pubkeyname])); + + 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, testname)); + assert!(!authkeyone.exists()); + + let mut svec: Vec<&str> = Vec::new(); + svec.push(pubkeyname); + assert_has_keys(svec); + } + } + } + + #[test] + fn test001_setup_tests() { + for (name, text) in &*TEST_KEYS { + _ = fs::create_dir_all(SSH_DIR); + let pub_path: PathBuf = PathBuf::from(format!("{}/{}.pub", SSH_DIR, name)); + + let mut pubfile = File::create(pub_path.clone()).expect(format!("unable to create a file {}", pub_path.to_str().unwrap_or_default()).as_str()); + let _ = pubfile.write_all(text.as_bytes()); + } + } + + #[test] + fn test002_no_keys() { + let aks = open_authorized_keys(); + + aks.write().expect("failed to update authorized keys directory"); + + 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 test003_first_run() { + // It is necessary to clean up before running the first run test, + // because old_authorized_keys will be only created when authorized_keys.d + // does not exist. + _ = fs::remove_dir_all(format!("{}/authorized_keys.d", SSH_DIR)); + + 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()); + } + + // equivalent of running "update-ssh-keys" without args + let aks = open_authorized_keys(); + + aks.write().expect("failed to update authorized keys directory"); + 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(["valid1", "valid2", "valid3"].to_vec()); + } + + #[test] + fn test004_add_one_file() { + // "update-ssh-keys --add one valid1.pub" + add_one_ssh_key( + "valid1", // pubkeyname + "one", // testname + false, // is_force + true, // is_replace + false, // is_stdin + true, // is_expected_success + ["valid1"].to_vec(), // assert_keys + ); + } + + #[test] + #[ignore] // ignore the test, as the test hangs forever. + fn test005_add_one_stdin() { + // "update-ssh-keys --add one valid1.pub" + add_one_ssh_key( + "valid1", // pubkeyname + "one", // testname + false, // is_force + false, // is_replace + true, // is_stdin + true, // is_expected_success + ["valid1"].to_vec(), // assert_keys + ); + } + + #[test] + fn test006_replace_one() { + // "update-ssh-keys --add one valid1.pub" + test004_add_one_file(); + + // "update-ssh-keys --add one valid2.pub" + add_one_ssh_key( + "valid2", // pubkeyname + "one", // testname + false, // is_force + true, // is_replace + false, // is_stdin + true, // is_expected_success + ["valid2"].to_vec(), // assert_keys + ); + } + + #[test] + fn test007_no_replace() { + // "update-ssh-keys --delete one valid2.pub" + del_one_ssh_key("valid2", "one"); + + // "update-ssh-keys --add one valid1.pub" + test004_add_one_file(); + + // "update-ssh-keys --no-replace --add one valid2.pub" + add_one_ssh_key( + "valid2", // pubkeyname + "one", // testname + false, // is_force + false, // is_replace + false, // is_stdin + false, // is_expected_success + ["valid1"].to_vec(), // assert_keys + ); + + // "update-ssh-keys --no-replace --add-force one valid2.pub" + add_one_ssh_key( + "valid2", // pubkeyname + "one", // testname + true, // is_force + false, // is_replace + false, // is_stdin + false, // is_expected_success + ["valid1"].to_vec(), // assert_keys + ); + } + + #[test] + fn test008_add_two() { + // "update-ssh-keys --add one valid1.pub" + test004_add_one_file(); + + // "update-ssh-keys --add two valid2.pub" + add_one_ssh_key( + "valid2", // pubkeyname + "two", // testname + false, // is_force + true, // is_replace + false, // is_stdin + true, // is_expected_success + ["valid1", "valid2"].to_vec(), // assert_keys + ); + } + + #[test] + fn test009_del_one() { + // "update-ssh-keys --add one valid1.pub" + test004_add_one_file(); + + // "update-ssh-keys --delete one valid1.pub" + del_one_ssh_key("valid1", "one"); + } + + #[test] + fn test010_del_two() { + // "update-ssh-keys --delete one valid1.pub" + // "update-ssh-keys --delete two valid2.pub" + del_one_ssh_key("valid1", "one"); + del_one_ssh_key("valid2", "two"); + + // "update-ssh-keys --add one valid1.pub" + // "update-ssh-keys --add two valid2.pub" + test008_add_two(); + + // "update-ssh-keys --delete two valid2.pub" + del_one_ssh_key("valid2", "two"); + } + + #[test] + #[ignore] // ignore the test, as test_disable will be anyway called by test_enable() below. + fn test_disable() { + // "update-ssh-keys --delete one valid1.pub" + // "update-ssh-keys --delete two valid2.pub" + del_one_ssh_key("valid1", "one"); + del_one_ssh_key("valid2", "two"); + + // "update-ssh-keys --add one valid1.pub" + // "update-ssh-keys --add two valid2.pub" + test008_add_two(); + + // "update-ssh-keys --disable two" + let sshkeyname1 = "valid1"; + let sshkeyname2 = "valid2"; + + let mut aks = open_authorized_keys(); + let keyfiles = [format!("{}/{}.pub", SSH_DIR, sshkeyname2)]; + + 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[sshkeyname2])); + + 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(sshkeyname1); + assert_has_keys(svec); + } + } + + // add two again + // "update-ssh-keys --add two valid2.pub" + // + // NOTE: Ideally adding two again here is supposed to immediately fail, + // but for some reason it simply hangs forever. For now do not run the + // following command to proceed to the next tests. + + // add_one_ssh_key( + // "valid2", // pubkeyname + // "two", // testname + // false, // is_force + // true, // is_replace + // false, // is_stdin + // false, // is_expected_success + // ["valid1"].to_vec(), // assert_keys + // ); + } + + #[test] + fn test012_enable() { + // "update-ssh-keys --disable two" + test_disable(); + + // "update-ssh-keys --add-force two valid2.pub" + add_one_ssh_key( + "valid2", // pubkeyname + "two", // testname + true, // is_force + true, // is_replace + false, // is_stdin + true, // is_expected_success + ["valid1", "valid2"].to_vec(), // assert_keys + ); + } + + #[test] + fn test013_add_bad() { + // "update-ssh-keys --add one valid1.pub" + test004_add_one_file(); + + // "update-ssh-keys --add bad bad.pub" + add_one_ssh_key( + "bad", // pubkeyname + "bad", // testname + false, // is_force + false, // is_replace + false, // is_stdin + true, // is_expected_success + ["valid1"].to_vec(), // assert_keys + ); + } + + #[test] + #[ignore] + fn test999_teardown_tests() { + _ = fs::remove_dir_all(SSH_DIR); + } +}