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 license fields are the same as in [`RegistryFilter`]. Both are
14/// separate structs 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    /// Minimum age in seconds of a crates.io version before the proxy serves
40    /// it (0 = disabled), counted from the `pubtime` of its index entry.
41    /// Younger versions are left out of the index and their downloads are
42    /// denied. Entries without a `pubtime` are served. Turn on
43    /// `proxy.remove_deleted_versions` too, else a version that crates.io
44    /// deletes while it is held back is served once it is old enough
45    #[arg(long = "cratesio-filter-min-release-age")]
46    pub min_release_age_seconds: u64,
47
48    /// Crates (comma-separated) whose versions are served regardless of the
49    /// minimum release age, e.g. to let an urgent security fix through
50    #[configurable(env_list)]
51    #[arg(value_delimiter = ',')]
52    pub min_release_age_exempt_crates: Vec<String>,
53}
54
55/// Filters for downloads of crates hosted in Kellnr.
56///
57/// The fields are the license fields of [`CratesioFilter`].
58#[derive(
59    Debug, Default, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs, ToSchema,
60)]
61#[serde(default)]
62#[configurable(clap_prefix = "registry-filter")]
63pub struct RegistryFilter {
64    /// Deny downloads of crates whose license is not allowed
65    pub license_enabled: bool,
66
67    /// Allowed SPDX license identifiers (comma-separated), e.g. "MIT,Apache-2.0"
68    #[configurable(env_list)]
69    #[arg(value_delimiter = ',')]
70    pub license_allowed: Vec<String>,
71
72    /// Decision for crates without a known or valid license: "deny" or "allow".
73    /// A license that is no valid SPDX expression counts as unknown
74    #[arg(value_enum)]
75    pub license_unknown: LicenseUnknown,
76
77    /// Crates (comma-separated) that are allowed regardless of their license
78    #[configurable(env_list)]
79    #[arg(value_delimiter = ',')]
80    pub license_exempt_crates: Vec<String>,
81}
82
83/// What the license filter does with a crate whose license is unknown: none
84/// is declared, the expression does not parse, or it could not be fetched.
85#[derive(Debug, PartialEq, Eq, Clone, Copy, ValueEnum, Default, ToSchema)]
86#[schema(rename_all = "lowercase")]
87pub enum LicenseUnknown {
88    #[default]
89    Deny,
90    Allow,
91}
92
93impl<'de> Deserialize<'de> for LicenseUnknown {
94    fn deserialize<D>(de: D) -> Result<Self, D::Error>
95    where
96        D: Deserializer<'de>,
97    {
98        match String::deserialize(de)?.to_lowercase().as_str() {
99            "deny" => Ok(Self::Deny),
100            "allow" => Ok(Self::Allow),
101            other => Err(serde::de::Error::custom(format!(
102                "unknown license_unknown value {other:?}; expected \"deny\" or \"allow\""
103            ))),
104        }
105    }
106}
107
108impl Serialize for LicenseUnknown {
109    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
110    where
111        S: serde::Serializer,
112    {
113        serializer.serialize_str(&self.to_string())
114    }
115}
116
117impl Display for LicenseUnknown {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        match self {
120            LicenseUnknown::Deny => write!(f, "deny"),
121            LicenseUnknown::Allow => write!(f, "allow"),
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use provcfg::Config;
129
130    use crate::settings::SettingsProv;
131    use crate::{CratesioFilter, LicenseUnknown, RegistryFilter, Settings};
132
133    fn from_toml(toml: &str) -> Result<Settings, provcfg::Error> {
134        Config::new()
135            .add_toml_str("test.toml", toml)
136            .build::<SettingsProv>()
137            .map(|prov| Settings::from(&prov))
138    }
139
140    #[test]
141    fn filters_are_disabled_by_default() {
142        let settings = Settings::default();
143
144        assert_eq!(CratesioFilter::default(), settings.cratesio_filter);
145        assert_eq!(RegistryFilter::default(), settings.registry_filter);
146        assert!(!settings.cratesio_filter.license_enabled);
147        assert!(settings.cratesio_filter.license_allowed.is_empty());
148        assert_eq!(
149            LicenseUnknown::Deny,
150            settings.cratesio_filter.license_unknown
151        );
152        assert!(settings.cratesio_filter.license_exempt_crates.is_empty());
153        assert_eq!(0, settings.cratesio_filter.min_release_age_seconds);
154        assert!(
155            settings
156                .cratesio_filter
157                .min_release_age_exempt_crates
158                .is_empty()
159        );
160    }
161
162    #[test]
163    fn both_sections_read_from_toml() {
164        let settings = from_toml(
165            r#"
166            [cratesio_filter]
167            license_enabled = true
168            license_allowed = ["MIT", "Apache-2.0"]
169            license_unknown = "allow"
170            license_exempt_crates = ["ring"]
171            min_release_age_seconds = 259200
172            min_release_age_exempt_crates = ["openssl", "tokio-util"]
173
174            [registry_filter]
175            license_enabled = true
176            license_allowed = ["MIT"]
177            "#,
178        )
179        .unwrap();
180
181        assert_eq!(
182            CratesioFilter {
183                license_enabled: true,
184                license_allowed: vec!["MIT".to_string(), "Apache-2.0".to_string()],
185                license_unknown: LicenseUnknown::Allow,
186                license_exempt_crates: vec!["ring".to_string()],
187                min_release_age_seconds: 259_200,
188                min_release_age_exempt_crates: vec![
189                    "openssl".to_string(),
190                    "tokio-util".to_string()
191                ],
192            },
193            settings.cratesio_filter
194        );
195        assert_eq!(
196            RegistryFilter {
197                license_enabled: true,
198                license_allowed: vec!["MIT".to_string()],
199                license_unknown: LicenseUnknown::Deny,
200                license_exempt_crates: vec![],
201            },
202            settings.registry_filter
203        );
204    }
205
206    #[test]
207    fn invalid_license_unknown_fails() {
208        let result = from_toml("[cratesio_filter]\nlicense_unknown = \"deneay\"\n");
209
210        assert!(result.is_err());
211    }
212
213    #[test]
214    fn list_settings_are_env_list_keys() {
215        let keys = crate::settings::env_list_keys();
216
217        for key in [
218            "cratesio_filter.license_allowed",
219            "cratesio_filter.license_exempt_crates",
220            "cratesio_filter.min_release_age_exempt_crates",
221            "registry_filter.license_allowed",
222            "registry_filter.license_exempt_crates",
223        ] {
224            assert!(keys.contains(&key), "{key} must be an env list key");
225        }
226    }
227}