Skip to main content

kellnr_settings/
filter.rs

1// The field names are the settings keys. The prefix leaves room for other filters.
2#![allow(clippy::struct_field_names)]
3
4use std::fmt::Display;
5
6use clap::ValueEnum;
7use provcfg::{ClapArgs, Configurable};
8use serde::{Deserialize, Deserializer, Serialize};
9use utoipa::ToSchema;
10
11/// Filters for downloads through the crates.io proxy.
12///
13/// The fields are the same as in [`RegistryFilter`]. Both are separate structs
14/// because the CLI flag prefix is set per struct.
15#[derive(
16    Debug, Default, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs, ToSchema,
17)]
18#[serde(default)]
19#[configurable(clap_prefix = "cratesio-filter")]
20pub struct CratesioFilter {
21    /// Deny downloads of crates whose license is not allowed
22    pub license_enabled: bool,
23
24    /// Allowed SPDX license identifiers (comma-separated), e.g. "MIT,Apache-2.0"
25    #[configurable(env_list)]
26    #[arg(value_delimiter = ',')]
27    pub license_allowed: Vec<String>,
28
29    /// Decision for crates without a known or valid license: "deny" or "allow".
30    /// A license that is no valid SPDX expression counts as unknown
31    #[arg(value_enum)]
32    pub license_unknown: LicenseUnknown,
33
34    /// Crates (comma-separated) that are allowed regardless of their license
35    #[configurable(env_list)]
36    #[arg(value_delimiter = ',')]
37    pub license_exempt_crates: Vec<String>,
38}
39
40/// Filters for downloads of crates hosted in Kellnr.
41///
42/// The fields are the same as in [`CratesioFilter`].
43#[derive(
44    Debug, Default, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs, ToSchema,
45)]
46#[serde(default)]
47#[configurable(clap_prefix = "registry-filter")]
48pub struct RegistryFilter {
49    /// Deny downloads of crates whose license is not allowed
50    pub license_enabled: bool,
51
52    /// Allowed SPDX license identifiers (comma-separated), e.g. "MIT,Apache-2.0"
53    #[configurable(env_list)]
54    #[arg(value_delimiter = ',')]
55    pub license_allowed: Vec<String>,
56
57    /// Decision for crates without a known or valid license: "deny" or "allow".
58    /// A license that is no valid SPDX expression counts as unknown
59    #[arg(value_enum)]
60    pub license_unknown: LicenseUnknown,
61
62    /// Crates (comma-separated) that are allowed regardless of their license
63    #[configurable(env_list)]
64    #[arg(value_delimiter = ',')]
65    pub license_exempt_crates: Vec<String>,
66}
67
68/// What the license filter does with a crate whose license is unknown: none
69/// is declared, the expression does not parse, or it could not be fetched.
70#[derive(Debug, PartialEq, Eq, Clone, Copy, ValueEnum, Default, ToSchema)]
71#[schema(rename_all = "lowercase")]
72pub enum LicenseUnknown {
73    #[default]
74    Deny,
75    Allow,
76}
77
78impl<'de> Deserialize<'de> for LicenseUnknown {
79    fn deserialize<D>(de: D) -> Result<Self, D::Error>
80    where
81        D: Deserializer<'de>,
82    {
83        match String::deserialize(de)?.to_lowercase().as_str() {
84            "deny" => Ok(Self::Deny),
85            "allow" => Ok(Self::Allow),
86            other => Err(serde::de::Error::custom(format!(
87                "unknown license_unknown value {other:?}; expected \"deny\" or \"allow\""
88            ))),
89        }
90    }
91}
92
93impl Serialize for LicenseUnknown {
94    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
95    where
96        S: serde::Serializer,
97    {
98        serializer.serialize_str(&self.to_string())
99    }
100}
101
102impl Display for LicenseUnknown {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match self {
105            LicenseUnknown::Deny => write!(f, "deny"),
106            LicenseUnknown::Allow => write!(f, "allow"),
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use provcfg::Config;
114
115    use crate::settings::SettingsProv;
116    use crate::{CratesioFilter, LicenseUnknown, RegistryFilter, Settings};
117
118    fn from_toml(toml: &str) -> Result<Settings, provcfg::Error> {
119        Config::new()
120            .add_toml_str("test.toml", toml)
121            .build::<SettingsProv>()
122            .map(|prov| Settings::from(&prov))
123    }
124
125    #[test]
126    fn filters_are_disabled_by_default() {
127        let settings = Settings::default();
128
129        assert_eq!(CratesioFilter::default(), settings.cratesio_filter);
130        assert_eq!(RegistryFilter::default(), settings.registry_filter);
131        assert!(!settings.cratesio_filter.license_enabled);
132        assert!(settings.cratesio_filter.license_allowed.is_empty());
133        assert_eq!(
134            LicenseUnknown::Deny,
135            settings.cratesio_filter.license_unknown
136        );
137        assert!(settings.cratesio_filter.license_exempt_crates.is_empty());
138    }
139
140    #[test]
141    fn both_sections_read_from_toml() {
142        let settings = from_toml(
143            r#"
144            [cratesio_filter]
145            license_enabled = true
146            license_allowed = ["MIT", "Apache-2.0"]
147            license_unknown = "allow"
148            license_exempt_crates = ["ring"]
149
150            [registry_filter]
151            license_enabled = true
152            license_allowed = ["MIT"]
153            "#,
154        )
155        .unwrap();
156
157        assert_eq!(
158            CratesioFilter {
159                license_enabled: true,
160                license_allowed: vec!["MIT".to_string(), "Apache-2.0".to_string()],
161                license_unknown: LicenseUnknown::Allow,
162                license_exempt_crates: vec!["ring".to_string()],
163            },
164            settings.cratesio_filter
165        );
166        assert_eq!(
167            RegistryFilter {
168                license_enabled: true,
169                license_allowed: vec!["MIT".to_string()],
170                license_unknown: LicenseUnknown::Deny,
171                license_exempt_crates: vec![],
172            },
173            settings.registry_filter
174        );
175    }
176
177    #[test]
178    fn invalid_license_unknown_fails() {
179        let result = from_toml("[cratesio_filter]\nlicense_unknown = \"deneay\"\n");
180
181        assert!(result.is_err());
182    }
183
184    #[test]
185    fn list_settings_are_env_list_keys() {
186        let keys = crate::settings::env_list_keys();
187
188        for key in [
189            "cratesio_filter.license_allowed",
190            "cratesio_filter.license_exempt_crates",
191            "registry_filter.license_allowed",
192            "registry_filter.license_exempt_crates",
193        ] {
194            assert!(keys.contains(&key), "{key} must be an env list key");
195        }
196    }
197}