Skip to main content

running_process/broker/server/
version_allow_list.rs

1//! Version floor and allow-list enforcement for service definitions.
2
3use std::cmp::Ordering;
4
5use crate::broker::protocol::ServiceDefinition;
6
7/// Reason a requested service version is blocked.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum VersionPolicyBlock {
10    /// The requested version is below `ServiceDefinition.min_version`.
11    BelowMinVersion,
12    /// The service definition has a strict allow-list and the request is absent.
13    OutsideAllowList,
14}
15
16/// Check `wanted_version` against the service definition's frozen policy.
17pub fn check_version_allowed(
18    wanted_version: &str,
19    service: &ServiceDefinition,
20) -> Result<(), VersionPolicyBlock> {
21    if !service.min_version.is_empty()
22        && compare_semver_core(wanted_version, &service.min_version) == Some(Ordering::Less)
23    {
24        return Err(VersionPolicyBlock::BelowMinVersion);
25    }
26    if !service.version_allow_list.is_empty()
27        && !service
28            .version_allow_list
29            .iter()
30            .any(|allowed| allowed == wanted_version)
31    {
32        return Err(VersionPolicyBlock::OutsideAllowList);
33    }
34    Ok(())
35}
36
37fn compare_semver_core(left: &str, right: &str) -> Option<Ordering> {
38    let left = parse_semver_core(left)?;
39    let right = parse_semver_core(right)?;
40    Some(left.cmp(&right))
41}
42
43fn parse_semver_core(version: &str) -> Option<[u64; 3]> {
44    let core = version.split_once('-').map_or(version, |(core, _)| core);
45    let mut parts = core.split('.');
46    let major = parts.next()?.parse().ok()?;
47    let minor = parts.next()?.parse().ok()?;
48    let patch = parts.next()?.parse().ok()?;
49    if parts.next().is_some() {
50        return None;
51    }
52    Some([major, minor, patch])
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    fn service(min_version: &str, allow: &[&str]) -> ServiceDefinition {
60        ServiceDefinition {
61            service_name: "zccache".into(),
62            binary_path: "/usr/bin/zccache".into(),
63            isolation: 0,
64            explicit_instance: String::new(),
65            per_version_binary_dir: String::new(),
66            min_version: min_version.into(),
67            version_allow_list: allow.iter().map(|v| (*v).into()).collect(),
68            labels: Default::default(),
69        }
70    }
71
72    #[test]
73    fn blocks_version_below_floor() {
74        assert_eq!(
75            check_version_allowed("1.9.9", &service("1.10.0", &[])),
76            Err(VersionPolicyBlock::BelowMinVersion)
77        );
78    }
79
80    #[test]
81    fn allows_version_at_floor() {
82        assert_eq!(
83            check_version_allowed("1.10.0", &service("1.10.0", &[])),
84            Ok(())
85        );
86    }
87
88    #[test]
89    fn blocks_version_outside_allow_list() {
90        assert_eq!(
91            check_version_allowed("1.12.0", &service("1.10.0", &["1.11.20"])),
92            Err(VersionPolicyBlock::OutsideAllowList)
93        );
94    }
95
96    #[test]
97    fn allows_version_inside_allow_list() {
98        assert_eq!(
99            check_version_allowed("1.11.20", &service("1.10.0", &["1.11.20"])),
100            Ok(())
101        );
102    }
103}