#![allow(dead_code)]
use std::sync::Arc;
use std::time::Duration;
use gunnar_core::model::{Grant, Level, PrincipalKind, Visibility};
use gunnar_core::{AuthTable, ControlStore, NewRepo, RepoCatalog, RepoRef};
use gunnar_ssh::seam::BannerTransport;
use gunnar_ssh::{Access, Control, Fingerprint, GunnarSsh, Principal, SshConfig};
use russh::keys::ssh_key::{Algorithm, PrivateKey};
use russh::keys::PrivateKeyWithHashAlg;
pub fn mint_key() -> PrivateKey {
PrivateKey::random(&mut gunnar_core::os_rng(), Algorithm::Ed25519).expect("mint an ed25519 key")
}
pub fn fingerprint_of(key: &PrivateKey) -> Fingerprint {
gunnar_ssh::fingerprint_of_key(key.public_key()).expect("a freshly minted key re-encodes")
}
pub fn authorized_keys_line(key: &PrivateKey) -> String {
key.public_key()
.to_openssh()
.expect("encode an authorized_keys line")
}
pub fn principal(id: &str) -> Principal {
Principal::new(id, id, PrincipalKind::User).expect("a legal principal id")
}
pub fn the_repo() -> RepoRef {
RepoRef::new("nordisk", "osm-katana").expect("a legitimate path")
}
pub struct Plane {
pub control: Control,
pub store: Arc<dyn ControlStore>,
}
impl Plane {
pub fn empty() -> Self {
let store = gunnar_core::memory_control_store().expect("control store");
Plane {
control: Control::open(Arc::clone(&store)).expect("control plane"),
store,
}
}
fn corrupt(&self, name: &str, junk: &[u8]) {
let id = self.store.put_blob(junk).expect("store the junk");
let current = self.store.read_doc(name).expect("read the document");
assert!(
self.store
.commit_doc(name, current, id)
.expect("commit the junk"),
"the compare-and-swap refused; the fixture did not manage to corrupt {name}"
);
}
pub fn auth(&self) -> &AuthTable {
self.control.auth()
}
pub fn catalog(&self) -> &RepoCatalog {
self.control.catalog()
}
pub fn with_the_repo(self) -> Self {
let path = the_repo();
self.catalog()
.create_namespace(path.namespace())
.expect("create namespace");
self.catalog()
.create(NewRepo::new(path).visibility(Visibility::Private))
.expect("create repository");
self
}
pub fn with_principal(self, id: &str) -> Self {
self.auth().add_principal(principal(id)).expect("principal");
self
}
pub fn with_key(self, id: &str, key: &PrivateKey) -> Self {
self.auth()
.register_key(id, &authorized_keys_line(key), None)
.expect("register key");
self
}
pub fn with_grant(self, id: &str, level: Access) -> Self {
self.auth()
.grant(Grant::repo(id, the_repo(), level))
.expect("grant");
self
}
pub fn with_namespace_grant(self, id: &str, namespace: &str, level: Level) -> Self {
self.auth()
.grant(Grant::namespace(id, namespace, level))
.expect("grant");
self
}
pub fn corrupt_the_catalog(&self) {
self.corrupt(self.catalog().ref_name(), b"not a catalog");
}
pub fn corrupt_the_auth_table(&self) {
self.corrupt(self.auth().ref_name(), b"not an identity table");
}
}
pub fn plane_with_key(key: &PrivateKey, level: Access) -> Plane {
Plane::empty()
.with_the_repo()
.with_principal("principal-1")
.with_key("principal-1", key)
.with_grant("principal-1", level)
}
pub struct Fixture {
pub server: gunnar_ssh::BoundServer,
pub transport: Arc<BannerTransport>,
}
pub async fn start(control: Control, grace: Option<Duration>) -> Fixture {
start_with_transport(control, grace, BannerTransport::new()).await
}
pub async fn start_with_transport(
control: Control,
grace: Option<Duration>,
transport: BannerTransport,
) -> Fixture {
let mut config = SshConfig::ephemeral_loopback();
if let Some(grace) = grace {
config.exec_grace = Some(grace);
}
start_with_config(control, config, transport).await
}
pub async fn start_with_config(
control: Control,
config: SshConfig,
transport: BannerTransport,
) -> Fixture {
let transport = Arc::new(transport);
let server = GunnarSsh::new(config, control, transport.clone())
.bind(mint_key())
.await
.expect("bind an ephemeral loopback port");
Fixture { server, transport }
}
pub async fn start_with_key(key: &PrivateKey, level: Access) -> Fixture {
start(plane_with_key(key, level).control, None).await
}
pub async fn start_with_readiness(
key: &PrivateKey,
level: Access,
ready: Arc<std::sync::atomic::AtomicBool>,
) -> Fixture {
start_with_readiness_and_config(key, level, ready, SshConfig::ephemeral_loopback()).await
}
pub async fn start_with_readiness_and_config(
key: &PrivateKey,
level: Access,
ready: Arc<std::sync::atomic::AtomicBool>,
config: SshConfig,
) -> Fixture {
let transport = Arc::new(BannerTransport::new());
let server = GunnarSsh::new(
config,
plane_with_key(key, level).control,
transport.clone(),
)
.with_readiness(ready)
.bind(mint_key())
.await
.expect("bind an ephemeral loopback port");
Fixture { server, transport }
}
pub struct TrustingClient;
impl russh::client::Handler for TrustingClient {
type Error = russh::Error;
async fn check_server_key(
&mut self,
_server_public_key: &russh::keys::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
}
pub async fn authenticate(
addr: std::net::SocketAddr,
key: &PrivateKey,
user: &str,
) -> Result<Option<russh::client::Handle<TrustingClient>>, russh::Error> {
let config = Arc::new(russh::client::Config {
inactivity_timeout: Some(Duration::from_secs(15)),
..Default::default()
});
let mut session = russh::client::connect(config, addr, TrustingClient).await?;
let result = session
.authenticate_publickey(
user,
PrivateKeyWithHashAlg::new(Arc::new(key.clone()), None),
)
.await?;
Ok(result.success().then_some(session))
}
pub async fn offer_keys(
addr: std::net::SocketAddr,
keys: &[PrivateKey],
user: &str,
) -> (usize, bool, Option<russh::Error>) {
let config = Arc::new(russh::client::Config {
inactivity_timeout: Some(Duration::from_secs(15)),
..Default::default()
});
let mut session = match russh::client::connect(config, addr, TrustingClient).await {
Ok(session) => session,
Err(err) => return (0, false, Some(err)),
};
let mut answered = 0usize;
for key in keys {
match session
.authenticate_publickey(
user,
PrivateKeyWithHashAlg::new(Arc::new(key.clone()), None),
)
.await
{
Ok(result) => {
answered += 1;
if result.success() {
return (answered, true, None);
}
}
Err(err) => return (answered, false, Some(err)),
}
}
(answered, false, None)
}
#[derive(Debug, PartialEq, Eq)]
pub enum ExecOutcome {
Refused,
Accepted {
stdout: String,
exit_status: Option<u32>,
},
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum EnvOutcome {
Accepted,
Refused,
Unanswered,
}
pub async fn exec_with_env(
session: &russh::client::Handle<TrustingClient>,
variables: &[(&str, &str)],
command: &str,
) -> Result<(Vec<EnvOutcome>, ExecOutcome), russh::Error> {
let mut channel = session.channel_open_session().await?;
let mut env_outcomes = Vec::new();
for (name, value) in variables {
channel.set_env(true, *name, *value).await?;
let mut answer = EnvOutcome::Unanswered;
while let Some(msg) = channel.wait().await {
match msg {
russh::ChannelMsg::Success => {
answer = EnvOutcome::Accepted;
break;
}
russh::ChannelMsg::Failure => {
answer = EnvOutcome::Refused;
break;
}
_ => {}
}
}
env_outcomes.push(answer);
}
channel.exec(true, command).await?;
Ok((env_outcomes, drain(channel).await))
}
pub async fn exec(
session: &russh::client::Handle<TrustingClient>,
command: &str,
) -> Result<ExecOutcome, russh::Error> {
let channel = session.channel_open_session().await?;
channel.exec(true, command).await?;
Ok(drain(channel).await)
}
async fn drain(mut channel: russh::Channel<russh::client::Msg>) -> ExecOutcome {
let mut stdout = Vec::new();
let mut exit_status = None;
let mut accepted = None;
while let Some(msg) = channel.wait().await {
match msg {
russh::ChannelMsg::Success => accepted = Some(true),
russh::ChannelMsg::Failure => {
accepted = Some(false);
break;
}
russh::ChannelMsg::Data { ref data } => stdout.extend_from_slice(data),
russh::ChannelMsg::ExtendedData { ref data, .. } => stdout.extend_from_slice(data),
russh::ChannelMsg::ExitStatus { exit_status: code } => exit_status = Some(code),
_ => {}
}
}
match accepted {
Some(true) => ExecOutcome::Accepted {
stdout: String::from_utf8_lossy(&stdout).into_owned(),
exit_status,
},
_ => ExecOutcome::Refused,
}
}