warg_server/policy/record/
authorization.rs1use 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#[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 keys: IndexSet<KeyID>,
29 #[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 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn with_superuser_key(mut self, key: KeyID) -> Self {
50 self.superuser_keys.insert(key);
51 self
52 }
53
54 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 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 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 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}