use crate::{plugin, ssh};
use std::collections::HashMap;
pub(super) type SshWindowKey = (String, String);
pub(super) struct SshWindow {
pub(super) opened: u64,
pub(super) host: String,
pub(super) name: String,
pub(super) private: Box<ssh_key::PrivateKey>,
}
pub(super) fn ssh_window_open<'a>(
windows: &'a HashMap<SshWindowKey, SshWindow>,
policy: &ssh::Windows,
approved: Option<&str>,
key: &SshWindowKey,
now: u64,
) -> Option<&'a SshWindow> {
if approved != Some(policy.hash.as_str()) {
return None;
}
let window = windows.get(key)?;
let secs = policy.hosts.get(&window.host)?;
(now < window.opened.saturating_add(*secs)).then_some(window)
}
pub(super) fn fingerprint(key: &ssh_key::PublicKey) -> String {
key.fingerprint(ssh_key::HashAlg::Sha256).to_string()
}
pub(super) type WindowKey = (String, String, String);
const MAX_WINDOWS_PER_SCOPE: usize = 256;
pub(super) struct Window {
pub(super) opened: u64,
pub(super) secret: Option<String>,
}
pub(super) fn window_open<'a>(
windows: &'a HashMap<WindowKey, Window>,
policy: &plugin::Windows,
approved: Option<&str>,
scope: &str,
tool: &str,
call: &str,
now: u64,
) -> Option<&'a Window> {
if approved != Some(policy.hash.as_str()) {
return None;
}
let secs = policy.tools.get(tool)?;
windows
.get(&(scope.to_string(), tool.to_string(), call.to_string()))
.filter(|w| now < w.opened.saturating_add(*secs))
}
pub(super) fn open_window(
windows: &mut HashMap<WindowKey, Window>,
key: WindowKey,
window: Window,
) {
let full = windows.keys().filter(|k| k.0 == key.0).count() >= MAX_WINDOWS_PER_SCOPE;
if full && !windows.contains_key(&key) {
let oldest = windows
.iter()
.filter(|(k, _)| k.0 == key.0)
.min_by_key(|(_, w)| w.opened)
.map(|(k, _)| k.clone());
if let Some(oldest) = oldest {
windows.remove(&oldest);
}
}
windows.insert(key, window);
}
#[cfg(test)]
mod tests {
use super::*;
use ssh_key::private::Ed25519Keypair;
#[test]
fn a_signing_window_covers_its_host_key_until_it_lapses_or_the_section_changes() {
let policy = ssh::parse_windows("[ssh.windows]\nlegion = \"1h\"\n").unwrap();
let pair = Ed25519Keypair::from_seed(&[3; 32]);
let private = ssh_key::PrivateKey::new(pair.into(), "").unwrap();
let mut windows = HashMap::new();
let key = ("key".to_string(), "SHA256:legion".to_string());
windows.insert(
key.clone(),
SshWindow {
opened: 1_000,
host: "legion".into(),
name: "ssh/legion".into(),
private: Box::new(private),
},
);
let hash = Some(policy.hash.as_str());
let open = |k: &SshWindowKey, approved, now| {
ssh_window_open(&windows, &policy, approved, k, now).is_some()
};
assert!(open(&key, hash, 1_000 + 3_599));
assert!(!open(&key, hash, 1_000 + 3_600));
assert!(!open(&("key".into(), "SHA256:evil".into()), hash, 1_001));
assert!(!open(&key, None, 1_001));
assert!(!open(&key, Some("an older section"), 1_001));
let dropped = ssh::parse_windows("[ssh.windows]\nm1 = \"1h\"\n").unwrap();
assert!(ssh_window_open(&windows, &dropped, Some(&dropped.hash), &key, 1_001).is_none());
let shorter = ssh::parse_windows("[ssh.windows]\nlegion = \"10m\"\n").unwrap();
assert!(ssh_window_open(&windows, &shorter, Some(&shorter.hash), &key, 1_599).is_some());
assert!(ssh_window_open(&windows, &shorter, Some(&shorter.hash), &key, 1_600).is_none());
}
fn policy(tool: &str, secs: u64) -> plugin::Windows {
plugin::Windows {
tools: [(tool.to_string(), secs)].into(),
hash: "approved".into(),
}
}
fn opened(scope: &str, tool: &str, at: u64) -> HashMap<WindowKey, Window> {
HashMap::from([(
(scope.to_string(), tool.to_string(), "call".to_string()),
Window {
opened: at,
secret: None,
},
)])
}
#[test]
fn a_window_covers_its_tool_until_it_ends() {
let windows = opened("local", "things3.logbook", 1_000);
let full = policy("things3.logbook", 60);
let open = |policy: &plugin::Windows, tool: &str, now| {
window_open(
&windows,
policy,
Some("approved"),
"local",
tool,
"call",
now,
)
.is_some()
};
assert!(open(&full, "things3.logbook", 1_000));
assert!(open(&full, "things3.logbook", 1_059));
assert!(!open(&full, "things3.logbook", 1_060), "an ended window");
assert!(!open(&full, "things3.today", 1_001), "another tool");
assert!(!open(
&policy("things3.logbook", 10),
"things3.logbook",
1_020
));
}
#[test]
fn a_window_covers_only_the_exact_call_that_opened_it() {
let windows = opened("local", "imap.search", 1_000);
let policy = policy("imap.search", 3_600);
let open = |call: &str| {
window_open(
&windows,
&policy,
Some("approved"),
"local",
"imap.search",
call,
1_001,
)
.is_some()
};
assert!(open("call"));
assert!(!open("another call"));
}
#[test]
fn a_window_covers_only_the_scope_that_opened_it() {
let a = "server (someone@example.com, 100.64.0.7)";
let b = "laptop (someone@example.com, 100.64.0.8)";
let policy = policy("imap.search", 3_600);
let open = |windows: &HashMap<WindowKey, Window>, scope: &str| {
window_open(
windows,
&policy,
Some("approved"),
scope,
"imap.search",
"call",
1_001,
)
.is_some()
};
let by_host = opened(a, "imap.search", 1_000);
assert!(open(&by_host, a));
assert!(!open(&by_host, b), "another tailnet host");
assert!(!open(&by_host, "local"), "a local call");
let by_local = opened("local", "imap.search", 1_000);
assert!(open(&by_local, "local"));
assert!(!open(&by_local, a), "a tailnet host");
}
#[test]
fn a_full_scope_drops_its_oldest_window_and_leaves_other_scopes() {
let key =
|scope: &str, n: usize| (scope.to_string(), "imap.read".to_string(), n.to_string());
let window = |opened| Window {
opened,
secret: None,
};
let mut windows = HashMap::new();
open_window(&mut windows, key("other", 0), window(1));
for n in 0..MAX_WINDOWS_PER_SCOPE {
open_window(
&mut windows,
key("local", n),
window(1_000 + u64::try_from(n).unwrap()),
);
}
assert_eq!(windows.len(), MAX_WINDOWS_PER_SCOPE + 1);
open_window(&mut windows, key("local", 5), window(5_000));
assert_eq!(windows.len(), MAX_WINDOWS_PER_SCOPE + 1);
open_window(&mut windows, key("local", 999), window(9_000));
assert_eq!(windows.len(), MAX_WINDOWS_PER_SCOPE + 1);
assert!(!windows.contains_key(&key("local", 0)), "the oldest went");
assert!(windows.contains_key(&key("local", 1)));
assert!(windows.contains_key(&key("local", 999)));
assert!(
windows.contains_key(&key("other", 0)),
"another scope keeps its window"
);
}
#[test]
fn no_window_applies_from_a_section_that_was_not_approved() {
let windows = opened("local", "imap.search", 1_000);
let policy = policy("imap.search", 3_600);
for approved in [None, Some("an older section")] {
let found = window_open(
&windows,
&policy,
approved,
"local",
"imap.search",
"call",
1_001,
);
assert!(found.is_none(), "{approved:?}");
}
let emptied = plugin::Windows {
tools: std::collections::BTreeMap::default(),
hash: "approved".into(),
};
let found = window_open(
&windows,
&emptied,
Some("approved"),
"local",
"imap.search",
"call",
1_001,
);
assert!(found.is_none());
}
}