warg_server/policy/record/
authorization.rs

1use super::{RecordPolicy, RecordPolicyError, RecordPolicyResult};
2use anyhow::{bail, Result};
3use indexmap::{IndexMap, IndexSet};
4use serde::Deserialize;
5use warg_crypto::signing::KeyID;
6use warg_protocol::{
7    package::{PackageEntry, PackageRecord},
8    registry::PackageName,
9    ProtoEnvelope,
10};
11
12/// A policy that ensures a published record is signed by an authorized key.
13#[derive(Default, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct AuthorizedKeyPolicy {
16    #[serde(skip)]
17    superuser_keys: IndexSet<KeyID>,
18    #[serde(default, rename = "namespace")]
19    namespaces: IndexMap<String, LogPolicy>,
20    #[serde(default, rename = "package")]
21    packages: IndexMap<PackageName, LogPolicy>,
22}
23
24#[derive(Default, Deserialize)]
25#[serde(deny_unknown_fields)]
26struct LogPolicy {
27    // Authorized key IDs
28    keys: IndexSet<KeyID>,
29    // If true, permission grants are permitted.
30    #[serde(default)]
31    delegation: bool,
32}
33
34impl LogPolicy {
35    fn key_authorized_for_entry(&self, key: &KeyID, is_init: bool) -> bool {
36        (self.delegation && !is_init) || self.keys.contains(key)
37    }
38}
39
40impl AuthorizedKeyPolicy {
41    /// Creates a new authorized key policy.
42    ///
43    /// By default, no keys are authorized.
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Sets an authorized key for publishing to any namespace.
49    pub fn with_superuser_key(mut self, key: KeyID) -> Self {
50        self.superuser_keys.insert(key);
51        self
52    }
53
54    /// Sets an authorized key for publishing to a particular namespace.
55    pub fn with_namespace_key(mut self, namespace: impl Into<String>, key: KeyID) -> Result<Self> {
56        self.namespace_or_default_mut(namespace)?.keys.insert(key);
57        Ok(self)
58    }
59
60    /// Enables delegation for a particular namespace.
61    pub fn with_namespace_delegation(mut self, namespace: impl Into<String>) -> Result<Self> {
62        self.namespace_or_default_mut(namespace)?.delegation = true;
63        Ok(self)
64    }
65
66    fn namespace_or_default_mut(&mut self, namespace: impl Into<String>) -> Result<&mut LogPolicy> {
67        let namespace = namespace.into();
68        if !PackageName::is_valid_namespace(&namespace) {
69            bail!("namespace `{namespace}` is not a valid kebab-cased string");
70        }
71
72        Ok(self.namespaces.entry(namespace).or_default())
73    }
74
75    /// Sets an authorized key for publishing to a particular package.
76    pub fn with_package_key(mut self, package_name: impl Into<String>, key: KeyID) -> Result<Self> {
77        self.package_or_default_mut(package_name)?.keys.insert(key);
78        Ok(self)
79    }
80
81    /// Enables delegation for a particular package.
82    pub fn with_package_delegation(mut self, package_name: impl Into<String>) -> Result<Self> {
83        self.package_or_default_mut(package_name)?.delegation = true;
84        Ok(self)
85    }
86
87    fn package_or_default_mut(
88        &mut self,
89        package_name: impl Into<String>,
90    ) -> Result<&mut LogPolicy> {
91        let package_name = PackageName::new(package_name)?;
92        Ok(self.packages.entry(package_name).or_default())
93    }
94
95    pub fn key_authorized_for_entry(
96        &self,
97        key: &KeyID,
98        package: &PackageName,
99        is_init: bool,
100    ) -> bool {
101        if self.superuser_keys.contains(key) {
102            return true;
103        }
104
105        if let Some(policy) = self.namespaces.get(package.namespace()) {
106            if policy.key_authorized_for_entry(key, is_init) {
107                return true;
108            }
109        }
110
111        if let Some(policy) = self.packages.get(package) {
112            if policy.key_authorized_for_entry(key, is_init) {
113                return true;
114            }
115        }
116
117        false
118    }
119}
120
121impl RecordPolicy for AuthorizedKeyPolicy {
122    fn check(
123        &self,
124        name: &PackageName,
125        record: &ProtoEnvelope<PackageRecord>,
126    ) -> RecordPolicyResult<()> {
127        let key = record.key_id();
128        for entry in &record.as_ref().entries {
129            let is_init = matches!(entry, PackageEntry::Init { .. });
130            if !self.key_authorized_for_entry(key, name, is_init) {
131                return Err(RecordPolicyError::Unauthorized(format!(
132                    "key id `{key}` is not authorized to publish to package `{name}`",
133                )));
134            }
135        }
136        Ok(())
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_key_authorized_for_package() -> Result<()> {
146        let super_key = KeyID::from("super-key".to_string());
147        let namespace_key = KeyID::from("namespace-key".to_string());
148        let package_key = KeyID::from("package-key".to_string());
149        let other_key = KeyID::from("other-key".to_string());
150
151        let policy = AuthorizedKeyPolicy::new()
152            .with_superuser_key(super_key.clone())
153            .with_namespace_key("my-namespace", namespace_key.clone())?
154            .with_package_key("my-namespace:my-package", package_key.clone())?;
155
156        let my_package: PackageName = "my-namespace:my-package".parse()?;
157        let my_ns_other_package: PackageName = "my-namespace:other-package".parse()?;
158        let other_namespace: PackageName = "other-namespace:any-package".parse()?;
159
160        assert!(policy.key_authorized_for_entry(&super_key, &my_package, false));
161        assert!(policy.key_authorized_for_entry(&super_key, &my_ns_other_package, false));
162        assert!(policy.key_authorized_for_entry(&super_key, &other_namespace, false));
163
164        assert!(policy.key_authorized_for_entry(&namespace_key, &my_package, false));
165        assert!(policy.key_authorized_for_entry(&namespace_key, &my_ns_other_package, false));
166        assert!(!policy.key_authorized_for_entry(&namespace_key, &other_namespace, false));
167
168        assert!(policy.key_authorized_for_entry(&package_key, &my_package, false));
169        assert!(!policy.key_authorized_for_entry(&package_key, &my_ns_other_package, false));
170        assert!(!policy.key_authorized_for_entry(&package_key, &other_namespace, false));
171
172        assert!(!policy.key_authorized_for_entry(&other_key, &my_package, false));
173        assert!(!policy.key_authorized_for_entry(&other_key, &my_ns_other_package, false));
174        assert!(!policy.key_authorized_for_entry(&other_key, &other_namespace, false));
175
176        Ok(())
177    }
178
179    #[test]
180    fn test_key_authorized_for_package_init() -> Result<()> {
181        let authed_key = KeyID::from("authed-key".to_string());
182        let other_key = KeyID::from("other-key".to_string());
183
184        let policy = AuthorizedKeyPolicy::new()
185            .with_namespace_key("ns1", authed_key.clone())?
186            .with_namespace_delegation("ns1")?
187            .with_package_key("ns2:pkg", authed_key.clone())?
188            .with_package_delegation("ns2:pkg")?;
189
190        let ns1_pkg: PackageName = "ns1:pkg".parse()?;
191        let ns2_pkg: PackageName = "ns2:pkg".parse()?;
192
193        assert!(policy.key_authorized_for_entry(&authed_key, &ns1_pkg, true));
194        assert!(policy.key_authorized_for_entry(&authed_key, &ns1_pkg, false));
195        assert!(policy.key_authorized_for_entry(&authed_key, &ns2_pkg, true));
196        assert!(policy.key_authorized_for_entry(&authed_key, &ns2_pkg, false));
197
198        assert!(!policy.key_authorized_for_entry(&other_key, &ns1_pkg, true));
199        assert!(policy.key_authorized_for_entry(&other_key, &ns1_pkg, false));
200        assert!(!policy.key_authorized_for_entry(&other_key, &ns2_pkg, true));
201        assert!(policy.key_authorized_for_entry(&other_key, &ns2_pkg, false));
202        Ok(())
203    }
204}