Skip to content

Commit

Permalink
feat: custom error types.
Browse files Browse the repository at this point in the history
Add custom error types for all errors generated in the library.

Signed-off-by: José Guilherme Vanz <[email protected]>
  • Loading branch information
jvanz committed Nov 23, 2023
1 parent 30e22b0 commit bb605e3
Show file tree
Hide file tree
Showing 15 changed files with 265 additions and 172 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ sigstore = { version = "0.7.2", default-features = false, features = [
"tuf",
"cached-client",
] }
thiserror = "1.0"
tracing = "0.1"
url = { version = "2.2", features = ["serde"] }
walkdir = "2"
Expand Down
69 changes: 69 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use thiserror::Error;

pub type Result<T> = std::result::Result<T, FetcherErrors>;

#[derive(Error, Debug)]
pub enum FetcherErrors {
#[error("{msg}: {source}")]
LoadCertificateError {
msg: String,
#[source]
source: reqwest::Error,
},
#[error("certificate {0} is not in PEM nor in DER encoding")]
LoadRawCertificateError(String),
#[error(transparent)]
UrlParserError(#[from] url::ParseError),
#[error("{0}")]
GithubUrlParserError(String),
#[error("invalid URL: {0}")]
InvalidURLError(String),
#[error("cannot retrieve path from uri: {0}")]
InvalidFilePathError(String),
#[error("invalid destination: {0}")]
InvalidDestinationError(String),
#[error("unknown scheme: {0}")]
UnknownSchemeError(String),
#[error("invalid wasm file")]
InvalidWasmFileError,
#[error("Not a valid configuration file: {0}")]
InvalidVerifyFileError(#[source] serde_yaml::Error),
#[error("{0}")]
InvalidVerifyFileConfigurationError(String),
#[error("multiple policies found with the same prefix: {0}")]
MultiplePoliciesFoundError(String),
#[error("{0}")]
BuildImmutableReferenceError(String),
#[error("{0}")]
ChecksumVerificationError(String),
#[error("{0}")]
ImageVerificationError(String),
#[error("Verification only works with OCI images: Not a valid oci image: {0}")]
InvalidOCIImageReferenceError(#[from] oci_distribution::ParseError),
#[error("Fail to interact with OCI registry: {0}")]
OCIRegistryError(#[from] oci_distribution::errors::OciDistributionError),
#[error("could not pull policy {0}: empty layers")]
EmptyLayersError(String),
#[error("Policy cannot be verified, local wasm file doesn't exist: {0}")]
MissingWasmFileError(String),
#[error("Cannot read certificate from file: {0}")]
CannotReadCertificateError(#[from] std::io::Error),
#[error("wasm module cannot be save to {0:?}: {1}")]
CannotWriteWasmModuleFile(String, #[source] std::io::Error),
#[error("cannot create public key: {0} ")]
CannotCreatePublicKeyError(#[source] sigstore::errors::SigstoreError),
#[error("could not build a cosign client: {0} ")]
CannotBuildCosignClientError(#[source] sigstore::errors::SigstoreError),
#[error(transparent)]
PolicyStoragePathError(#[from] std::path::StripPrefixError),
#[error("failed to get image trusted layers: {0}")]
FailedToFetchTrustedLayersError(#[from] sigstore::errors::SigstoreError),
#[error("failed to create the http client: {0}")]
FailedToCreateHttpClientError(#[from] reqwest::Error),
#[error(transparent)]
FailedToParseYamlDataError(#[from] serde_yaml::Error),
#[error("cannot read policy in local storage: {0}")]
FailedToReadPolicyInLocalStorageError(#[from] walkdir::Error),
#[error("{0}")]
OtherError(String),
}
5 changes: 2 additions & 3 deletions src/fetcher.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use anyhow::Result;
use async_trait::async_trait;
use url::Url;

use crate::sources::Certificate;
use crate::{errors, sources::Certificate};

#[derive(Clone, Debug, PartialEq)]
pub(crate) enum ClientProtocol {
Expand All @@ -22,5 +21,5 @@ pub(crate) enum TlsVerificationMode {
#[async_trait]
pub(crate) trait PolicyFetcher {
// Download and return the bytes of the WASM module
async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> Result<Vec<u8>>;
async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> errors::Result<Vec<u8>>;
}
33 changes: 24 additions & 9 deletions src/https.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,51 @@
#![allow(clippy::upper_case_acronyms)]

use anyhow::{anyhow, Result};
use async_trait::async_trait;
use std::{
boxed::Box,
convert::{TryFrom, TryInto},
};
use url::Url;

use crate::fetcher::{ClientProtocol, PolicyFetcher, TlsVerificationMode};
use crate::errors;
use crate::sources::Certificate;
use crate::{
errors::FetcherErrors,
fetcher::{ClientProtocol, PolicyFetcher, TlsVerificationMode},
};

// Struct used to reference a WASM module that is hosted on a HTTP(s) server
#[derive(Default)]
pub(crate) struct Https {}

impl TryFrom<&Certificate> for reqwest::Certificate {
type Error = anyhow::Error;
type Error = FetcherErrors;

fn try_from(certificate: &Certificate) -> Result<Self> {
fn try_from(certificate: &Certificate) -> errors::Result<Self> {
match certificate {
Certificate::Der(certificate) => reqwest::Certificate::from_der(certificate)
.map_err(|err| anyhow!("could not load certificate as DER encoded: {}", err)),
Certificate::Pem(certificate) => reqwest::Certificate::from_pem(certificate)
.map_err(|err| anyhow!("could not load certificate as PEM encoded: {}", err)),
Certificate::Der(certificate) => {
reqwest::Certificate::from_der(certificate).map_err(|err| {
FetcherErrors::LoadCertificateError {
msg: "could not load certificate as DER encoded".to_owned(),
source: err,
}
})
}
Certificate::Pem(certificate) => {
reqwest::Certificate::from_pem(certificate).map_err(|err| {
FetcherErrors::LoadCertificateError {
msg: "could not load certificate as PEM encoded".to_owned(),
source: err,
}
})
}
}
}
}

#[async_trait]
impl PolicyFetcher for Https {
async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> Result<Vec<u8>> {
async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> errors::Result<Vec<u8>> {
let mut client_builder = reqwest::Client::builder();
match client_protocol {
ClientProtocol::Http => {}
Expand Down
43 changes: 22 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ extern crate reqwest;
extern crate rustls;
extern crate walkdir;

use anyhow::{anyhow, Result};
use std::boxed::Box;
use std::fs;
use url::Url;

pub mod errors;
pub mod fetcher;
mod https;
pub mod policy;
Expand All @@ -16,6 +16,7 @@ pub mod sources;
pub mod store;
pub mod verify;

use crate::errors::FetcherErrors;
use crate::fetcher::{ClientProtocol, PolicyFetcher, TlsVerificationMode};
use crate::https::Https;
use crate::policy::Policy;
Expand Down Expand Up @@ -78,7 +79,7 @@ pub async fn fetch_policy(
url: &str,
destination: PullDestination,
sources: Option<&Sources>,
) -> Result<Policy> {
) -> errors::Result<Policy> {
let url = parse_url(url)?;
match url.scheme() {
"file" => {
Expand All @@ -87,11 +88,11 @@ pub async fn fetch_policy(
uri: url.to_string(),
local_path: url
.to_file_path()
.map_err(|err| anyhow!("cannot retrieve path from uri {}: {:?}", url, err))?,
.map_err(|_| FetcherErrors::InvalidFilePathError(url.to_string()))?,
});
}
"registry" | "http" | "https" => Ok(()),
_ => Err(anyhow!("unknown scheme: {}", url.scheme())),
_ => Err(FetcherErrors::UnknownSchemeError(url.scheme().to_owned())),
}?;
let (store, mut destination) = pull_destination(&url, &destination)?;
if let Some(store) = store {
Expand Down Expand Up @@ -134,11 +135,7 @@ pub async fn fetch_policy(
{
Err(err) => {
if !sources.is_insecure_source(&host_and_port(&url)?) {
return Err(anyhow!(
"the policy {} could not be downloaded due to error: {}",
url,
err
));
return Err(err);
}
}
Ok(bytes) => return create_file_if_valid(&bytes, &destination, url.to_string()),
Expand All @@ -155,11 +152,11 @@ pub async fn fetch_policy(

match policy_fetcher.fetch(&url, ClientProtocol::Http).await {
Ok(bytes) => create_file_if_valid(&bytes, &destination, url.to_string()),
Err(e) => Err(anyhow!("could not pull policy {}: {}", url, e)),
Err(e) => Err(e),
}
}

fn client_protocol(url: &Url, sources: &Sources) -> Result<ClientProtocol> {
fn client_protocol(url: &Url, sources: &Sources) -> errors::Result<ClientProtocol> {
if let Some(certificates) = sources.source_authority(&host_and_port(url)?) {
return Ok(ClientProtocol::Https(
TlsVerificationMode::CustomCaCertificates(certificates),
Expand All @@ -168,7 +165,10 @@ fn client_protocol(url: &Url, sources: &Sources) -> Result<ClientProtocol> {
Ok(ClientProtocol::Https(TlsVerificationMode::SystemCa))
}

fn pull_destination(url: &Url, destination: &PullDestination) -> Result<(Option<Store>, PathBuf)> {
fn pull_destination(
url: &Url,
destination: &PullDestination,
) -> errors::Result<(Option<Store>, PathBuf)> {
Ok(match destination {
PullDestination::MainStore => {
let store = Store::default();
Expand Down Expand Up @@ -196,19 +196,19 @@ fn pull_destination(url: &Url, destination: &PullDestination) -> Result<(Option<
// Helper function, takes the URL of the policy and allocates the
// right struct to interact with it
#[allow(clippy::box_default)]
fn url_fetcher(scheme: &str) -> Result<Box<dyn PolicyFetcher>> {
fn url_fetcher(scheme: &str) -> errors::Result<Box<dyn PolicyFetcher>> {
match scheme {
"http" | "https" => Ok(Box::new(Https::default())),
"registry" => Ok(Box::new(Registry::new())),
_ => Err(anyhow!("unknown scheme: {}", scheme)),
_ => Err(FetcherErrors::UnknownSchemeError(scheme.to_owned())),
}
}

pub(crate) fn host_and_port(url: &Url) -> Result<String> {
pub(crate) fn host_and_port(url: &Url) -> errors::Result<String> {
Ok(format!(
"{}{}",
url.host_str()
.ok_or_else(|| anyhow!("invalid URL {}", url))?,
.ok_or_else(|| FetcherErrors::InvalidURLError(url.to_string()))?,
url.port()
.map(|port| format!(":{}", port))
.unwrap_or_default(),
Expand All @@ -222,12 +222,13 @@ pub(crate) fn host_and_port(url: &Url) -> Result<String> {
// https://webassembly.github.io/spec/core/bikeshed/#binary-magic
const WASM_MAGIC_NUMBER: [u8; 4] = [0x00, 0x61, 0x73, 0x6D];

fn create_file_if_valid(bytes: &[u8], destination: &Path, url: String) -> Result<Policy> {
fn create_file_if_valid(bytes: &[u8], destination: &Path, url: String) -> errors::Result<Policy> {
if !bytes.starts_with(&WASM_MAGIC_NUMBER) {
return Err(anyhow!("invalid wasm file"));
return Err(FetcherErrors::InvalidWasmFileError);
};
fs::write(destination, bytes)
.map_err(|e| anyhow!("wasm module cannot be save to {:?}: {}", destination, e))?;
fs::write(destination, bytes).map_err(|e| {
FetcherErrors::CannotWriteWasmModuleFile(destination.to_string_lossy().to_string(), e)
})?;

Ok(Policy {
uri: url,
Expand Down Expand Up @@ -359,7 +360,7 @@ mod tests {
port: Some(5000),
path: "/kubewarden/policies/test".to_string(),
}))]
fn url_parsing(#[case] url: &str, #[case] expected: anyhow::Result<UrlParseDetails>) {
fn url_parsing(#[case] url: &str, #[case] expected: errors::Result<UrlParseDetails>) {
let res = parse_url(url);
println!("{} -> {:?}", url, res);
assert_eq!(res.is_ok(), expected.is_ok());
Expand Down
3 changes: 2 additions & 1 deletion src/policy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::errors;
use sha2::{Digest, Sha256};
use std::fmt;
use std::fmt::Display;
Expand All @@ -10,7 +11,7 @@ pub struct Policy {
}

impl Policy {
pub fn digest(&self) -> Result<String, std::io::Error> {
pub fn digest(&self) -> errors::Result<String> {
let d = Sha256::digest(std::fs::read(&self.local_path)?);
Ok(format!("{:x}", d))
}
Expand Down
Loading

0 comments on commit bb605e3

Please sign in to comment.