blob: 26025402cbec0c0268ac2b41c2b6f78a5c6e470b [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::Error,
diagnostics_log as flog,
fidl::endpoints::ProtocolMarker,
fidl_fuchsia_component_resolution as fresolution, fidl_fuchsia_logger as flogger,
fuchsia_async as fasync,
fuchsia_component::server::ServiceFs,
fuchsia_component_test::LocalComponentHandles,
fuchsia_url::{ComponentUrl, PackageUrl},
futures::{StreamExt, TryStreamExt},
itertools::Itertools,
std::{collections::HashSet, sync::Arc},
tracing::warn,
};
type LogSubscriber = dyn tracing::Subscriber + std::marker::Send + std::marker::Sync + 'static;
// The list of non-hermetic packages allowed to resolved by a test.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AllowedPackages {
// Strict list of allowed packages.
pkgs: Arc<HashSet<String>>,
}
impl AllowedPackages {
pub fn zero_allowed_pkgs() -> Self {
Self { pkgs: HashSet::new().into() }
}
pub fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = String>,
{
Self { pkgs: Arc::new(HashSet::from_iter(iter)) }
}
}
async fn validate_hermetic_package(
component_url_str: &str,
subscriber: Arc<LogSubscriber>,
hermetic_test_package_name: &String,
other_allowed_packages: &AllowedPackages,
) -> Result<(), fresolution::ResolverError> {
let component_url = ComponentUrl::parse(component_url_str).map_err(|err| {
warn!("cannot parse {}, {:?}", component_url_str, err);
fresolution::ResolverError::InvalidArgs
})?;
match component_url.package_url() {
PackageUrl::Absolute(pkg_url) => {
let package_name = pkg_url.name();
if hermetic_test_package_name != package_name.as_ref()
&& !other_allowed_packages.pkgs.contains(package_name.as_ref())
{
let s = format!("failed to resolve component {}: package {} is not in the test package allowlist: '{}, {}'
\nSee https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#hermetic-resolver
for more information.",
&component_url_str, package_name, hermetic_test_package_name, other_allowed_packages.pkgs.iter().join(", "));
// log in both test managers log sink and test's log sink so that it is easy to retrieve.
tracing::subscriber::with_default(subscriber, || {
warn!("{}", s);
});
warn!("{}", s);
return Err(fresolution::ResolverError::PackageNotFound);
}
}
PackageUrl::Relative(_url) => {
// don't do anything as we don't restrict relative urls.
}
}
Ok(())
}
async fn serve_resolver(
mut stream: fresolution::ResolverRequestStream,
subscriber: Arc<LogSubscriber>,
hermetic_test_package_name: Arc<String>,
other_allowed_packages: AllowedPackages,
full_resolver: Arc<fresolution::ResolverProxy>,
) {
while let Some(request) = stream.try_next().await.expect("failed to serve component resolver") {
match request {
fresolution::ResolverRequest::Resolve { component_url, responder } => {
let result = if let Err(err) = validate_hermetic_package(
&component_url,
subscriber.clone(),
&hermetic_test_package_name,
&other_allowed_packages,
)
.await
{
Err(err)
} else {
let subscriber = subscriber.clone();
full_resolver.resolve(&component_url).await.unwrap_or_else(|err| {
tracing::subscriber::with_default(subscriber, || {
warn!("failed to resolve component {}: {:?}", component_url, err);
});
Err(fresolution::ResolverError::Internal)
})
};
if let Err(e) = responder.send(result) {
warn!("Failed sending load response for {}: {}", component_url, e);
}
}
fresolution::ResolverRequest::ResolveWithContext {
component_url,
context,
responder,
} => {
// We don't need to worry about validating context because it should have
// been produced by Resolve call above.
let result = if let Err(err) = validate_hermetic_package(
&component_url,
subscriber.clone(),
&hermetic_test_package_name,
&other_allowed_packages,
)
.await
{
Err(err)
} else {
let subscriber = subscriber.clone();
full_resolver
.resolve_with_context(&component_url, &context)
.await
.unwrap_or_else(|err| {
tracing::subscriber::with_default(subscriber, || {
warn!(
"failed to resolve component {} with context {:?}: {:?}",
component_url, context, err
);
});
Err(fresolution::ResolverError::Internal)
})
};
if let Err(e) = responder.send(result) {
warn!("Failed sending load response for {}: {}", component_url, e);
}
}
}
}
}
pub async fn serve_hermetic_resolver(
handles: LocalComponentHandles,
hermetic_test_package_name: Arc<String>,
other_allowed_packages: AllowedPackages,
full_resolver: Arc<fresolution::ResolverProxy>,
) -> Result<(), Error> {
let mut fs = ServiceFs::new();
let mut tasks = vec![];
let log_proxy = handles
.connect_to_named_protocol::<flogger::LogSinkMarker>(flogger::LogSinkMarker::DEBUG_NAME)?;
let tags = ["test_resolver"];
let log_publisher = match flog::Publisher::new(
flog::PublisherOptions::default().tags(&tags).use_log_sink(log_proxy),
) {
Ok(publisher) => Arc::new(publisher) as Arc<LogSubscriber>,
Err(e) => {
warn!("Error creating log publisher for resolver: {:?}", e);
Arc::new(tracing::subscriber::NoSubscriber::default()) as Arc<LogSubscriber>
}
};
fs.dir("svc").add_fidl_service(move |stream: fresolution::ResolverRequestStream| {
let full_resolver = full_resolver.clone();
let hermetic_test_package_name = hermetic_test_package_name.clone();
let other_allowed_packages = other_allowed_packages.clone();
let log_publisher = log_publisher.clone();
tasks.push(fasync::Task::local(async move {
serve_resolver(
stream,
log_publisher,
hermetic_test_package_name,
other_allowed_packages,
full_resolver,
)
.await;
}));
});
fs.serve_connection(handles.outgoing_dir)?;
fs.collect::<()>().await;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use fidl::endpoints::create_proxy_and_stream;
use maplit::hashset;
async fn respond_to_resolve_requests(mut stream: fresolution::ResolverRequestStream) {
while let Some(request) =
stream.try_next().await.expect("failed to serve component mock resolver")
{
match request {
fresolution::ResolverRequest::Resolve { component_url, responder } => {
match component_url.as_str() {
"fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm"
| "fuchsia-pkg://fuchsia.com/package-three#meta/comp.cm"
| "fuchsia-pkg://fuchsia.com/package-four#meta/comp.cm" => {
responder.send(Ok(fresolution::Component::default()))
}
"fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm" => {
responder.send(Err(fresolution::ResolverError::ResourceUnavailable))
}
_ => responder.send(Err(fresolution::ResolverError::Internal)),
}
.expect("failed sending response");
}
fresolution::ResolverRequest::ResolveWithContext {
component_url,
context: _,
responder,
} => {
match component_url.as_str() {
"fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm" | "name#resource" => {
responder.send(Ok(fresolution::Component::default()))
}
_ => responder.send(Err(fresolution::ResolverError::PackageNotFound)),
}
.expect("failed sending response");
}
}
}
}
// Run hermetic resolver
fn run_resolver(
hermetic_test_package_name: Arc<String>,
other_allowed_packages: AllowedPackages,
mock_full_resolver: Arc<fresolution::ResolverProxy>,
) -> (fasync::Task<()>, fresolution::ResolverProxy) {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<fresolution::ResolverMarker>().unwrap();
let subscriber = tracing::subscriber::NoSubscriber::default();
let task = fasync::Task::local(async move {
serve_resolver(
stream,
Arc::new(subscriber),
hermetic_test_package_name,
other_allowed_packages,
mock_full_resolver,
)
.await;
});
(task, proxy)
}
#[fuchsia::test]
async fn test_successful_resolve() {
let pkg_name = "package-one".to_string();
let (resolver_proxy, resolver_request_stream) =
create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock full resolver proxy");
let _full_resolver_task = fasync::Task::spawn(async move {
respond_to_resolve_requests(resolver_request_stream).await;
});
let (_task, hermetic_resolver_proxy) = run_resolver(
pkg_name.into(),
AllowedPackages::zero_allowed_pkgs(),
Arc::new(resolver_proxy),
);
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
.await
.unwrap(),
Ok(fresolution::Component::default())
);
let mock_context = fresolution::Context { bytes: vec![0] };
assert_eq!(
hermetic_resolver_proxy
.resolve_with_context("name#resource", &mock_context)
.await
.unwrap(),
Ok(fresolution::Component::default())
);
assert_eq!(
hermetic_resolver_proxy
.resolve_with_context("name#not_found", &mock_context)
.await
.unwrap(),
Err(fresolution::ResolverError::PackageNotFound)
);
assert_eq!(
hermetic_resolver_proxy
.resolve_with_context(
"fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm",
&mock_context
)
.await
.unwrap(),
Ok(fresolution::Component::default())
);
}
#[fuchsia::test]
async fn drop_connection_on_resolve() {
let pkg_name = "package-one".to_string();
let (resolver_proxy, resolver_request_stream) =
create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock full resolver proxy");
let _full_resolver_task = fasync::Task::spawn(async move {
respond_to_resolve_requests(resolver_request_stream).await;
});
let (_task, hermetic_resolver_proxy) = run_resolver(
pkg_name.into(),
AllowedPackages::zero_allowed_pkgs(),
Arc::new(resolver_proxy),
);
let _ =
hermetic_resolver_proxy.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm");
drop(hermetic_resolver_proxy); // code should not crash
}
#[fuchsia::test]
async fn test_package_not_allowed() {
let (resolver_proxy, _) = create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock full resolver proxy");
let (_task, hermetic_resolver_proxy) = run_resolver(
"package-two".to_string().into(),
AllowedPackages::zero_allowed_pkgs(),
Arc::new(resolver_proxy),
);
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
.await
.unwrap(),
Err(fresolution::ResolverError::PackageNotFound)
);
let mock_context = fresolution::Context { bytes: vec![0] };
assert_eq!(
hermetic_resolver_proxy
.resolve_with_context(
"fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm",
&mock_context
)
.await
.unwrap(),
Err(fresolution::ResolverError::PackageNotFound)
);
}
#[fuchsia::test]
async fn other_packages_allowed() {
let (resolver_proxy, resolver_request_stream) =
create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock full resolver proxy");
let list = hashset!("package-three".to_string(), "package-four".to_string());
let _full_resolver_task = fasync::Task::spawn(async move {
respond_to_resolve_requests(resolver_request_stream).await;
});
let (_task, hermetic_resolver_proxy) = run_resolver(
"package-two".to_string().into(),
AllowedPackages::from_iter(list),
Arc::new(resolver_proxy),
);
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
.await
.unwrap(),
Err(fresolution::ResolverError::PackageNotFound)
);
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-three#meta/comp.cm")
.await
.unwrap(),
Ok(fresolution::Component::default())
);
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-four#meta/comp.cm")
.await
.unwrap(),
Ok(fresolution::Component::default())
);
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm")
.await
.unwrap(),
// we return this error from our mock resolver for package-two.
Err(fresolution::ResolverError::ResourceUnavailable)
);
}
#[fuchsia::test]
async fn test_failed_resolve() {
let (resolver_proxy, resolver_request_stream) =
create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock full resolver proxy");
let _full_resolver_task = fasync::Task::spawn(async move {
respond_to_resolve_requests(resolver_request_stream).await;
});
let pkg_name = "package-two".to_string();
let (_task, hermetic_resolver_proxy) = run_resolver(
pkg_name.into(),
AllowedPackages::zero_allowed_pkgs(),
Arc::new(resolver_proxy),
);
assert_eq!(
hermetic_resolver_proxy
.resolve("fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm")
.await
.unwrap(),
Err(fresolution::ResolverError::ResourceUnavailable)
);
}
#[fuchsia::test]
async fn test_invalid_url() {
let (resolver_proxy, _) = create_proxy_and_stream::<fresolution::ResolverMarker>()
.expect("failed to create mock full resolver proxy");
let pkg_name = "package-two".to_string();
let (_task, hermetic_resolver_proxy) = run_resolver(
pkg_name.into(),
AllowedPackages::zero_allowed_pkgs(),
Arc::new(resolver_proxy),
);
assert_eq!(
hermetic_resolver_proxy.resolve("invalid_url").await.unwrap(),
Err(fresolution::ResolverError::InvalidArgs)
);
}
}