use crate::caveats::{Scope, ScopeExt};
use crate::git_caveats::GitCaveats;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SshCaveats {
pub hosts: Scope<String>,
pub keys: Scope<String>,
}
impl SshCaveats {
#[must_use]
pub fn top() -> Self {
Self {
hosts: Scope::top(),
keys: Scope::top(),
}
}
#[must_use]
pub fn none() -> Self {
Self {
hosts: Scope::none(),
keys: Scope::none(),
}
}
#[must_use]
pub fn meet(&self, other: &Self) -> Self {
Self {
hosts: self.hosts.meet(&other.hosts),
keys: self.keys.meet(&other.keys),
}
}
#[must_use]
pub fn leq(&self, other: &Self) -> bool {
self.hosts.leq(&other.hosts) && self.keys.leq(&other.keys)
}
#[must_use]
pub fn permits_host(&self, host: &str) -> bool {
self.hosts.permits(&host.to_string())
}
#[must_use]
pub fn permits_key(&self, fp: &str) -> bool {
self.keys.permits(&fp.to_string())
}
}
impl Default for SshCaveats {
fn default() -> Self {
Self::none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitNetVerb {
Fetch,
Push,
Clone,
}
#[must_use]
pub fn git_over_ssh_permitted(
git: &GitCaveats,
ssh: &SshCaveats,
verb: GitNetVerb,
remote: &str,
host: &str,
) -> bool {
let git_ok = match verb {
GitNetVerb::Fetch => git.permits_fetch(remote),
GitNetVerb::Push => git.permits_push(remote),
GitNetVerb::Clone => git.permits_clone(),
};
git_ok && ssh.permits_host(host)
}
#[cfg(test)]
mod tests {
use super::*;
fn full_git() -> GitCaveats {
GitCaveats {
remote: Scope::only(["origin".to_string()]),
fetch: true,
push: true,
..GitCaveats::top()
}
}
#[test]
fn default_and_none_are_fail_closed() {
assert_eq!(SshCaveats::default(), SshCaveats::none());
let n = SshCaveats::none();
assert!(!n.permits_host("github.com"));
assert!(!n.permits_key("SHA256:abc"));
}
#[test]
fn top_permits_hosts_and_keys() {
let t = SshCaveats::top();
assert!(t.permits_host("github.com"));
assert!(t.permits_key("SHA256:abc"));
}
#[test]
fn host_scope_is_bounded() {
let s = SshCaveats {
hosts: Scope::only(["github.com".to_string()]),
keys: Scope::top(),
};
assert!(s.permits_host("github.com"));
assert!(!s.permits_host("evil.example.com"));
}
#[test]
fn meet_attenuates_and_leq_orders() {
let bounded = SshCaveats {
hosts: Scope::only(["github.com".to_string()]),
keys: Scope::none(),
};
let m = SshCaveats::top().meet(&bounded);
assert!(m.permits_host("github.com") && !m.permits_host("x"));
assert!(!m.permits_key("SHA256:abc"));
assert!(m.leq(&SshCaveats::top()));
assert!(SshCaveats::none().leq(&bounded));
}
#[test]
fn disabling_ssh_blocks_push_and_pull() {
let git = full_git();
let ssh = SshCaveats::none(); assert!(!git_over_ssh_permitted(
&git,
&ssh,
GitNetVerb::Fetch,
"origin",
"github.com"
));
assert!(!git_over_ssh_permitted(
&git,
&ssh,
GitNetVerb::Push,
"origin",
"github.com"
));
}
#[test]
fn allow_pull_block_push() {
let ssh = SshCaveats {
hosts: Scope::only(["github.com".to_string()]),
keys: Scope::top(),
};
let read_git = GitCaveats {
remote: Scope::only(["origin".to_string()]),
fetch: true,
push: false,
..GitCaveats::top()
};
assert!(
git_over_ssh_permitted(&read_git, &ssh, GitNetVerb::Fetch, "origin", "github.com"),
"fetch/pull is allowed"
);
assert!(
!git_over_ssh_permitted(&read_git, &ssh, GitNetVerb::Push, "origin", "github.com"),
"push is blocked"
);
}
#[test]
fn full_grant_allows_both() {
let ssh = SshCaveats {
hosts: Scope::only(["github.com".to_string()]),
keys: Scope::top(),
};
assert!(git_over_ssh_permitted(
&full_git(),
&ssh,
GitNetVerb::Fetch,
"origin",
"github.com"
));
assert!(git_over_ssh_permitted(
&full_git(),
&ssh,
GitNetVerb::Push,
"origin",
"github.com"
));
}
#[test]
fn host_must_be_in_scope_even_with_git_grant() {
let ssh = SshCaveats {
hosts: Scope::only(["github.com".to_string()]),
keys: Scope::top(),
};
assert!(!git_over_ssh_permitted(
&full_git(),
&ssh,
GitNetVerb::Push,
"origin",
"gitlab.com"
));
}
}