use std::sync::Arc;
use gunnar_grpc::{
Caller, ControlPlane, EventBus, MemoryControlPlane, MirrorService, ReposService, ServerConfig,
ServiceContext,
};
use gunnar_proto::mirror_server::Mirror as _;
use gunnar_proto::repos_server::Repos as _;
use gunnar_proto::*;
use tonic::{Code, Request};
const ACCOUNT: &str = "acme";
const REPO: &str = "vault";
struct Rig {
plane: Arc<MemoryControlPlane>,
repos: ReposService,
bus: Arc<EventBus>,
}
fn caller(id: &str) -> Caller {
Caller {
principal_id: id.to_owned(),
fingerprint: format!("SHA256:{id}"),
machine: false,
authenticated: true,
}
}
impl Rig {
fn new() -> Rig {
let plane = Arc::new(MemoryControlPlane::with_repo(ACCOUNT, REPO));
plane
.add_principal(ACCOUNT, ACCOUNT, PrincipalKind::User)
.expect("the account's owner");
let control: Arc<dyn ControlPlane> = plane.clone();
let bus = Arc::new(EventBus::new(16));
let ctx = ServiceContext::new(control, Arc::clone(&bus), ServerConfig::default());
Rig {
repos: ReposService::new(ctx),
plane,
bus,
}
}
fn principal(&self, id: &str, scope_repo: &str, level: GrantLevel) -> Caller {
self.plane
.add_principal(id, id, PrincipalKind::User)
.expect("principal");
self.plane
.grant(id, ACCOUNT, scope_repo, level)
.expect("grant");
caller(id)
}
async fn set(&self, who: &Caller, encrypted: bool) -> Result<Repo, tonic::Status> {
let mut request = Request::new(SetEncryptedRequest {
path: Some(RepoPath::new(ACCOUNT, REPO)),
encrypted,
});
request.extensions_mut().insert(who.clone());
self.repos
.set_encrypted(request)
.await
.map(|r| r.into_inner().repo.expect("a repo comes back"))
}
fn encrypted(&self) -> bool {
self.plane
.get_repo(&RepoPath::new(ACCOUNT, REPO))
.expect("the repo")
.encrypted
}
}
#[tokio::test]
async fn a_new_repository_is_not_encrypted() {
assert!(!Rig::new().encrypted());
}
#[tokio::test]
async fn the_account_admin_turns_encryption_on_and_off() {
let rig = Rig::new();
let on = rig
.set(&caller(ACCOUNT), true)
.await
.expect("the owner turns it on");
assert!(on.encrypted);
assert!(rig.encrypted());
let carol = rig.principal("carol", "", GrantLevel::Admin);
let off = rig
.set(&carol, false)
.await
.expect("a namespace admin turns it off");
assert!(!off.encrypted);
assert!(!rig.encrypted());
}
#[tokio::test]
async fn a_repository_admin_or_a_writer_may_not() {
let rig = Rig::new();
let bob = rig.principal("bob", REPO, GrantLevel::Admin);
let refused = rig
.set(&bob, true)
.await
.expect_err("a repo admin is not the account admin");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
assert!(
refused.message().contains("account admin"),
"the refusal says whose decision it is: {}",
refused.message()
);
let dave = rig.principal("dave", "", GrantLevel::Write);
assert_eq!(
rig.set(&dave, true)
.await
.expect_err("write is not admin")
.code(),
Code::PermissionDenied
);
assert!(!rig.encrypted(), "a refusal wrote nothing");
}
fn mirrors(rig: &Rig) -> MirrorService {
let control: Arc<dyn ControlPlane> = rig.plane.clone();
MirrorService::new(ServiceContext::new(
control,
Arc::new(EventBus::new(16)),
ServerConfig::default(),
))
}
async fn add_mirror(
svc: &MirrorService,
who: &Caller,
adopt: bool,
) -> Result<MirrorConfig, tonic::Status> {
let mut request = Request::new(AddMirrorRequest {
repo: Some(RepoPath::new(ACCOUNT, REPO)),
remote_url: "https://example.org/upstream.git".into(),
prune: false,
create_repo: false,
adopt,
});
request.extensions_mut().insert(who.clone());
svc.add(request)
.await
.map(|r| r.into_inner().mirror.expect("a mirror comes back"))
}
async fn sync_mirror(
svc: &MirrorService,
who: &Caller,
) -> Result<SyncMirrorResponse, tonic::Status> {
let mut request = Request::new(SyncMirrorRequest {
repo: Some(RepoPath::new(ACCOUNT, REPO)),
});
request.extensions_mut().insert(who.clone());
svc.sync(request).await.map(|r| r.into_inner())
}
#[tokio::test]
async fn an_encrypted_repository_is_never_made_a_mirror() {
let rig = Rig::new();
let svc = mirrors(&rig);
rig.set(&caller(ACCOUNT), true)
.await
.expect("the owner turns it on");
let bob = rig.principal("bob", REPO, GrantLevel::Admin);
for (who, adopt) in [(caller(ACCOUNT), false), (bob.clone(), false), (bob, true)] {
let refused = add_mirror(&svc, &who, adopt)
.await
.expect_err("a mirror's pack is an upstream's plaintext");
assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
assert!(
refused.message().contains("encrypted") && !refused.message().contains("Level 1"),
"the refusal says why, in plain words: {}",
refused.message()
);
}
assert!(
rig.plane
.mirror_status(None, Some(&RepoPath::new(ACCOUNT, REPO)))
.expect("status")
.is_empty(),
"no mirror was registered"
);
}
#[tokio::test]
async fn a_mirror_is_never_turned_encrypted() {
let rig = Rig::new();
let svc = mirrors(&rig);
add_mirror(&svc, &caller(ACCOUNT), false)
.await
.expect("a plain repository may be a mirror");
let refused = rig
.set(&caller(ACCOUNT), true)
.await
.expect_err("a mirror's content is the upstream's, never sealed by its members");
assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
assert!(
refused.message().contains("mirror"),
"{}",
refused.message()
);
assert!(!rig.encrypted(), "a refusal wrote nothing");
rig.set(&caller(ACCOUNT), false)
.await
.expect("off is always allowed");
}
#[tokio::test]
async fn a_mirror_made_encrypted_before_the_rule_is_never_synced() {
let rig = Rig::new();
let svc = mirrors(&rig);
add_mirror(&svc, &caller(ACCOUNT), false)
.await
.expect("registered");
rig.plane
.set_encrypted(&RepoPath::new(ACCOUNT, REPO), true)
.expect("the flag, set below the door");
let refused = sync_mirror(&svc, &caller(ACCOUNT))
.await
.expect_err("an encrypted repository is never fetched into");
assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
assert!(
refused.message().contains("encrypted"),
"{}",
refused.message()
);
}
#[tokio::test]
async fn turning_encryption_on_or_off_is_an_event() {
use tokio_stream::StreamExt as _;
let rig = Rig::new();
let mut stream = rig.bus.subscribe(gunnar_grpc::EventFilter::default());
rig.set(&caller(ACCOUNT), true).await.expect("on");
rig.set(&caller(ACCOUNT), true)
.await
.expect("on again: no change");
rig.set(&caller(ACCOUNT), false).await.expect("off");
let mut seen = Vec::new();
while let Ok(Some(Ok(event))) =
tokio::time::timeout(std::time::Duration::from_millis(500), stream.next()).await
{
if let Some(event::Payload::EncryptedChanged(e)) = &event.payload {
assert_eq!(event.kind, EventKind::EncryptedChanged as i32);
assert_eq!(e.repo, Some(RepoPath::new(ACCOUNT, REPO)));
assert_eq!(e.principal_id, ACCOUNT);
seen.push((e.from, e.to));
}
}
assert_eq!(
seen,
vec![(false, true), (true, false)],
"on and off, each once"
);
}