use std::sync::Arc;
use gunnar_grpc::auth::PersonSession;
use gunnar_grpc::{
AccessService, AccountsService, Caller, ClansService, ControlPlane, EnterpriseService,
EventBus, ReposService, ServerConfig, ServiceContext,
};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::accounts_server::Accounts as _;
use gunnar_proto::clans_server::Clans as _;
use gunnar_proto::enterprise_server::Enterprise as _;
use gunnar_proto::repos_server::Repos as _;
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use p256::ecdsa::signature::Signer as _;
use tonic::{Code, Request};
const RP_ID: &str = "console.example";
const ORIGIN: &str = "https://console.example";
fn person(id: &str) -> Caller {
Caller {
principal_id: id.to_owned(),
fingerprint: format!("SHA256:{id}"),
machine: false,
authenticated: true,
}
}
fn machine(id: &str) -> Caller {
Caller {
machine: true,
..person(id)
}
}
fn authed<T>(caller: &Caller, body: T) -> Request<T> {
let mut r = Request::new(body);
r.extensions_mut().insert(caller.clone());
r
}
fn unhex(text: &str) -> Vec<u8> {
(0..text.len())
.step_by(2)
.map(|i| u8::from_str_radix(&text[i..i + 2], 16).unwrap())
.collect()
}
fn unb64(text: &str) -> Vec<u8> {
gunnar_grpc::webauthn::from_b64url(text).expect("base64url")
}
fn stepped<T>(caller: &Caller, body: T) -> Request<T> {
let mut r = authed(caller, body);
r.extensions_mut().insert(PersonSession {
stepped_up: true,
stepped_up_at_unix_ms: None,
});
r
}
fn sha256(bytes: &[u8]) -> Vec<u8> {
unhex(&gunnar_grpc::webauthn::sha256_hex(bytes))
}
struct Passkey {
key: p256::ecdsa::SigningKey,
id: Vec<u8>,
counter: std::cell::Cell<u32>,
}
impl Passkey {
fn new(seed: u8, id: &str) -> Passkey {
Passkey {
key: p256::ecdsa::SigningKey::from_slice(&[seed; 32]).expect("a scalar"),
id: id.as_bytes().to_vec(),
counter: std::cell::Cell::new(0),
}
}
fn public(&self) -> Vec<u8> {
self.key
.verifying_key()
.to_sec1_point(false)
.as_bytes()
.to_vec()
}
fn get(&self, challenge: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
self.counter.set(self.counter.get() + 1);
let client_data = format!(
r#"{{"type":"webauthn.get","challenge":"{}","origin":"{ORIGIN}","crossOrigin":false}}"#,
gunnar_grpc::webauthn::b64url(challenge)
)
.into_bytes();
let mut auth_data = sha256(RP_ID.as_bytes());
auth_data.push(0x01 | 0x04);
auth_data.extend_from_slice(&self.counter.get().to_be_bytes());
let mut message = auth_data.clone();
message.extend_from_slice(&sha256(&client_data));
let signature: p256::ecdsa::Signature = self.key.sign(&message);
(
client_data,
auth_data,
signature.to_der().as_bytes().to_vec(),
)
}
}
struct World {
_dir: tempfile::TempDir,
stores: Arc<ServerStores>,
plane: Arc<CoreControlPlane>,
ctx: ServiceContext,
identity: Arc<gunnar_grpc::identity::GunnarIdentity>,
relay: Caller,
op: Caller,
acme: Caller,
bob: Caller,
acme_key: Passkey,
bob_key: Passkey,
}
fn world() -> World {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
let control: Arc<dyn ControlPlane> = plane.clone();
let identity = gunnar_server::identity::build(
dir.path(),
Arc::clone(&control),
&ServerConfig::default(),
false,
);
let ctx = ServiceContext::new(
control,
Arc::new(EventBus::new(64)),
ServerConfig::default(),
)
.with_identity(Arc::clone(&identity));
plane
.add_principal("gunnar-ui", "gunnar-ui", PrincipalKind::Machine)
.expect("relay");
plane
.grant("gunnar-ui", "", "", GrantLevel::Admin)
.expect("the relay is a server admin");
plane
.add_principal("op", "op", PrincipalKind::User)
.expect("the operator");
plane
.grant("op", "", "", GrantLevel::Admin)
.expect("the operator is a server admin");
plane
.add_principal("acme", "acme", PrincipalKind::User)
.expect("a customer");
let w = World {
_dir: dir,
stores,
plane,
ctx,
identity,
relay: machine("gunnar-ui"),
op: person("op"),
acme: person("acme"),
bob: person("bob"),
acme_key: Passkey::new(11, "acme-yubikey"),
bob_key: Passkey::new(12, "bob-yubikey"),
};
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
w.create(&w.op, "op", "notes", Visibility::Private)
.await
.expect("the operator's own repository");
w.repos()
.create_namespace(authed(
&w.op,
CreateNamespaceRequest {
name: "nordisk".into(),
description: String::new(),
},
))
.await
.expect("the operator's forge namespace");
w.create(&w.op, "nordisk", "gunnar", Visibility::Private)
.await
.expect("nordisk/gunnar");
w.create(&w.acme, "acme", "site", Visibility::Private)
.await
.expect("acme/site");
w.create(&w.acme, "acme", "www", Visibility::Public)
.await
.expect("acme/www");
w.accounts()
.enroll_employee(authed(
&w.acme,
EnrollEmployeeRequest {
account: "acme".into(),
id: "bob".into(),
name: "Bob".into(),
..Default::default()
},
))
.await
.expect("acme enrolls bob");
});
w.plane
.add_credential(
"acme",
&w.acme_key.id,
&w.acme_key.public(),
"acme's YubiKey",
0,
)
.expect("acme's passkey");
w.plane
.add_credential(
"bob",
&w.bob_key.id,
&w.bob_key.public(),
"bob's YubiKey",
0,
)
.expect("bob's passkey");
w
}
impl World {
fn repos(&self) -> ReposService {
ReposService::new(self.ctx.clone())
}
fn accounts(&self) -> AccountsService {
AccountsService::new(self.ctx.clone())
}
fn access(&self) -> AccessService {
AccessService::new(self.ctx.clone())
}
fn enterprise(&self) -> EnterpriseService {
EnterpriseService::new(self.ctx.clone())
}
fn clans(&self) -> ClansService {
ClansService::new(self.ctx.clone())
}
async fn log_of(&self, who: &Caller, account: &str) -> Result<AuditLogResponse, tonic::Status> {
self.enterprise()
.audit_log(authed(
who,
AuditLogRequest {
account: account.into(),
filter: None,
page_size: 200,
before_seq: None,
},
))
.await
.map(|r| r.into_inner())
}
async fn create(
&self,
who: &Caller,
ns: &str,
repo: &str,
visibility: Visibility,
) -> Result<Repo, tonic::Status> {
self.repos()
.create(authed(
who,
CreateRepoRequest {
path: Some(RepoPath::new(ns, repo)),
visibility: visibility as i32,
..Default::default()
},
))
.await
.map(|r| r.into_inner().repo.expect("a repo"))
}
async fn challenge(&self) -> Vec<u8> {
self.access()
.machine_challenge(authed(&self.relay, MachineChallengeRequest::default()))
.await
.expect("the relay gets a challenge")
.into_inner()
.challenge
}
fn login(&self, key: &Passkey) -> Result<nornir_identity::LoggedIn, nornir_identity::Refused> {
let c = self.identity.login_start(&nornir_identity::Relay {
rp_id: RP_ID.into(),
origin: ORIGIN.into(),
client: None,
})?;
let challenge = unb64(&c.challenge);
let (client_data_json, authenticator_data, signature) = key.get(&challenge);
self.identity.login_finish(nornir_identity::Assertion {
ceremony_id: c.id,
credential_id: key.id.clone(),
client_data_json,
authenticator_data,
signature,
rp_id: RP_ID.into(),
origin: ORIGIN.into(),
})
}
async fn log(&self, who: &Caller, kinds: &[&str]) -> Result<AuditLogResponse, tonic::Status> {
self.enterprise()
.audit_log(authed(
who,
AuditLogRequest {
account: "acme".into(),
filter: Some(AuditFilter {
kinds: kinds.iter().map(|k| k.to_string()).collect(),
..Default::default()
}),
page_size: 200,
before_seq: None,
},
))
.await
.map(|r| r.into_inner())
}
async fn listed(&self, who: &Caller, namespace: &str) -> Vec<String> {
let mut names: Vec<String> = self
.repos()
.list(authed(
who,
ListReposRequest {
namespace: namespace.into(),
..Default::default()
},
))
.await
.expect("a listing")
.into_inner()
.repos
.into_iter()
.filter_map(|r| r.path.map(|p| format!("{}/{}", p.namespace, p.repo)))
.collect();
names.sort();
names
}
async fn get(&self, who: &Caller, ns: &str, repo: &str) -> Result<Repo, tonic::Status> {
self.repos()
.get(authed(
who,
GetRepoRequest {
path: Some(RepoPath::new(ns, repo)),
},
))
.await
.map(|r| r.into_inner().repo.expect("a repo"))
}
}
fn kinds(log: &AuditLogResponse) -> Vec<String> {
log.entries
.iter()
.map(|e| format!("{}:{}:{}", e.kind, e.actor_role, e.outcome))
.collect()
}
#[tokio::test(flavor = "multi_thread")]
async fn the_account_admin_reads_every_security_event_and_nobody_else_does() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
w.login(&w.bob_key).expect("bob logs in");
w.bob_key.counter.set(0);
assert!(
w.login(&w.bob_key).is_err(),
"a counter that went back is refused"
);
let acme = w
.identity
.vouched_session("acme", true)
.expect("acme's session")
.token;
w.identity
.invite(
&acme,
nornir_identity::InviteAsk {
person: Some("bob".into()),
..Default::default()
},
)
.expect("acme invites bob to another device");
let refused = w
.repos()
.set_visibility(authed(
&w.bob,
SetVisibilityRequest {
path: Some(RepoPath::new("acme", "site")),
visibility: Visibility::Public as i32,
},
))
.await
.expect_err("a member may not make a repository public");
assert_eq!(refused.code(), Code::PermissionDenied);
w.repos()
.set_visibility(authed(
&w.acme,
SetVisibilityRequest {
path: Some(RepoPath::new("acme", "site")),
visibility: Visibility::Public as i32,
},
))
.await
.expect("the account admin may");
let log = w.log(&w.acme, &[]).await.expect("the admin reads the log");
let seen = kinds(&log);
for want in [
"repo.created:admin:ok",
"member.added:admin:ok",
"login:member:ok",
"invite.created:admin:ok",
"visibility.changed:member:refused",
"visibility.changed:admin:ok",
] {
assert!(
seen.iter().any(|s| s == want),
"{want} is not in the log: {seen:#?}"
);
}
assert!(
seen.iter().any(|s| s == "login:member:refused"),
"the refused login is not in the log: {seen:#?}"
);
let refused = log
.entries
.iter()
.find(|e| e.kind == "login" && e.outcome == "refused")
.unwrap();
assert_eq!(
(refused.actor.as_str(), refused.target.as_str()),
("bob", "passkey"),
"{refused:?}"
);
assert!(
!seen.iter().any(|s| s.starts_with("repo.created:operator")),
"the operator's own repositories are not acme's: {seen:#?}"
);
assert_eq!(
log.entries.first().map(|e| e.kind.as_str()),
Some("visibility.changed"),
"newest first"
);
let chain = log
.chain
.as_ref()
.expect("the first page says whether the chain holds");
assert!(chain.holds, "{chain:?}");
assert_eq!(chain.tip_hash, log.entries[0].hash);
assert_eq!(log.retention_days, 365, "a year by default");
let made_public = log
.entries
.iter()
.find(|e| e.kind == "visibility.changed" && e.outcome == "ok")
.unwrap();
assert_eq!(made_public.detail, "private → public");
assert_eq!(made_public.target, "acme/site");
let vis = w.log(&w.acme, &["visibility"]).await.unwrap();
assert_eq!(vis.entries.len(), 2, "{:#?}", kinds(&vis));
for who in [&w.bob, &w.op] {
let err = w
.log(who, &[])
.await
.expect_err("only the account's admins");
assert_eq!(
err.code(),
Code::PermissionDenied,
"{}: {err:?}",
who.principal_id
);
assert!(err.message().contains("account admin"), "{}", err.message());
}
let csv = w
.enterprise()
.export_audit_log(authed(
&w.acme,
ExportAuditLogRequest {
account: "acme".into(),
format: AuditExportFormat::Csv as i32,
..Default::default()
},
))
.await
.expect("a CSV")
.into_inner();
let text = String::from_utf8(csv.body).unwrap();
assert!(
text.starts_with("seq,at_utc,kind,actor,actor_role,target,detail,outcome,hash\n"),
"{text}"
);
assert_eq!(csv.entries as usize, log.entries.len());
assert!(
csv.file_name.starts_with("audit-acme-") && csv.file_name.ends_with(".csv"),
"{}",
csv.file_name
);
let json = w
.enterprise()
.export_audit_log(authed(
&w.acme,
ExportAuditLogRequest {
account: "acme".into(),
format: AuditExportFormat::Json as i32,
..Default::default()
},
))
.await
.expect("JSON")
.into_inner();
assert_eq!(json.content_type, "application/json");
let after = w.log(&w.acme, &["audit"]).await.unwrap();
assert_eq!(
after.entries.len(),
2,
"both exports are entries: {:#?}",
kinds(&after)
);
let err = w
.enterprise()
.export_audit_log(authed(
&w.op,
ExportAuditLogRequest {
account: "acme".into(),
..Default::default()
},
))
.await
.expect_err("the operator exports nothing");
assert_eq!(err.code(), Code::PermissionDenied);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_operator_principal_gets_aggregates_only() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
assert_eq!(
w.listed(&w.op, "").await,
vec!["acme/www", "nordisk/gunnar", "op/notes"],
"the operator's listing: its own, the forge's, and acme's PUBLIC repository only"
);
assert!(
w.listed(&w.acme, "")
.await
.contains(&"acme/site".to_owned()),
"acme sees its own"
);
assert_eq!(
w.listed(&w.op, "acme").await,
vec!["acme/www"],
"inside acme: the public subset"
);
let err = w
.get(&w.op, "acme", "site")
.await
.expect_err("a private repository by name");
assert_eq!(err.code(), Code::PermissionDenied);
w.get(&w.op, "nordisk", "gunnar")
.await
.expect("the operator's own forge is the operator's");
let principals: Vec<String> = w
.access()
.list_principals(authed(&w.op, ListPrincipalsRequest::default()))
.await
.expect("the operator lists principals")
.into_inner()
.principals
.into_iter()
.map(|p| p.id)
.collect();
assert!(
principals.contains(&"acme".to_owned()),
"an account's own name is how the operator addresses it"
);
assert!(
!principals.contains(&"bob".to_owned()),
"a member's name is not the operator's: {principals:?}"
);
let creds = w
.access()
.list_credentials(authed(&w.op, ListCredentialsRequest::default()))
.await
.expect("credentials")
.into_inner()
.credentials;
assert!(
creds
.iter()
.all(|c| c.principal_id != "bob" && !c.label.contains("bob")),
"a member's passkey names nobody: {creds:?}"
);
assert!(
creds.iter().any(|c| c.principal_id == "acme"),
"the account's own person is how it is recovered: {creds:?}"
);
assert_eq!(
creds.len(),
2,
"every passkey is still offered at login, a member's by id alone: {creds:?}"
);
let err = w
.accounts()
.list_employees(authed(
&w.op,
ListEmployeesRequest {
account: "acme".into(),
},
))
.await
.expect_err("the members of acme");
assert_eq!(err.code(), Code::PermissionDenied);
let err = w
.repos()
.set_visibility(authed(
&w.op,
SetVisibilityRequest {
path: Some(RepoPath::new("acme", "site")),
visibility: Visibility::Public as i32,
},
))
.await
.expect_err("only the account admin makes things public — the operator neither");
assert_eq!(err.code(), Code::PermissionDenied);
assert!(err.message().contains("account admin"), "{}", err.message());
let err = w
.create(&w.op, "acme", "planted", Visibility::Private)
.await
.expect_err("nor creates inside");
assert_eq!(err.code(), Code::PermissionDenied);
let totals = w
.enterprise()
.aggregates(authed(&w.op, AggregatesRequest {}))
.await
.expect("the operator's totals")
.into_inner();
assert_eq!(
totals.accounts, 2,
"acme and the operator's own: {totals:?}"
);
assert_eq!(totals.enterprise_accounts, 1);
assert_eq!(totals.members, 1);
assert_eq!(
totals.repos, 4,
"the whole estate: acme's two, op/notes and the forge's nordisk/gunnar: {totals:?}"
);
assert_eq!(totals.public_repos, 1);
assert_eq!(
totals.operator_repos, 1,
"nordisk/gunnar, the operator's share of the four"
);
assert_eq!(
totals.health == "ok",
totals.health_alarms == 0,
"{totals:?}"
);
let err = w
.enterprise()
.aggregates(authed(&w.bob, AggregatesRequest {}))
.await
.expect_err("a member is no operator");
assert_eq!(err.code(), Code::PermissionDenied);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_totals_count_the_whole_estate_the_operators_own_namespaces_too() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
w.create(&w.op, "nordisk", "site", Visibility::Public)
.await
.expect("a public repository in the operator's forge");
let path = gunnar_core::RepoRef::new("nordisk", "gunnar").expect("legal");
let stored = tokio::task::spawn_blocking({
let (stores, plane) = (Arc::clone(&w.stores), Arc::clone(&w.plane));
move || {
let repo = stores
.catalog()
.get(&path)
.expect("read")
.expect("nordisk/gunnar exists");
let handle = stores.repo_store(&repo).expect("open");
let mut pb = gunnar_store::conformance::pack::PackBuilder::new(handle.hash_kind());
pb.object(gix_object::Kind::Blob, vec![b'n'; 256 << 10]);
handle
.serve()
.put_pack(&pb.build().expect("pack").bytes)
.expect("put_pack");
drop(handle);
stores.close_repo(&path);
let read = || {
let row = &plane.usage(Some("nordisk")).expect("usage")[0];
row.pack_bytes.saturating_add(row.lfs_bytes)
};
let (mut last, mut same) = (read(), 0);
for _ in 0..100 {
std::thread::sleep(std::time::Duration::from_millis(300));
let now = read();
same = if now == last { same + 1 } else { 0 };
last = now;
if same >= 3 {
break;
}
}
last
}
})
.await
.unwrap();
assert!(stored > 0, "the pack is measured in nordisk");
let totals = w
.enterprise()
.aggregates(authed(&w.op, AggregatesRequest {}))
.await
.expect("the operator's totals")
.into_inner();
assert_eq!(
totals.repos, 5,
"acme's two, op/notes, nordisk/gunnar and nordisk/site: {totals:?}"
);
assert_eq!(
totals.public_repos, 2,
"acme/www and nordisk/site: {totals:?}"
);
assert_eq!(
totals.operator_repos, 2,
"nordisk's two, already inside the five: {totals:?}"
);
assert!(
totals.stored_bytes >= stored,
"the storage used leaves the operator's forge out: {} < {stored} in nordisk alone",
totals.stored_bytes
);
let everything: u64 = w
.plane
.usage(None)
.expect("every namespace's usage")
.iter()
.map(|r| r.pack_bytes.saturating_add(r.lfs_bytes))
.sum();
assert_eq!(
totals.stored_bytes, everything,
"the storage used is every namespace's, pack and LFS"
);
assert_eq!(
totals.accounts, 2,
"accounts are still people: acme and the operator's own, never the forge"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_account_admin_approves_a_look_with_a_step_up_and_ends_it() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let asked = w
.enterprise()
.request_operator_access(authed(
&w.op,
RequestOperatorAccessRequest {
account: "acme".into(),
reason: "ticket 4711: a push hangs".into(),
minutes: 30,
},
))
.await
.expect("the operator asks")
.into_inner()
.request
.unwrap();
assert_eq!(asked.state, OperatorAccessState::Pending as i32);
assert!(
w.get(&w.op, "acme", "site").await.is_err(),
"asking is not having"
);
let err = w
.enterprise()
.request_operator_access(authed(
&w.bob,
RequestOperatorAccessRequest {
account: "acme".into(),
reason: "x".into(),
minutes: 30,
},
))
.await
.expect_err("only an operator asks");
assert_eq!(err.code(), Code::PermissionDenied);
let pending = w
.enterprise()
.list_operator_access(authed(
&w.acme,
ListOperatorAccessRequest {
account: "acme".into(),
},
))
.await
.expect("acme sees the request")
.into_inner()
.requests;
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].reason, "ticket 4711: a push hangs");
let approve = |who: &Caller, code: &str, fresh: bool| {
let body = ApproveOperatorAccessRequest {
request_id: asked.request_id.clone(),
code: code.into(),
minutes: 0,
};
if fresh {
stepped(who, body)
} else {
authed(who, body)
}
};
let err = w
.enterprise()
.approve_operator_access(approve(&w.acme, &asked.code, false))
.await
.expect_err("no step-up");
assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
assert!(err.message().contains("[stepup]"), "{err:?}");
let err = w
.enterprise()
.approve_operator_access(approve(&w.acme, "XXX-000", true))
.await
.expect_err("the wrong code");
assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
let err = w
.enterprise()
.approve_operator_access(approve(&w.bob, &asked.code, true))
.await
.expect_err("a member decides nothing");
assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
let err = w
.enterprise()
.approve_operator_access(approve(&w.op, &asked.code, true))
.await
.expect_err("nor the operator itself");
assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
let approved = w
.enterprise()
.approve_operator_access(approve(&w.acme, &asked.code, true))
.await
.expect("acme approves")
.into_inner()
.request
.unwrap();
assert_eq!(approved.state, OperatorAccessState::Approved as i32);
assert!(approved.granted_until_unix_ms > gunnar_core::now_ms() + 25 * 60_000);
let err = w
.enterprise()
.approve_operator_access(approve(&w.acme, &asked.code, true))
.await
.expect_err("decided");
assert_ne!(err.code(), Code::Ok);
w.get(&w.op, "acme", "site")
.await
.expect("the approved look reads");
assert!(w.listed(&w.op, "").await.contains(&"acme/site".to_owned()));
let err = w
.repos()
.set_visibility(authed(
&w.op,
SetVisibilityRequest {
path: Some(RepoPath::new("acme", "site")),
visibility: Visibility::Public as i32,
},
))
.await
.expect_err("a look writes nothing");
assert_eq!(err.code(), Code::PermissionDenied);
assert_eq!(
w.log(&w.op, &[])
.await
.expect_err("the log stays the admins'")
.code(),
Code::PermissionDenied
);
let ended = w
.enterprise()
.revoke_operator_access(authed(
&w.acme,
RevokeOperatorAccessRequest {
request_id: asked.request_id.clone(),
},
))
.await
.expect("acme ends it")
.into_inner()
.request
.unwrap();
assert_eq!(ended.state, OperatorAccessState::Revoked as i32);
assert!(w.get(&w.op, "acme", "site").await.is_err(), "ended");
let log = w.log(&w.acme, &["operator_access"]).await.unwrap();
assert_eq!(
kinds(&log),
vec![
"operator_access.revoked:admin:ok",
"operator_access.approved:admin:ok",
"operator_access.requested:operator:ok"
],
);
assert!(
log.entries[2].detail.contains("ticket 4711"),
"{:?}",
log.entries[2]
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_name_the_filter_stops_is_refused_held_or_allowed_by_the_account_admin() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let err = w
.create(&w.acme, "acme", "fuck-this", Visibility::Private)
.await
.expect_err("an offensive name");
assert_eq!(err.code(), Code::InvalidArgument);
assert!(
err.message().contains("refused")
&& err.message().to_lowercase().contains("choose another name"),
"{}",
err.message()
);
assert!(
!err.message()
.to_lowercase()
.contains("contains a word \"fuck"),
"the word is not repeated back"
);
assert!(
w.get(&w.acme, "acme", "fuck-this").await.is_err(),
"nothing was made"
);
let err = w
.create(&w.acme, "acme", "sh1t_h3ad", Visibility::Private)
.await
.expect_err("leet");
assert_eq!(err.code(), Code::InvalidArgument);
let err = w
.create(&w.acme, "acme", "porn-filter", Visibility::Private)
.await
.expect_err("held");
assert_eq!(err.code(), Code::FailedPrecondition);
assert!(
err.message().contains("account admin's review"),
"{}",
err.message()
);
let queue = w
.enterprise()
.list_name_reviews(authed(
&w.acme,
ListNameReviewsRequest {
account: "acme".into(),
},
))
.await
.expect("acme's queue")
.into_inner();
assert_eq!(queue.pending, 1, "{queue:?}");
assert_eq!(
queue.reviews.len(),
3,
"the two refusals are noted too: {queue:?}"
);
let held = queue
.reviews
.iter()
.find(|r| r.state == "pending")
.unwrap()
.clone();
for who in [&w.op, &w.bob] {
let err = w
.enterprise()
.decide_name_review(authed(
who,
DecideNameReviewRequest {
account: "acme".into(),
id: held.id.clone(),
allow: true,
},
))
.await
.expect_err("only the account admin decides");
assert_eq!(err.code(), Code::PermissionDenied, "{}", who.principal_id);
}
w.enterprise()
.decide_name_review(authed(
&w.acme,
DecideNameReviewRequest {
account: "acme".into(),
id: held.id.clone(),
allow: true,
},
))
.await
.expect("acme allows it");
w.create(&w.acme, "acme", "porn-filter", Visibility::Private)
.await
.expect("allowed in acme now");
let err = w
.accounts()
.enroll_employee(authed(
&w.acme,
EnrollEmployeeRequest {
account: "acme".into(),
id: "support".into(),
name: "support".into(),
..Default::default()
},
))
.await
.expect_err("a reserved name");
assert_eq!(err.code(), Code::InvalidArgument);
assert!(err.message().contains("reserved"), "{}", err.message());
let err = w
.access()
.add_principal(authed(
&w.relay,
AddPrincipalRequest {
name: "administrator".into(),
kind: PrincipalKind::User as i32,
id: String::new(),
},
))
.await
.expect_err("a machine relaying a signup");
assert_eq!(err.code(), Code::InvalidArgument);
w.access()
.add_principal(authed(
&w.op,
AddPrincipalRequest {
name: "security".into(),
kind: PrincipalKind::User as i32,
id: String::new(),
},
))
.await
.expect("the operator names a staff account");
let log = w.log(&w.acme, &["name"]).await.unwrap();
let seen = kinds(&log);
for want in [
"name.refused:admin:refused",
"name.held:admin:refused",
"name.allowed:admin:ok",
] {
assert!(seen.iter().any(|s| s == want), "{want}: {seen:#?}");
}
let mut lists = w
.enterprise()
.get_name_lists(authed(&w.op, GetNameListsRequest {}))
.await
.expect("the lists")
.into_inner();
assert!(!lists.blocked.is_empty() && !lists.reserved.is_empty());
lists.blocked.push("frobnicate".into());
w.enterprise()
.set_name_lists(authed(
&w.op,
SetNameListsRequest {
lists: Some(lists.clone()),
},
))
.await
.expect("the operator sets the lists");
let err = w
.create(&w.acme, "acme", "frobnicate-all", Visibility::Private)
.await
.expect_err("the new word");
assert_eq!(err.code(), Code::InvalidArgument);
let err = w
.enterprise()
.set_name_lists(authed(&w.acme, SetNameListsRequest { lists: Some(lists) }))
.await
.expect_err("an account admin does not set the server's lists");
assert_eq!(err.code(), Code::PermissionDenied);
let checked = w
.enterprise()
.check_name(authed(
&w.bob,
CheckNameRequest {
kind: "repo".into(),
name: "f\u{0430}ggot-list".into(),
account: "acme".into(),
},
))
.await
.expect("anyone authenticated asks")
.into_inner();
assert_eq!(checked.verdict, "blocked", "a Cyrillic а is still a");
}
#[tokio::test(flavor = "multi_thread")]
async fn a_machine_enrolment_approval_is_in_the_approvers_account_log() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let op_key = Passkey::new(13, "op-yubikey");
w.plane
.add_credential("op", &op_key.id, &op_key.public(), "op's YubiKey", 0)
.expect("op's passkey");
let kits = [
Passkey::new(14, "kit-oden-key"),
Passkey::new(15, "kit-dwarf-key"),
];
let ask = |machine: &'static str, challenge: Vec<u8>| {
let kit = if machine == "kit-oden" {
&kits[0]
} else {
&kits[1]
};
RequestMachineEnrolmentRequest {
machine: machine.to_owned(),
label: format!("installer kit on {machine}"),
server_level: GrantLevel::Admin as i32,
credential_id: kit.id.clone(),
public_key: kit.public(),
client_data_json: format!(
r#"{{"type":"webauthn.create","challenge":"{}","origin":"{ORIGIN}","crossOrigin":false}}"#,
gunnar_grpc::webauthn::b64url(&challenge)
)
.into_bytes(),
rp_id: RP_ID.to_owned(),
origin: ORIGIN.to_owned(),
sign_count: 0,
attestation_object: gunnar_grpc::webauthn::attestation_none(RP_ID, &kit.id, &kit.public(), 0),
}
};
let held = w
.access()
.request_machine_enrolment(authed(&w.relay, ask("kit-oden", w.challenge().await)))
.await
.expect("the kit asks, relayed")
.into_inner()
.request
.unwrap();
w.access()
.approve_machine_enrolment(stepped(
&w.op,
ApproveMachineEnrolmentRequest {
request_id: held.request_id.clone(),
code: held.code.clone(),
server_level: GrantLevel::Unspecified as i32,
},
))
.await
.expect("the operator approves with its own touch");
let refused = w
.access()
.request_machine_enrolment(authed(&w.relay, ask("kit-dwarf", w.challenge().await)))
.await
.expect("a second kit asks")
.into_inner()
.request
.unwrap();
w.access()
.refuse_machine_enrolment(authed(
&w.op,
RefuseMachineEnrolmentRequest {
request_id: refused.request_id.clone(),
},
))
.await
.expect("the operator refuses it");
let log = w
.enterprise()
.audit_log(authed(
&w.op,
AuditLogRequest {
account: "op".into(),
filter: Some(AuditFilter {
kinds: vec!["approval".into()],
..Default::default()
}),
page_size: 10,
before_seq: None,
},
))
.await
.expect("the operator reads its OWN account's log")
.into_inner();
let seen: Vec<String> = log
.entries
.iter()
.map(|e| format!("{}:{}:{}:{}", e.kind, e.actor, e.actor_role, e.target))
.collect();
assert_eq!(
seen,
vec![
"approval.machine_refused:op:admin:kit-dwarf",
"approval.machine:op:admin:kit-oden"
],
"{seen:#?}"
);
assert!(
log.entries[1].detail.contains(&held.code),
"{:?}",
log.entries[1]
);
let acme = w.log(&w.acme, &["approval"]).await.unwrap();
assert!(acme.entries.is_empty(), "{:?}", kinds(&acme));
}
#[tokio::test(flavor = "multi_thread")]
async fn a_clan_is_an_account_to_the_enterprise_list() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
for id in ["carol", "dave"] {
w.plane
.add_principal(id, id, PrincipalKind::User)
.expect("an ordinary account");
}
let (carol, dave) = (person("carol"), person("dave"));
let err = w
.clans()
.create(authed(
&carol,
CreateClanRequest {
name: "fuck-crew".into(),
contribution_bytes: 0,
},
))
.await
.expect_err("an offensive clan name");
assert_eq!(err.code(), Code::InvalidArgument, "{err:?}");
assert!(
w.plane.namespace("fuck-crew").unwrap().is_none(),
"a refused clan left a namespace"
);
w.clans()
.create(authed(
&carol,
CreateClanRequest {
name: "fellows".into(),
contribution_bytes: 0,
},
))
.await
.expect("a clean clan name");
w.clans()
.invite(authed(
&carol,
InviteToClanRequest {
clan: "fellows".into(),
principal_id: "dave".into(),
},
))
.await
.expect("carol invites dave");
w.clans()
.accept(authed(
&dave,
AcceptClanInviteRequest {
clan: "fellows".into(),
contribution_bytes: 0,
},
))
.await
.expect("dave joins");
w.clans()
.set_admin(authed(
&carol,
SetClanAdminRequest {
clan: "fellows".into(),
principal_id: "dave".into(),
admin: true,
},
))
.await
.expect("carol makes dave an admin");
w.clans()
.set_contribution(authed(
&dave,
SetClanContributionRequest {
clan: "fellows".into(),
contribution_bytes: 0,
},
))
.await
.expect("dave sets what he gives");
w.create(&dave, "fellows", "plans", Visibility::Private)
.await
.expect("a member makes a private repository");
assert!(
!w.listed(&w.op, "")
.await
.contains(&"fellows/plans".to_owned()),
"the operator lists a clan's repository"
);
let err = w
.get(&w.op, "fellows", "plans")
.await
.expect_err("a clan's private repository by name");
assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
let totals = w
.enterprise()
.aggregates(authed(&w.op, AggregatesRequest {}))
.await
.expect("totals")
.into_inner();
assert_eq!(
totals.operator_repos, 1,
"a clan's repository is not the operator's: {totals:?}"
);
assert_eq!(
totals.repos, 5,
"the whole estate: acme's two, op/notes, nordisk/gunnar and fellows/plans: {totals:?}"
);
let log = w
.log_of(&carol, "fellows")
.await
.expect("the clan's admin reads the clan's log");
let seen = kinds(&log);
for want in [
"clan.created:admin:ok",
"clan.invited:admin:ok",
"clan.joined:member:ok",
"clan.admin_made:admin:ok",
"clan.contribution:admin:ok",
"repo.created:admin:ok",
] {
assert!(
seen.iter().any(|s| s == want),
"{want} is not in the clan's log: {seen:#?}"
);
}
for who in [&w.op, &w.acme] {
let err = w
.log_of(who, "fellows")
.await
.expect_err("only the clan's admins");
assert_eq!(
err.code(),
Code::PermissionDenied,
"{}: {err:?}",
who.principal_id
);
}
let own = kinds(&w.log_of(&dave, "dave").await.expect("dave reads his own"));
assert!(
own.iter().any(|s| s == "clan.joined:admin:ok"),
"dave's own log: {own:#?}"
);
let own = kinds(
&w.log_of(&carol, "carol")
.await
.expect("carol reads her own"),
);
assert!(
own.iter().any(|s| s == "clan.created:admin:ok"),
"carol's own log: {own:#?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_repository_offered_out_of_a_clan_is_in_the_clans_the_offerers_and_the_receivers_logs() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
for id in ["carol", "dave"] {
w.plane
.add_principal(id, id, PrincipalKind::User)
.expect("an ordinary account");
}
let (carol, dave) = (person("carol"), person("dave"));
w.clans()
.create(authed(
&carol,
CreateClanRequest {
name: "fellows".into(),
contribution_bytes: 0,
},
))
.await
.expect("carol makes the clan");
w.create(&carol, "fellows", "plans", Visibility::Private)
.await
.expect("a clan's repository");
let offer = |who: &Caller| {
let who = who.clone();
let clans = w.clans();
async move {
clans
.offer_repo(authed(
&who,
OfferRepoRequest {
clan: "fellows".into(),
repo: "plans".into(),
account: "dave".into(),
},
))
.await
.map(|_| ())
}
};
let answer = |who: &Caller, accept: bool| {
let who = who.clone();
let clans = w.clans();
async move {
clans
.answer_repo_offer(authed(
&who,
AnswerRepoOfferRequest {
clan: "fellows".into(),
repo: "plans".into(),
accept,
},
))
.await
.map(|_| ())
}
};
offer(&carol)
.await
.expect("carol offers fellows/plans to dave");
answer(&carol, false).await.expect("carol withdraws it");
offer(&carol).await.expect("offered again");
answer(&dave, false).await.expect("dave declines it");
offer(&carol).await.expect("offered a third time");
answer(&dave, true).await.expect("dave takes it");
const EVENTS: [&str; 4] = [
"clan.repo_offered",
"clan.repo_offer_withdrawn",
"clan.repo_offer_declined",
"clan.repo_moved",
];
let count = |seen: &[String], kind: &str| {
seen.iter()
.filter(|s| s.split(':').next() == Some(kind) && s.ends_with(":ok"))
.count()
};
for (reader, account, offered) in [
(&carol, "fellows", 3),
(&carol, "carol", 3),
(&dave, "dave", 3),
] {
let seen = kinds(
&w.log_of(reader, account)
.await
.unwrap_or_else(|e| panic!("{account}'s log: {e:?}")),
);
assert_eq!(
count(&seen, "clan.repo_offered"),
offered,
"three offers in {account}'s log: {seen:#?}"
);
for kind in &EVENTS[1..] {
assert_eq!(
count(&seen, kind),
1,
"{kind} once in {account}'s log: {seen:#?}"
);
}
}
let acme = kinds(&w.log_of(&w.acme, "acme").await.expect("acme's own"));
for kind in EVENTS {
assert_eq!(
count(&acme, kind),
0,
"{kind} in an uninvolved account's log: {acme:#?}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn who_am_i_names_the_account_and_the_callers_role_in_it() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let who = |caller: &Caller| {
let access = w.access();
let caller = caller.clone();
async move {
let me = access
.who_am_i(authed(&caller, WhoAmIRequest {}))
.await
.expect("an answer")
.into_inner();
(me.account, me.account_role, me.account_admin)
}
};
assert_eq!(
who(&w.acme).await,
("acme".to_owned(), "admin".to_owned(), "acme".to_owned())
);
assert_eq!(
who(&w.bob).await,
("acme".to_owned(), "employee".to_owned(), "acme".to_owned()),
"a member acts in the account that enrolled him"
);
assert_eq!(
who(&w.op).await,
("op".to_owned(), "individual".to_owned(), "op".to_owned())
);
assert_eq!(
who(&w.relay).await,
(String::new(), String::new(), String::new()),
"a machine with no account of its own speaks for none"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_account_admin_gives_a_member_write_and_takes_it_away() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let write = |who: &Caller, principal: &str| {
let access = w.access();
let (who, principal) = (who.clone(), principal.to_owned());
async move {
access
.grant(authed(
&who,
GrantRequest {
principal_id: principal,
namespace: "acme".into(),
repo: "site".into(),
level: GrantLevel::Write as i32,
},
))
.await
.map(|_| ())
}
};
let listed = |who: &Caller, namespace: &str, repo: &str| {
let access = w.access();
let (who, namespace, repo) = (who.clone(), namespace.to_owned(), repo.to_owned());
async move {
access
.list_grants(authed(
&who,
ListGrantsRequest {
principal_id: String::new(),
namespace,
repo,
},
))
.await
.map(|r| {
let mut rows: Vec<String> = r
.into_inner()
.grants
.into_iter()
.map(|g| {
format!("{}:{}/{}:{}", g.principal_id, g.namespace, g.repo, g.level)
})
.collect();
rows.sort();
rows
})
}
};
let (read, writes) = (GrantLevel::Read as i32, GrantLevel::Write as i32);
let own = w
.access()
.check(authed(
&w.bob,
CheckRequest {
namespace: "bob".into(),
repo: String::new(),
level: GrantLevel::Admin as i32,
},
))
.await
.expect("an answer")
.into_inner();
assert!(
!own.allowed,
"a member is no account's admin by his name: {own:?}"
);
let refused = write(&w.bob, "bob")
.await
.expect_err("a member grants nothing");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
let refused = listed(&w.bob, "acme", "")
.await
.expect_err("a member reads no grants");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
assert_eq!(
listed(&w.acme, "acme", "")
.await
.expect("the admin reads the account's grants"),
[format!("bob:acme/:{read}")]
);
write(&w.acme, "bob")
.await
.expect("the account admin grants Write");
assert_eq!(
listed(&w.acme, "acme", "site")
.await
.expect("one repository's"),
[format!("bob:acme/site:{writes}")]
);
let may = w
.access()
.check(authed(
&w.bob,
CheckRequest {
namespace: "acme".into(),
repo: "site".into(),
level: GrantLevel::Write as i32,
},
))
.await
.expect("an answer")
.into_inner();
assert!(may.allowed, "bob may write acme/site now: {may:?}");
for (namespace, why) in [("nordisk", "another namespace"), ("", "the whole table")] {
let refused = listed(&w.acme, namespace, "").await.expect_err(why);
assert_eq!(refused.code(), Code::PermissionDenied, "{why}: {refused:?}");
}
w.access()
.revoke(authed(
&w.acme,
RevokeRequest {
principal_id: "bob".into(),
namespace: "acme".into(),
repo: "site".into(),
},
))
.await
.expect("the account admin takes Write away");
assert_eq!(
listed(&w.acme, "acme", "site")
.await
.expect("one repository's"),
Vec::<String>::new()
);
let may = w
.access()
.check(authed(
&w.bob,
CheckRequest {
namespace: "acme".into(),
repo: "site".into(),
level: GrantLevel::Write as i32,
},
))
.await
.expect("an answer")
.into_inner();
assert!(!may.allowed, "bob may no longer write acme/site: {may:?}");
let log = kinds(&w.log(&w.acme, &["grant"]).await.expect("the admin's log"));
for want in ["grant.added:admin:ok", "grant.revoked:admin:ok"] {
assert!(
log.iter().any(|s| s == want),
"{want} is not in acme's log: {log:#?}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn users_the_admin_bought_are_added_in_the_admins_name() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let order = "gunnar/acme/2026-10-07+users=1";
w.accounts()
.enroll_employee(authed(
&w.op,
EnrollEmployeeRequest {
account: "acme".into(),
id: "carl".into(),
name: "Carl".into(),
order: order.into(),
..Default::default()
},
))
.await
.expect("the seller delivers the paid order");
let log = w
.log(&w.acme, &["member.added"])
.await
.expect("the admin's log");
let carl = log
.entries
.iter()
.find(|e| e.target == "carl")
.expect("carl's entry");
assert_eq!(
(carl.actor.as_str(), carl.actor_role.as_str()),
("acme", "admin"),
"{carl:?}"
);
assert!(
carl.detail.contains(order) && carl.detail.contains("delivered by op"),
"the delivery is named: {carl:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn turning_encryption_on_or_off_is_in_the_accounts_audit_log() {
let w = tokio::task::spawn_blocking(world).await.unwrap();
let set = |on: bool| SetEncryptedRequest {
path: Some(RepoPath::new("acme", "site")),
encrypted: on,
};
let refused = w
.repos()
.set_encrypted(authed(&w.bob, set(true)))
.await
.expect_err("a member may not");
assert_eq!(refused.code(), Code::PermissionDenied);
w.repos()
.set_encrypted(authed(&w.acme, set(true)))
.await
.expect("the account admin turns it on");
w.repos()
.set_encrypted(authed(&w.acme, set(false)))
.await
.expect("…and off");
let log = w.log(&w.acme, &[]).await.expect("the admin reads the log");
let seen = kinds(&log);
for want in [
"encrypted.changed:member:refused",
"encrypted.changed:admin:ok",
] {
assert!(
seen.iter().any(|s| s == want),
"{want} is not in the log: {seen:#?}"
);
}
let off = log
.entries
.iter()
.find(|e| e.kind == "encrypted.changed" && e.outcome == "ok")
.expect("the newest");
assert_eq!(
off.detail, "encrypted → plaintext allowed",
"newest first: the turn OFF"
);
assert_eq!(off.target, "acme/site");
}