#![allow(
dead_code,
reason = "shared fixture module; each test binary uses a subset"
)]
use std::sync::Arc;
use tollgate_auth::{CredentialVerifier, HmacRegistry};
use tollgate_client::{BearerToken, HttpStore, HttpStoreConfig, StaticBearer};
use tollgate_server::security::{
ControlIdentity, ProvisionerLimits, ProvisionerPolicyTemplate, Role, SecurityPolicy,
ServerSecurity,
};
pub const INSTANCE: &str = "fixture-instance-credential-98-only";
pub const OPERATOR: &str = "fixture-operator-credential-98-only";
pub const PROVISIONER: &str = "fixture-provisioner-credential-39-only";
pub const PROVISIONER_MAX_BUDGET: u64 = 1_000;
pub fn provisioner_template() -> ProvisionerPolicyTemplate {
use tollgate_core::{CostTable, CostUnits, PermissionBits, PolicyRevision, ResolvedLimits};
ProvisionerPolicyTemplate {
cost_table: Arc::new(CostTable::builder(CostUnits(1), CostUnits(1)).build()),
limits: ResolvedLimits::new(1),
permissions: PermissionBits(0),
policy_revision: PolicyRevision::UNSTATED,
}
}
pub fn policy() -> SecurityPolicy {
let verifier = Arc::new(HmacRegistry::new(b"fixture-server-secret"));
verifier.install_credentials([
INSTANCE.as_bytes(),
OPERATOR.as_bytes(),
PROVISIONER.as_bytes(),
]);
let identities = [
(
verifier.verify(INSTANCE.as_bytes()).unwrap().principal,
ControlIdentity::new("test-instance", Role::Instance).unwrap(),
),
(
verifier.verify(OPERATOR.as_bytes()).unwrap().principal,
ControlIdentity::new("test-operator", Role::Operator).unwrap(),
),
(
verifier.verify(PROVISIONER.as_bytes()).unwrap().principal,
ControlIdentity::provisioner(
"test-provisioner",
ProvisionerLimits::new(
tollgate_core::CostUnits(PROVISIONER_MAX_BUDGET),
vec![provisioner_template()],
)
.unwrap(),
)
.unwrap(),
),
];
SecurityPolicy::new()
.with_bearer(verifier, identities)
.unwrap()
}
pub fn security() -> Arc<ServerSecurity> {
ServerSecurity::new(policy(), None).unwrap()
}
pub fn http(base: impl Into<String>) -> Arc<HttpStore> {
HttpStore::with_config(
base,
HttpStoreConfig {
bearer: Some(StaticBearer::new(BearerToken::new(INSTANCE).unwrap())),
..Default::default()
},
)
.unwrap()
}
#[derive(Clone, Debug)]
pub struct CapturedEvent {
pub target: String,
pub level: tracing::Level,
pub fields: std::collections::BTreeMap<String, String>,
}
#[derive(Clone, Default)]
pub struct EventCapture(Arc<std::sync::Mutex<Vec<CapturedEvent>>>);
impl EventCapture {
pub fn events(&self) -> Vec<CapturedEvent> {
self.0.lock().unwrap().clone()
}
pub async fn during<T>(&self, work: impl std::future::Future<Output = T>) -> T {
use tracing::instrument::WithSubscriber;
use tracing_subscriber::layer::SubscriberExt;
arm();
work.with_subscriber(tracing_subscriber::registry().with(self.clone()))
.await
}
#[must_use]
pub fn on_this_thread(&self) -> tracing::subscriber::DefaultGuard {
use tracing_subscriber::layer::SubscriberExt;
arm();
tracing::subscriber::set_default(tracing_subscriber::registry().with(self.clone()))
}
}
fn arm() {
static INSTALLED: std::sync::Once = std::sync::Once::new();
INSTALLED.call_once(|| {
drop(tracing::subscriber::set_global_default(
tracing_subscriber::registry(),
));
});
}
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for EventCapture {
fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
#[derive(Default)]
struct Fields(std::collections::BTreeMap<String, String>);
impl tracing::field::Visit for Fields {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0.insert(field.name().into(), format!("{value:?}"));
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.0.insert(field.name().into(), value.into());
}
}
let mut fields = Fields::default();
event.record(&mut fields);
self.0.lock().unwrap().push(CapturedEvent {
target: event.metadata().target().into(),
level: *event.metadata().level(),
fields: fields.0,
});
}
}
pub struct Certificates {
pub ca: String,
pub server: String,
pub server_key: String,
pub client: String,
pub client_key: String,
pub other_client: String,
pub other_client_key: String,
}
pub fn certificates() -> Certificates {
use rcgen::{
BasicConstraints, CertificateParams, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair,
KeyUsagePurpose,
};
let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::DigitalSignature,
];
let key = KeyPair::generate().unwrap();
let ca = params.self_signed(&key).unwrap().pem();
let issuer = Issuer::new(params, key);
let mut params = CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()]).unwrap();
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
let key = KeyPair::generate().unwrap();
let server = params.signed_by(&key, &issuer).unwrap().pem();
let server_key = key.serialize_pem();
let mut params = CertificateParams::new(vec!["fixture-instance".into()]).unwrap();
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
let key = KeyPair::generate().unwrap();
let client = params.signed_by(&key, &issuer).unwrap().pem();
let client_key = key.serialize_pem();
let mut params = CertificateParams::new(vec!["fixture-other-instance".into()]).unwrap();
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
let other_key = KeyPair::generate().unwrap();
let other_client = params.signed_by(&other_key, &issuer).unwrap().pem();
Certificates {
ca,
server,
server_key,
client,
client_key,
other_client,
other_client_key: other_key.serialize_pem(),
}
}
#[derive(Clone, Copy)]
pub enum TransportMode {
LoopbackBearer,
TlsBearer,
Mtls,
}
pub fn transport(
mode: TransportMode,
address: std::net::SocketAddr,
) -> (Arc<ServerSecurity>, Arc<HttpStore>) {
use tollgate_server::transport::{TlsConfig, certificate_fingerprint};
if matches!(mode, TransportMode::LoopbackBearer) {
return (security(), http(format!("http://{address}")));
}
let certificates = certificates();
let tls = TlsConfig::from_pem(
certificates.server.as_bytes(),
certificates.server_key.as_bytes(),
Some(certificates.ca.as_bytes()),
)
.unwrap();
let policy = policy()
.with_certificate(
certificate_fingerprint(certificates.client.as_bytes()).unwrap(),
ControlIdentity::new("test-mtls-instance", Role::Instance).unwrap(),
)
.unwrap();
let security = ServerSecurity::new(policy, Some(tls)).unwrap();
let mut config = HttpStoreConfig {
root_ca_pem: Some(certificates.ca.into_bytes()),
..Default::default()
};
match mode {
TransportMode::TlsBearer => {
config.bearer = Some(StaticBearer::new(BearerToken::new(INSTANCE).unwrap()))
}
TransportMode::Mtls => {
config.identity_pem = Some(
format!("{}{}", certificates.client, certificates.client_key)
.into_bytes()
.into(),
)
}
TransportMode::LoopbackBearer => unreachable!(),
}
(
security,
HttpStore::with_config(format!("https://{address}"), config).unwrap(),
)
}
pub async fn send(
router: &axum::Router,
request: axum::http::Request<axum::body::Body>,
) -> (axum::http::StatusCode, serde_json::Value) {
use http_body_util::BodyExt as _;
use tower::ServiceExt as _;
let response = router.clone().oneshot(request).await.unwrap();
let status = response.status();
let bytes = response.into_body().collect().await.unwrap().to_bytes();
let value = if bytes.is_empty() {
serde_json::Value::Null
} else {
serde_json::from_slice(&bytes).unwrap()
};
(status, value)
}