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 fidl::endpoints::ProtocolMarker;
7use fuchsia_component::server::ServiceFs;
8use fuchsia_component_test::LocalComponentHandles;
9use fuchsia_url::{ComponentUrl, PackageUrl};
10use futures::{StreamExt, TryStreamExt};
11use itertools::Itertools;
12use log::warn;
13use std::collections::HashSet;
14use std::sync::Arc;
15use {
16    diagnostics_log as flog, fidl_fuchsia_component_resolution as fresolution,
17    fidl_fuchsia_logger as flogger, fidl_fuchsia_pkg as fpkg, fuchsia_async as fasync,
18};
19
20type LogSubscriber = dyn log::Log + std::marker::Send + std::marker::Sync + 'static;
21
22// The list of non-hermetic packages allowed to resolved by a test.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct AllowedPackages {
25    // Strict list of allowed packages.
26    pkgs: Arc<HashSet<String>>,
27}
28
29impl AllowedPackages {
30    pub fn zero_allowed_pkgs() -> Self {
31        Self { pkgs: HashSet::new().into() }
32    }
33
34    pub fn from_iter<I>(iter: I) -> Self
35    where
36        I: IntoIterator<Item = String>,
37    {
38        Self { pkgs: Arc::new(HashSet::from_iter(iter)) }
39    }
40}
41
42async fn validate_hermetic_package(
43    component_url_str: &str,
44    logger: Arc<LogSubscriber>,
45    hermetic_test_package_name: &String,
46    other_allowed_packages: &AllowedPackages,
47) -> Result<(), fresolution::ResolverError> {
48    let component_url = ComponentUrl::parse(component_url_str).map_err(|err| {
49        warn!("cannot parse {}, {:?}", component_url_str, err);
50        fresolution::ResolverError::InvalidArgs
51    })?;
52
53    match component_url.package_url() {
54        PackageUrl::Absolute(pkg_url) => {
55            let package_name = pkg_url.name();
56            if hermetic_test_package_name != package_name.as_ref()
57                && !other_allowed_packages.pkgs.contains(package_name.as_ref())
58            {
59                let s = format!("failed to resolve component {}: package {} is not in the test package allowlist: '{}, {}'
60                \nSee https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#hermetic-resolver
61                for more information.",
62                &component_url_str, package_name, hermetic_test_package_name, other_allowed_packages.pkgs.iter().join(", "));
63                // log in both test managers log sink and test's log sink so that it is easy to retrieve.
64
65                let mut builder = log::Record::builder();
66                builder.level(log::Level::Warn);
67                logger.log(&builder.args(format_args!("{}", s)).build());
68                warn!("{}", s);
69                return Err(fresolution::ResolverError::PackageNotFound);
70            }
71        }
72        PackageUrl::Relative(_url) => {
73            // don't do anything as we don't restrict relative urls.
74        }
75    }
76    Ok(())
77}
78
79async fn validate_hermetic_url(
80    pkg_url_str: &str,
81    logger: Arc<LogSubscriber>,
82    hermetic_test_package_name: &String,
83    other_allowed_packages: &AllowedPackages,
84) -> Result<(), fpkg::ResolveError> {
85    let pkg_url = PackageUrl::parse(pkg_url_str).map_err(|err| {
86        warn!("cannot parse {}, {:?}", pkg_url_str, err);
87        fpkg::ResolveError::InvalidUrl
88    })?;
89
90    match pkg_url {
91        PackageUrl::Absolute(pkg_url) => {
92            let package_name = pkg_url.name();
93            if hermetic_test_package_name != package_name.as_ref()
94                && !other_allowed_packages.pkgs.contains(package_name.as_ref())
95            {
96                let s = format!("failed to resolve component {}: package {} is not in the test package allowlist: '{}, {}'
97                \nSee https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#hermetic-resolver
98                for more information.",
99                &pkg_url_str, package_name, hermetic_test_package_name, other_allowed_packages.pkgs.iter().join(", "));
100                // log in both test managers log sink and test's log sink so that it is easy to retrieve.
101                let mut builder = log::Record::builder();
102                builder.level(log::Level::Warn);
103                logger.log(&builder.args(format_args!("{}", s)).build());
104                warn!("{}", s);
105                return Err(fpkg::ResolveError::PackageNotFound);
106            }
107        }
108        PackageUrl::Relative(_url) => {
109            // don't do anything as we don't restrict relative urls.
110        }
111    }
112    Ok(())
113}
114
115async fn serve_resolver(
116    mut stream: fresolution::ResolverRequestStream,
117    logger: Arc<LogSubscriber>,
118    hermetic_test_package_name: Arc<String>,
119    other_allowed_packages: AllowedPackages,
120    full_resolver: Arc<fresolution::ResolverProxy>,
121) {
122    while let Some(request) = stream.try_next().await.expect("failed to serve component resolver") {
123        match request {
124            fresolution::ResolverRequest::Resolve { component_url, responder } => {
125                let result = if let Err(err) = validate_hermetic_package(
126                    &component_url,
127                    logger.clone(),
128                    &hermetic_test_package_name,
129                    &other_allowed_packages,
130                )
131                .await
132                {
133                    Err(err)
134                } else {
135                    let logger = logger.clone();
136                    full_resolver.resolve(&component_url).await.unwrap_or_else(|err| {
137                        let mut builder = log::Record::builder();
138                        builder.level(log::Level::Warn);
139                        logger.log(
140                            &builder
141                                .args(format_args!(
142                                    "failed to resolve component {}: {:?}",
143                                    component_url, err
144                                ))
145                                .build(),
146                        );
147                        Err(fresolution::ResolverError::Internal)
148                    })
149                };
150                if let Err(e) = responder.send(result) {
151                    warn!("Failed sending load response for {}: {}", component_url, e);
152                }
153            }
154            fresolution::ResolverRequest::ResolveWithContext {
155                component_url,
156                context,
157                responder,
158            } => {
159                // We don't need to worry about validating context because it should have
160                // been produced by Resolve call above.
161                let result = if let Err(err) = validate_hermetic_package(
162                    &component_url,
163                    logger.clone(),
164                    &hermetic_test_package_name,
165                    &other_allowed_packages,
166                )
167                .await
168                {
169                    Err(err)
170                } else {
171                    let logger = logger.clone();
172                    full_resolver
173                        .resolve_with_context(&component_url, &context)
174                        .await
175                        .unwrap_or_else(|err| {
176                            let mut builder = log::Record::builder();
177                            builder.level(log::Level::Warn);
178                            logger.log(
179                                &builder
180                                    .args(format_args!(
181                                        "failed to resolve component {} with context {:?}: {:?}",
182                                        component_url, context, err
183                                    ))
184                                    .build(),
185                            );
186                            Err(fresolution::ResolverError::Internal)
187                        })
188                };
189                if let Err(e) = responder.send(result) {
190                    warn!("Failed sending load response for {}: {}", component_url, e);
191                }
192            }
193            fresolution::ResolverRequest::_UnknownMethod { ordinal, .. } => {
194                warn!(ordinal:%; "Unknown Resolver request");
195            }
196        }
197    }
198}
199
200async fn serve_pkg_resolver(
201    mut stream: fpkg::PackageResolverRequestStream,
202    logger: Arc<LogSubscriber>,
203    hermetic_test_package_name: Arc<String>,
204    other_allowed_packages: AllowedPackages,
205    pkg_resolver: Arc<fpkg::PackageResolverProxy>,
206) {
207    while let Some(request) = stream.try_next().await.expect("failed to serve component resolver") {
208        match request {
209            fpkg::PackageResolverRequest::Resolve { package_url, dir, responder } => {
210                let result = if let Err(err) = validate_hermetic_url(
211                    &package_url,
212                    logger.clone(),
213                    &hermetic_test_package_name,
214                    &other_allowed_packages,
215                )
216                .await
217                {
218                    Err(err)
219                } else {
220                    let logger = logger.clone();
221                    pkg_resolver.resolve(&package_url, dir).await.unwrap_or_else(|err| {
222                        let mut builder = log::Record::builder();
223                        builder.level(log::Level::Warn);
224                        logger.log(
225                            &builder
226                                .args(format_args!(
227                                    "failed to resolve pkg {}: {:?}",
228                                    package_url, err
229                                ))
230                                .build(),
231                        );
232                        Err(fpkg::ResolveError::Internal)
233                    })
234                };
235                let result_ref = result.as_ref();
236                let result_ref = result_ref.map_err(|e| e.to_owned());
237                if let Err(e) = responder.send(result_ref) {
238                    warn!("Failed sending load response for {}: {}", package_url, e);
239                }
240            }
241            fpkg::PackageResolverRequest::ResolveWithContext {
242                package_url,
243                context,
244                dir,
245                responder,
246            } => {
247                // We don't need to worry about validating context because it should have
248                // been produced by Resolve call above.
249                let result = if let Err(err) = validate_hermetic_url(
250                    &package_url,
251                    logger.clone(),
252                    &hermetic_test_package_name,
253                    &other_allowed_packages,
254                )
255                .await
256                {
257                    Err(err)
258                } else {
259                    let logger = logger.clone();
260                    pkg_resolver
261                        .resolve_with_context(&package_url, &context, dir)
262                        .await
263                        .unwrap_or_else(|err| {
264                            let mut builder = log::Record::builder();
265                            builder.level(log::Level::Warn);
266                            logger.log(
267                                &builder
268                                    .args(format_args!(
269                                        "failed to resolve pkg {} with context {:?}: {:?}",
270                                        package_url, context, err
271                                    ))
272                                    .build(),
273                            );
274                            Err(fpkg::ResolveError::Internal)
275                        })
276                };
277                let result_ref = result.as_ref();
278                let result_ref = result_ref.map_err(|e| e.to_owned());
279                if let Err(e) = responder.send(result_ref) {
280                    warn!("Failed sending load response for {}: {}", package_url, e);
281                }
282            }
283            fpkg::PackageResolverRequest::GetHash { package_url, responder } => {
284                let result = if let Err(_err) = validate_hermetic_url(
285                    package_url.url.as_str(),
286                    logger.clone(),
287                    &hermetic_test_package_name,
288                    &other_allowed_packages,
289                )
290                .await
291                {
292                    Err(zx::Status::INTERNAL.into_raw())
293                } else {
294                    let logger = logger.clone();
295                    pkg_resolver.get_hash(&package_url).await.unwrap_or_else(|err| {
296                        let mut builder = log::Record::builder();
297                        builder.level(log::Level::Warn);
298                        logger.log(
299                            &builder
300                                .args(format_args!(
301                                    "failed to resolve pkg {}: {:?}",
302                                    package_url.url.as_str(),
303                                    err
304                                ))
305                                .build(),
306                        );
307                        Err(zx::Status::INTERNAL.into_raw())
308                    })
309                };
310                let result_ref = result.as_ref();
311                let result_ref = result_ref.map_err(|e| e.to_owned());
312                if let Err(e) = responder.send(result_ref) {
313                    warn!("Failed sending load response for {}: {}", package_url.url.as_str(), e);
314                }
315            }
316        }
317    }
318}
319
320struct NoOpLogger;
321
322impl log::Log for NoOpLogger {
323    fn enabled(&self, _metadata: &log::Metadata<'_>) -> bool {
324        false
325    }
326
327    fn log(&self, _record: &log::Record<'_>) {}
328
329    fn flush(&self) {}
330}
331
332pub async fn serve_hermetic_resolver(
333    handles: LocalComponentHandles,
334    hermetic_test_package_name: Arc<String>,
335    other_allowed_packages: AllowedPackages,
336    full_resolver: Arc<fresolution::ResolverProxy>,
337    pkg_resolver: Arc<fpkg::PackageResolverProxy>,
338) -> Result<(), Error> {
339    let mut fs = ServiceFs::new();
340    let mut resolver_tasks = vec![];
341    let mut pkg_resolver_tasks = vec![];
342    let log_client = handles.connect_to_named_protocol(flogger::LogSinkMarker::DEBUG_NAME)?;
343    let tags = ["test_resolver"];
344    let log_publisher = match flog::Publisher::new(
345        flog::PublisherOptions::default().tags(&tags).use_log_sink(log_client),
346    ) {
347        Ok(publisher) => Arc::new(publisher) as Arc<LogSubscriber>,
348        Err(e) => {
349            warn!("Error creating log publisher for resolver: {:?}", e);
350            Arc::new(NoOpLogger) as Arc<LogSubscriber>
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 = NoOpLogger;
453        let task = fasync::Task::local(async move {
454            serve_resolver(
455                stream,
456                Arc::new(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}