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_proxy = handles
343        .connect_to_named_protocol::<flogger::LogSinkMarker>(flogger::LogSinkMarker::DEBUG_NAME)?;
344    let tags = ["test_resolver"];
345    let log_publisher = match flog::Publisher::new(
346        flog::PublisherOptions::default().tags(&tags).use_log_sink(log_proxy),
347    ) {
348        Ok(publisher) => Arc::new(publisher) as Arc<LogSubscriber>,
349        Err(e) => {
350            warn!("Error creating log publisher for resolver: {:?}", e);
351            Arc::new(NoOpLogger) as Arc<LogSubscriber>
352        }
353    };
354
355    let resolver_hermetic_test_package_name = hermetic_test_package_name.clone();
356    let resolver_other_allowed_packages = other_allowed_packages.clone();
357    let resolver_log_publisher = log_publisher.clone();
358
359    let pkg_resolver_hermetic_test_package_name = hermetic_test_package_name.clone();
360    let pkg_resolver_other_allowed_packages = other_allowed_packages.clone();
361    let pkg_resolver_log_publisher = log_publisher.clone();
362
363    fs.dir("svc").add_fidl_service(move |stream: fresolution::ResolverRequestStream| {
364        let full_resolver = full_resolver.clone();
365        let hermetic_test_package_name = resolver_hermetic_test_package_name.clone();
366        let other_allowed_packages = resolver_other_allowed_packages.clone();
367        let log_publisher = resolver_log_publisher.clone();
368        resolver_tasks.push(fasync::Task::local(async move {
369            serve_resolver(
370                stream,
371                log_publisher,
372                hermetic_test_package_name,
373                other_allowed_packages,
374                full_resolver,
375            )
376            .await;
377        }));
378    });
379    fs.dir("svc").add_fidl_service(move |stream: fpkg::PackageResolverRequestStream| {
380        let pkg_resolver = pkg_resolver.clone();
381        let hermetic_test_package_name = pkg_resolver_hermetic_test_package_name.clone();
382        let other_allowed_packages = pkg_resolver_other_allowed_packages.clone();
383        let log_publisher = pkg_resolver_log_publisher.clone();
384        pkg_resolver_tasks.push(fasync::Task::local(async move {
385            serve_pkg_resolver(
386                stream,
387                log_publisher,
388                hermetic_test_package_name,
389                other_allowed_packages,
390                pkg_resolver,
391            )
392            .await;
393        }));
394    });
395    fs.serve_connection(handles.outgoing_dir)?;
396    fs.collect::<()>().await;
397    Ok(())
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use fidl::endpoints::create_proxy_and_stream;
404    use maplit::hashset;
405
406    async fn respond_to_resolve_requests(mut stream: fresolution::ResolverRequestStream) {
407        while let Some(request) =
408            stream.try_next().await.expect("failed to serve component mock resolver")
409        {
410            match request {
411                fresolution::ResolverRequest::Resolve { component_url, responder } => {
412                    match component_url.as_str() {
413                        "fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm"
414                        | "fuchsia-pkg://fuchsia.com/package-three#meta/comp.cm"
415                        | "fuchsia-pkg://fuchsia.com/package-four#meta/comp.cm" => {
416                            responder.send(Ok(fresolution::Component::default()))
417                        }
418                        "fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm" => {
419                            responder.send(Err(fresolution::ResolverError::ResourceUnavailable))
420                        }
421                        _ => responder.send(Err(fresolution::ResolverError::Internal)),
422                    }
423                    .expect("failed sending response");
424                }
425                fresolution::ResolverRequest::ResolveWithContext {
426                    component_url,
427                    context: _,
428                    responder,
429                } => {
430                    match component_url.as_str() {
431                        "fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm" | "name#resource" => {
432                            responder.send(Ok(fresolution::Component::default()))
433                        }
434                        _ => responder.send(Err(fresolution::ResolverError::PackageNotFound)),
435                    }
436                    .expect("failed sending response");
437                }
438                fresolution::ResolverRequest::_UnknownMethod { .. } => {
439                    panic!("Unknown Resolver request");
440                }
441            }
442        }
443    }
444
445    // Run hermetic resolver
446    fn run_resolver(
447        hermetic_test_package_name: Arc<String>,
448        other_allowed_packages: AllowedPackages,
449        mock_full_resolver: Arc<fresolution::ResolverProxy>,
450    ) -> (fasync::Task<()>, fresolution::ResolverProxy) {
451        let (proxy, stream) =
452            fidl::endpoints::create_proxy_and_stream::<fresolution::ResolverMarker>();
453        let logger = NoOpLogger;
454        let task = fasync::Task::local(async move {
455            serve_resolver(
456                stream,
457                Arc::new(logger),
458                hermetic_test_package_name,
459                other_allowed_packages,
460                mock_full_resolver,
461            )
462            .await;
463        });
464        (task, proxy)
465    }
466
467    #[fuchsia::test]
468    async fn test_successful_resolve() {
469        let pkg_name = "package-one".to_string();
470
471        let (resolver_proxy, resolver_request_stream) =
472            create_proxy_and_stream::<fresolution::ResolverMarker>();
473        let _full_resolver_task = fasync::Task::spawn(async move {
474            respond_to_resolve_requests(resolver_request_stream).await;
475        });
476
477        let (_task, hermetic_resolver_proxy) = run_resolver(
478            pkg_name.into(),
479            AllowedPackages::zero_allowed_pkgs(),
480            Arc::new(resolver_proxy),
481        );
482
483        assert_eq!(
484            hermetic_resolver_proxy
485                .resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
486                .await
487                .unwrap(),
488            Ok(fresolution::Component::default())
489        );
490        let mock_context = fresolution::Context { bytes: vec![0] };
491        assert_eq!(
492            hermetic_resolver_proxy
493                .resolve_with_context("name#resource", &mock_context)
494                .await
495                .unwrap(),
496            Ok(fresolution::Component::default())
497        );
498        assert_eq!(
499            hermetic_resolver_proxy
500                .resolve_with_context("name#not_found", &mock_context)
501                .await
502                .unwrap(),
503            Err(fresolution::ResolverError::PackageNotFound)
504        );
505        assert_eq!(
506            hermetic_resolver_proxy
507                .resolve_with_context(
508                    "fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm",
509                    &mock_context
510                )
511                .await
512                .unwrap(),
513            Ok(fresolution::Component::default())
514        );
515    }
516
517    #[fuchsia::test]
518    async fn drop_connection_on_resolve() {
519        let pkg_name = "package-one".to_string();
520
521        let (resolver_proxy, resolver_request_stream) =
522            create_proxy_and_stream::<fresolution::ResolverMarker>();
523        let _full_resolver_task = fasync::Task::spawn(async move {
524            respond_to_resolve_requests(resolver_request_stream).await;
525        });
526
527        let (_task, hermetic_resolver_proxy) = run_resolver(
528            pkg_name.into(),
529            AllowedPackages::zero_allowed_pkgs(),
530            Arc::new(resolver_proxy),
531        );
532
533        let _ =
534            hermetic_resolver_proxy.resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm");
535        drop(hermetic_resolver_proxy); // code should not crash
536    }
537
538    #[fuchsia::test]
539    async fn test_package_not_allowed() {
540        let (resolver_proxy, _) = create_proxy_and_stream::<fresolution::ResolverMarker>();
541
542        let (_task, hermetic_resolver_proxy) = run_resolver(
543            "package-two".to_string().into(),
544            AllowedPackages::zero_allowed_pkgs(),
545            Arc::new(resolver_proxy),
546        );
547
548        assert_eq!(
549            hermetic_resolver_proxy
550                .resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
551                .await
552                .unwrap(),
553            Err(fresolution::ResolverError::PackageNotFound)
554        );
555        let mock_context = fresolution::Context { bytes: vec![0] };
556        assert_eq!(
557            hermetic_resolver_proxy
558                .resolve_with_context(
559                    "fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm",
560                    &mock_context
561                )
562                .await
563                .unwrap(),
564            Err(fresolution::ResolverError::PackageNotFound)
565        );
566    }
567
568    #[fuchsia::test]
569    async fn other_packages_allowed() {
570        let (resolver_proxy, resolver_request_stream) =
571            create_proxy_and_stream::<fresolution::ResolverMarker>();
572
573        let list = hashset!("package-three".to_string(), "package-four".to_string());
574
575        let _full_resolver_task = fasync::Task::spawn(async move {
576            respond_to_resolve_requests(resolver_request_stream).await;
577        });
578
579        let (_task, hermetic_resolver_proxy) = run_resolver(
580            "package-two".to_string().into(),
581            AllowedPackages::from_iter(list),
582            Arc::new(resolver_proxy),
583        );
584
585        assert_eq!(
586            hermetic_resolver_proxy
587                .resolve("fuchsia-pkg://fuchsia.com/package-one#meta/comp.cm")
588                .await
589                .unwrap(),
590            Err(fresolution::ResolverError::PackageNotFound)
591        );
592
593        assert_eq!(
594            hermetic_resolver_proxy
595                .resolve("fuchsia-pkg://fuchsia.com/package-three#meta/comp.cm")
596                .await
597                .unwrap(),
598            Ok(fresolution::Component::default())
599        );
600
601        assert_eq!(
602            hermetic_resolver_proxy
603                .resolve("fuchsia-pkg://fuchsia.com/package-four#meta/comp.cm")
604                .await
605                .unwrap(),
606            Ok(fresolution::Component::default())
607        );
608
609        assert_eq!(
610            hermetic_resolver_proxy
611                .resolve("fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm")
612                .await
613                .unwrap(),
614            // we return this error from our mock resolver for package-two.
615            Err(fresolution::ResolverError::ResourceUnavailable)
616        );
617    }
618
619    #[fuchsia::test]
620    async fn test_failed_resolve() {
621        let (resolver_proxy, resolver_request_stream) =
622            create_proxy_and_stream::<fresolution::ResolverMarker>();
623        let _full_resolver_task = fasync::Task::spawn(async move {
624            respond_to_resolve_requests(resolver_request_stream).await;
625        });
626
627        let pkg_name = "package-two".to_string();
628        let (_task, hermetic_resolver_proxy) = run_resolver(
629            pkg_name.into(),
630            AllowedPackages::zero_allowed_pkgs(),
631            Arc::new(resolver_proxy),
632        );
633
634        assert_eq!(
635            hermetic_resolver_proxy
636                .resolve("fuchsia-pkg://fuchsia.com/package-two#meta/comp.cm")
637                .await
638                .unwrap(),
639            Err(fresolution::ResolverError::ResourceUnavailable)
640        );
641    }
642
643    #[fuchsia::test]
644    async fn test_invalid_url() {
645        let (resolver_proxy, _) = create_proxy_and_stream::<fresolution::ResolverMarker>();
646
647        let pkg_name = "package-two".to_string();
648        let (_task, hermetic_resolver_proxy) = run_resolver(
649            pkg_name.into(),
650            AllowedPackages::zero_allowed_pkgs(),
651            Arc::new(resolver_proxy),
652        );
653
654        assert_eq!(
655            hermetic_resolver_proxy.resolve("invalid_url").await.unwrap(),
656            Err(fresolution::ResolverError::InvalidArgs)
657        );
658    }
659}