From 78a7f542d2cc21be828323765113b5998987ca65 Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Fri, 27 Oct 2023 17:58:57 +0200 Subject: [PATCH 1/3] README: update CI status URL, badge, test program Update URLs to CI status, badge, update programming language of test programs. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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. From f4e31f790e319896dee8dafa3f9bb393923c162f Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Fri, 27 Oct 2023 18:02:24 +0200 Subject: [PATCH 2/3] .travis.yml: delete travis config Travis CI is not used any more. Delete. --- .travis.yml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .travis.yml 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' From fe5500d5e0593ec75729adc51e11b3bd442f6faf Mon Sep 17 00:00:00 2001 From: Dongsu Park Date: Tue, 21 Nov 2023 16:19:22 +0100 Subject: [PATCH 3/3] 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), + } + } +}