test_manager_lib/
resolver.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::Error;
6use diagnostics_log::Publisher;
7use fidl::endpoints::DiscoverableProtocolMarker;
8use fuchsia_component::server::ServiceFs;
9use fuchsia_component_test::LocalComponentHandles;
10use fuchsia_url::{ComponentUrl, PackageUrl};
11use futures::{StreamExt, TryStreamExt};
12use itertools::Itertools;
13use log::{Log, warn};
14use std::collections::HashSet;
15use std::sync::Arc;
16use {
17    diagnostics_log as flog, fidl_fuchsia_component_resolution as fresolution,
18    fidl_fuchsia_logger as flogger, fidl_fuchsia_pkg as fpkg, fuchsia_async as fasync,
19};
20
21// The list of non-hermetic packages allowed to resolved by a test.
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct AllowedPackages {
24    // Strict list of allowed packages.
25    pkgs: Arc<HashSet<String>>,
26}
27
28impl AllowedPackages {
29    pub fn zero_allowed_pkgs() -> Self {
30        Self { pkgs: HashSet::new().into() }
31    }
32
33    pub fn from_iter<I>(iter: I) -> Self
34    where
35        I: IntoIterator<Item = String>,
36    {
37        Self { pkgs: Arc::new(HashSet::from_iter(iter)) }
38    }
39}
40
41async fn validate_hermetic_package(
42    component_url_str: &str,
43    logger: OptionLogger,
44    hermetic_test_package_name: &String,
45    other_allowed_packages: &AllowedPackages,
46) -> Result<(), fresolution::ResolverError> {
47    let component_url = ComponentUrl::parse(component_url_str).map_err(|err| {
48        warn!("cannot parse {}, {:?}", component_url_str, err);
49        fresolution::ResolverError::InvalidArgs
50    })?;
51
52    match component_url.package_url() {
53        PackageUrl::Absolute(pkg_url) => {
54            let package_name = pkg_url.name();
55            if hermetic_test_package_name != package_name.as_ref()
56                && !other_allowed_packages.pkgs.contains(package_name.as_ref())
57            {
58                let s = format!("failed to resolve component {}: package {} is not in the test package allowlist: '{}, {}'
59                \nSee https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#hermetic-resolver
60                for more information.",
61                &component_url_str, package_name, hermetic_test_package_name, other_allowed_packages.pkgs.iter().join(", "));
62                // log in both test managers log sink and test's log sink so that it is easy to retrieve.
63
64                let mut builder = log::Record::builder();
65                builder.level(log::Level::Warn);
66                logger.log(&builder.args(format_args!("{}", s)).build());
67                warn!("{}", s);
68                return Err(fresolution::ResolverError::PackageNotFound);
69            }
70        }
71        PackageUrl::Relative(_url) => {
72            // don't do anything as we don't restrict relative urls.
73        }
74    }
75    Ok(())
76}
77
78async fn validate_hermetic_url(
79    pkg_url_str: &str,
80    logger: OptionLogger,
81    hermetic_test_package_name: &String,
82    other_allowed_packages: &AllowedPackages,
83) -> Result<(), fpkg::ResolveError> {
84    let pkg_url = PackageUrl::parse(pkg_url_str).map_err(|err| {
85        warn!("cannot parse {}, {:?}", pkg_url_str, err);
86        fpkg::ResolveError::InvalidUrl
87    })?;
88
89    match pkg_url {
90        PackageUrl::Absolute(pkg_url) => {
91            let package_name = pkg_url.name();
92            if hermetic_test_package_name != package_name.as_ref()
93                && !other_allowed_packages.pkgs.contains(package_name.as_ref())
94            {
95                let s = format!("failed to resolve component {}: package {} is not in the test package allowlist: '{}, {}'
96                \nSee https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#hermetic-resolver
97                for more information.",
98                &pkg_url_str, package_name, hermetic_test_package_name, other_allowed_packages.pkgs.iter().join(", "));
99                // log in both test managers log sink and test's log sink so that it is easy to retrieve.
100                let mut builder = log::Record::builder();
101                builder.level(log::Level::Warn);
102                logger.log(&builder.args(format_args!("{}", s)).build());
103                warn!("{}", s);
104                return Err(fpkg::ResolveError::PackageNotFound);
105            }
106        }
107        PackageUrl::Relative(_url) => {
108            // don't do anything as we don't restrict relative urls.
109        }
110    }
111    Ok(())
112}
113
114async fn serve_resolver(
115    mut stream: fresolution::ResolverRequestStream,
116    logger: OptionLogger,
117    hermetic_test_package_name: Arc<String>,
118    other_allowed_packages: AllowedPackages,
119    full_resolver: Arc<fresolution::ResolverProxy>,
120) {
121    while let Some(request) = stream.try_next().await.expect("failed to serve component resolver") {
122        match request {
123            fresolution::ResolverRequest::Resolve { component_url, responder } => {
124                let result = if let Err(err) = validate_hermetic_package(
125                    &component_url,
126                    logger.clone(),
127                    &hermetic_test_package_name,
128                    &other_allowed_packages,
129                )
130                .await
131                {
132                    Err(err)
133                } else {
134                    let logger = logger.clone();
135                    full_resolver.resolve(&component_url).await.unwrap_or_else(|err| {
136                        let mut builder = log::Record::builder();
137                        builder.level(log::Level::Warn);
138                        logger.log(
139                            &builder
140                                .args(format_args!(
141                                    "failed to resolve component {}: {:?}",
142                                    component_url, err
143                                ))
144                                .build(),
145                        );
146                        Err(fresolution::ResolverError::Internal)
147                    })
148                };
149                if let Err(e) = responder.send(result) {
150                    warn!("Failed sending load response for {}: {}", component_url, e);
151                }
152            }
153            fresolution::ResolverRequest::ResolveWithContext {
154                component_url,
155                context,
156                responder,
157            } => {
158                // We don't need to worry about validating context because it should have
159                // been produced by Resolve call above.
160                let result = if let Err(err) = validate_hermetic_package(
161                    &component_url,
162                    logger.clone(),
163                    &hermetic_test_package_name,
164                    &other_allowed_packages,
165                )
166                .await
167                {
168                    Err(err)
169                } else {
170                    let logger = logger.clone();
171                    full_resolver
172                        .resolve_with_context(&component_url, &context)
173                        .await
174                        .unwrap_or_else(|err| {
175                            let mut builder = log::Record::builder();
176                            builder.level(log::Level::Warn);
177                            logger.log(
178                                &builder
179                                    .args(format_args!(
180                                        "failed to resolve component {} with context {:?}: {:?}",
181                                        component_url, context, err
182                                    ))
183                                    .build(),
184                            );
185                            Err(fresolution::ResolverError::Internal)
186                        })
187                };
188                if let Err(e) = responder.send(result) {
189                    warn!("Failed sending load response for {}: {}", component_url, e);
190                }
191            }
192            fresolution::ResolverRequest::_UnknownMethod { ordinal, .. } => {
193                warn!(ordinal:%; "Unknown Resolver request");
194            }
195        }
196    }
197}
198
199async fn serve_pkg_resolver(
200    mut stream: fpkg::PackageResolverRequestStream,
201    logger: OptionLogger,
202    hermetic_test_package_name: Arc<String>,
203    other_allowed_packages: AllowedPackages,
204    pkg_resolver: Arc<fpkg::PackageResolverProxy>,
205) {
206    while let Some(request) = stream.try_next().await.expect("failed to serve component resolver") {
207        match request {
208            fpkg::PackageResolverRequest::Resolve { package_url, dir, responder } => {
209                let result = if let Err(err) = validate_hermetic_url(
210                    &package_url,
211                    logger.clone(),
212                    &hermetic_test_package_name,
213                    &other_allowed_packages,
214                )
215                .await
216                {
217                    Err(err)
218                } else {
219                    let logger = logger.clone();
220                    pkg_resolver.resolve(&package_url, dir).await.unwrap_or_else(|err| {
221                        let mut builder = log::Record::builder();
222                        builder.level(log::Level::Warn);
223                        logger.log(
224                            &builder
225                                .args(format_args!(
226                                    "failed to resolve pkg {}: {:?}",
227                                    package_url, err
228                                ))
229                                .build(),
230                        );
231                        Err(fpkg::ResolveError::Internal)
232                    })
233                };
234                let result_ref = result.as_ref();
235                let result_ref = result_ref.map_err(|e| e.to_owned());
236                if let Err(e) = responder.send(result_ref) {
237                    warn!("Failed sending load response for {}: {}", package_url, e);
238                }
239            }
240            fpkg::PackageResolverRequest::ResolveWithContext {
241                package_url,
242                context,
243                dir,
244                responder,
245            } => {
246                // We don't need to worry about validating context because it should have
247                // been produced by Resolve call above.
248                let result = if let Err(err) = validate_hermetic_url(
249                    &package_url,
250                    logger.clone(),
251                    &hermetic_test_package_name,
252                    &other_allowed_packages,
253                )
254                .await
255                {
256                    Err(err)
257                } else {
258                    let logger = logger.clone();
259                    pkg_resolver
260                        .resolve_with_context(&package_url, &context, dir)
261                        .await
262                        .unwrap_or_else(|err| {
263                            let mut builder = log::Record::builder();
264                            builder.level(log::Level::Warn);
265                            logger.log(
266                                &builder
267                                    .args(format_args!(
268                                        "failed to resolve pkg {} with context {:?}: {:?}",
269                                        package_url, context, err
270                                    ))
271                                    .build(),
272                            );
273                            Err(fpkg::ResolveError::Internal)
274                        })
275                };
276                let result_ref = result.as_ref();
277                let result_ref = result_ref.map_err(|e| e.to_owned());
278                if let Err(e) = responder.send(result_ref) {
279                    warn!("Failed sending load response for {}: {}", package_url, e);
280                }
281            }
282            fpkg::PackageResolverRequest::GetHash { package_url, responder } => {
283                let result = if let Err(_err) = validate_hermetic_url(
284                    package_url.url.as_str(),
285                    logger.clone(),
286                    &hermetic_test_package_name,
287                    &other_allowed_packages,
288                )
289                .await
290                {
291                    Err(zx::Status::INTERNAL.into_raw())
292                } else {
293                    let logger = logger.clone();
294                    pkg_resolver.get_hash(&package_url).await.unwrap_or_else(|err| {
295                        let mut builder = log::Record::builder();
296                        builder.level(log::Level::Warn);
297                        logger.log(
298                            &builder
299                                .args(format_args!(
300                                    "failed to resolve pkg {}: {:?}",
301                                    package_url.url.as_str(),
302                                    err
303                                ))
304                                .build(),
305                        );
306                        Err(zx::Status::INTERNAL.into_raw())
307                    })
308                };
309                let result_ref = result.as_ref();
310                let result_ref = result_ref.map_err(|e| e.to_owned());
311                if let Err(e) = responder.send(result_ref) {
312                    warn!("Failed sending load response for {}: {}", package_url.url.as_str(), e);
313                }
314            }
315        }
316    }
317}
318
319#[derive(Clone)]
320pub struct OptionLogger(Option<Publisher>);
321
322impl OptionLogger {
323    pub fn log(&self, record: &log::Record<'_>) {
324        if let Some(logger) = self.0.as_ref() {
325            logger.log(record);
326        }
327    }
328}
329
330pub async fn serve_hermetic_resolver(
331    handles: LocalComponentHandles,
332    hermetic_test_package_name: Arc<String>,
333    other_allowed_packages: AllowedPackages,
334    full_resolver: Arc<fresolution::ResolverProxy>,
335    pkg_resolver: Arc<fpkg::PackageResolverProxy>,
336) -> Result<(), Error> {
337    let mut fs = ServiceFs::new();
338    let mut resolver_tasks = vec![];
339    let mut pkg_resolver_tasks = vec![];
340    let log_client = handles.connect_to_named_protocol(flogger::LogSinkMarker::PROTOCOL_NAME)?;
341    let tags = ["test_resolver"];
342    let log_publisher = match flog::Publisher::new_async(
343        flog::PublisherOptions::default().tags(&tags).use_log_sink(log_client),
344    )
345    .await
346    {
347        Ok(publisher) => OptionLogger(Some(publisher)),
348        Err(e) => {
349            warn!("Error creating log publisher for resolver: {:?}", e);
350            OptionLogger(None)
351        }
352    };
353
354    let resolver_hermetic_test_package_name = hermetic_test_package_name.clone();
355    let resolver_other_allowed_packages = other_allowed_packages.clone();
356    let resolver_log_publisher = log_publisher.clone();
357
358    let pkg_resolver_hermetic_test_package_name = hermetic_test_package_name.clone();
359    let pkg_resolver_other_allowed_packages = other_allowed_packages.clone();
360    let pkg_resolver_log_publisher = log_publisher.clone();
361
362    fs.dir("svc").add_fidl_service(move |stream: fresolution::ResolverRequestStream| {
363        let full_resolver = full_resolver.clone();
364        let hermetic_test_package_name = resolver_hermetic_test_package_name.clone();
365        let other_allowed_packages = resolver_other_allowed_packages.clone();
366        let log_publisher = resolver_log_publisher.clone();
367        resolver_tasks.push(fasync::Task::local(async move {
368            serve_resolver(
369                stream,
370                log_publisher,
371                hermetic_test_package_name,
372                other_allowed_packages,
373                full_resolver,
374            )
375            .await;
376        }));
377    });
378    fs.dir("svc").add_fidl_service(move |stream: fpkg::PackageResolverRequestStream| {
379        let pkg_resolver = pkg_resolver.clone();
380        let hermetic_test_package_name = pkg_resolver_hermetic_test_package_name.clone();
381        let other_allowed_packages = pkg_resolver_other_allowed_packages.clone();
382        let log_publisher = pkg_resolver_log_publisher.clone();
383        pkg_resolver_tasks.push(fasync::Task::local(async move {
384            serve_pkg_resolver(
385                stream,
386                log_publisher,
387                hermetic_test_package_name,
388                other_allowed_packages,
389                pkg_resolver,
390            )
391            .await;
392        }));
393    });
394    fs.serve_connection(handles.outgoing_dir)?;
395    fs.collect::<()>().await;
396    Ok(())
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use fidl::endpoints::create_proxy_and_stream;
403    use maplit::hashset;
404
405    async fn respond_to_resolve_requests(mut stream: fresolution::ResolverRequestStream) {
406        while let Some(request) =
407            stream.try_next().await.expect("failed to serve component mock resolver")
408        {
409            match request {
410                fresolution::ResolverRequest::Resolve { component_url, responder } => {
411                    match component_url.as_str() {
412                        "fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm"
413                        | "fuchsia-pkg://fuchsia.com/package-three#meta/comp.cm"
414                        | "fuchsia-pkg://fuchsia.com/package-four#meta/comp.cm" => {
415                            responder.send(Ok(fresolution::Component::default()))
416                        }
417                        "fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm" => {
418                            responder.send(Err(fresolution::ResolverError::ResourceUnavailable))
419                        }
420                        _ => responder.send(Err(fresolution::ResolverError::Internal)),
421                    }
422                    .expect("failed sending response");
423                }
424                fresolution::ResolverRequest::ResolveWithContext {
425                    component_url,
426                    context: _,
427                    responder,
428                } => {
429                    match component_url.as_str() {
430                        "fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm" | "name#resource" => {
431                            responder.send(Ok(fresolution::Component::default()))
432                        }
433                        _ => responder.send(Err(fresolution::ResolverError::PackageNotFound)),
434                    }
435                    .expect("failed sending response");
436                }
437                fresolution::ResolverRequest::_UnknownMethod { .. } => {
438                    panic!("Unknown Resolver request");
439                }
440            }
441        }
442    }
443
444    // Run hermetic resolver
445    fn run_resolver(
446        hermetic_test_package_name: Arc<String>,
447        other_allowed_packages: AllowedPackages,
448        mock_full_resolver: Arc<fresolution::ResolverProxy>,
449    ) -> (fasync::Task<()>, fresolution::ResolverProxy) {
450        let (proxy, stream) =
451            fidl::endpoints::create_proxy_and_stream::<fresolution::ResolverMarker>();
452        let logger = OptionLogger(None);
453        let task = fasync::Task::local(async move {
454            serve_resolver(
455                stream,
456                logger,
457                hermetic_test_package_name,
458                other_allowed_packages,
459                mock_full_resolver,
460            )
461            .await;
462        });
463        (task, proxy)
464    }
465
466    #[fuchsia::test]
467    async fn test_successful_resolve() {
468        let pkg_name = "package-one".to_string();
469
470        let (resolver_proxy, resolver_request_stream) =
471            create_proxy_and_stream::<fresolution::ResolverMarker>();
472        let _full_resolver_task = fasync::Task::spawn(async move {
473            respond_to_resolve_requests(resolver_request_stream).await;
474        });
475
476        let (_task, hermetic_resolver_proxy) = run_resolver(
477            pkg_name.into(),
478            AllowedPackages::zero_allowed_pkgs(),
479            Arc::new(resolver_proxy),
480        );
481
482        assert_eq!(
483            hermetic_resolver_proxy
484                .resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
485                .await
486                .unwrap(),
487            Ok(fresolution::Component::default())
488        );
489        let mock_context = fresolution::Context { bytes: vec![0] };
490        assert_eq!(
491            hermetic_resolver_proxy
492                .resolve_with_context("name#resource", &mock_context)
493                .await
494                .unwrap(),
495            Ok(fresolution::Component::default())
496        );
497        assert_eq!(
498            hermetic_resolver_proxy
499                .resolve_with_context("name#not_found", &mock_context)
500                .await
501                .unwrap(),
502            Err(fresolution::ResolverError::PackageNotFound)
503        );
504        assert_eq!(
505            hermetic_resolver_proxy
506                .resolve_with_context(
507                    "fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm",
508                    &mock_context
509                )
510                .await
511                .unwrap(),
512            Ok(fresolution::Component::default())
513        );
514    }
515
516    #[fuchsia::test]
517    async fn drop_connection_on_resolve() {
518        let pkg_name = "package-one".to_string();
519
520        let (resolver_proxy, resolver_request_stream) =
521            create_proxy_and_stream::<fresolution::ResolverMarker>();
522        let _full_resolver_task = fasync::Task::spawn(async move {
523            respond_to_resolve_requests(resolver_request_stream).await;
524        });
525
526        let (_task, hermetic_resolver_proxy) = run_resolver(
527            pkg_name.into(),
528            AllowedPackages::zero_allowed_pkgs(),
529            Arc::new(resolver_proxy),
530        );
531
532        let _ =
533            hermetic_resolver_proxy.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm");
534        drop(hermetic_resolver_proxy); // code should not crash
535    }
536
537    #[fuchsia::test]
538    async fn test_package_not_allowed() {
539        let (resolver_proxy, _) = create_proxy_and_stream::<fresolution::ResolverMarker>();
540
541        let (_task, hermetic_resolver_proxy) = run_resolver(
542            "package-two".to_string().into(),
543            AllowedPackages::zero_allowed_pkgs(),
544            Arc::new(resolver_proxy),
545        );
546
547        assert_eq!(
548            hermetic_resolver_proxy
549                .resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
550                .await
551                .unwrap(),
552            Err(fresolution::ResolverError::PackageNotFound)
553        );
554        let mock_context = fresolution::Context { bytes: vec![0] };
555        assert_eq!(
556            hermetic_resolver_proxy
557                .resolve_with_context(
558                    "fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm",
559                    &mock_context
560                )
561                .await
562                .unwrap(),
563            Err(fresolution::ResolverError::PackageNotFound)
564        );
565    }
566
567    #[fuchsia::test]
568    async fn other_packages_allowed() {
569        let (resolver_proxy, resolver_request_stream) =
570            create_proxy_and_stream::<fresolution::ResolverMarker>();
571
572        let list = hashset!("package-three".to_string(), "package-four".to_string());
573
574        let _full_resolver_task = fasync::Task::spawn(async move {
575            respond_to_resolve_requests(resolver_request_stream).await;
576        });
577
578        let (_task, hermetic_resolver_proxy) = run_resolver(
579            "package-two".to_string().into(),
580            AllowedPackages::from_iter(list),
581            Arc::new(resolver_proxy),
582        );
583
584        assert_eq!(
585            hermetic_resolver_proxy
586                .resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
587                .await
588                .unwrap(),
589            Err(fresolution::ResolverError::PackageNotFound)
590        );
591
592        assert_eq!(
593            hermetic_resolver_proxy
594                .resolve("fuchsia-pkg://fuchsia.com/package-three#meta/comp.cm")
595                .await
596                .unwrap(),
597            Ok(fresolution::Component::default())
598        );
599
600        assert_eq!(
601            hermetic_resolver_proxy
602                .resolve("fuchsia-pkg://fuchsia.com/package-four#meta/comp.cm")
603                .await
604                .unwrap(),
605            Ok(fresolution::Component::default())
606        );
607
608        assert_eq!(
609            hermetic_resolver_proxy
610                .resolve("fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm")
611                .await
612                .unwrap(),
613            // we return this error from our mock resolver for package-two.
614            Err(fresolution::ResolverError::ResourceUnavailable)
615        );
616    }
617
618    #[fuchsia::test]
619    async fn test_failed_resolve() {
620        let (resolver_proxy, resolver_request_stream) =
621            create_proxy_and_stream::<fresolution::ResolverMarker>();
622        let _full_resolver_task = fasync::Task::spawn(async move {
623            respond_to_resolve_requests(resolver_request_stream).await;
624        });
625
626        let pkg_name = "package-two".to_string();
627        let (_task, hermetic_resolver_proxy) = run_resolver(
628            pkg_name.into(),
629            AllowedPackages::zero_allowed_pkgs(),
630            Arc::new(resolver_proxy),
631        );
632
633        assert_eq!(
634            hermetic_resolver_proxy
635                .resolve("fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm")
636                .await
637                .unwrap(),
638            Err(fresolution::ResolverError::ResourceUnavailable)
639        );
640    }
641
642    #[fuchsia::test]
643    async fn test_invalid_url() {
644        let (resolver_proxy, _) = create_proxy_and_stream::<fresolution::ResolverMarker>();
645
646        let pkg_name = "package-two".to_string();
647        let (_task, hermetic_resolver_proxy) = run_resolver(
648            pkg_name.into(),
649            AllowedPackages::zero_allowed_pkgs(),
650            Arc::new(resolver_proxy),
651        );
652
653        assert_eq!(
654            hermetic_resolver_proxy.resolve("invalid_url").await.unwrap(),
655            Err(fresolution::ResolverError::InvalidArgs)
656        );
657    }
658}