| 83e0677b Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
use super::*; |
| 28fc23d7 Andy McCall ↶ |
9 |
use crate::identity::SecretKey; |
| 83e0677b Andy McCall ↶ |
10 |
use crate::policy::{Policy, Require, Rule}; |
| 11 |
use crate::protocol::SignedPolicy; |
| 12 |
|
| 13 |
|
| 14 |
const POLICY_LIFETIME_MS: u64 = 365 * 24 * 60 * 60 * 1000; |
| 15 |
|
| 28fc23d7 Andy McCall ↶ |
16 |
|
| 17 |
|
| 18 |
#[derive(Clone, Debug)] |
| 19 |
pub struct PolicyUpdate { |
| 20 |
pub policy: Policy, |
| 21 |
pub sigs: Vec<Signature>, |
| 22 |
pub applied: bool, |
| 23 |
} |
| 24 |
|
| 25 |
impl PolicyUpdate { |
| 26 |
pub fn signed(&self) -> SignedPolicy { |
| 75167672 Andy McCall |
27 |
SignedPolicy { |
| 28 |
policy: self.policy.clone(), |
| 29 |
sigs: self.sigs.clone(), |
| 30 |
} |
| 28fc23d7 Andy McCall ↶ |
31 |
} |
| 32 |
} |
| 33 |
|
| 83e0677b Andy McCall ↶ |
34 |
impl Repo { |
| 35 |
|
| 36 |
pub fn current_policy(&self) -> Result<Option<(ObjectRef, SignedPolicy)>> { |
| 75167672 Andy McCall |
37 |
let Some(r) = self.view()?.policy else { |
| 38 |
return Ok(None); |
| 39 |
}; |
| 83e0677b Andy McCall ↶ |
40 |
let policy = Policy::decode(&self.store.get_typed(&r, ObjectType::Policy)?)?; |
| 75167672 Andy McCall |
41 |
Ok(Some(( |
| 42 |
r, |
| 43 |
SignedPolicy { |
| 44 |
policy, |
| 45 |
sigs: self.store.signatures(&r)?, |
| 46 |
}, |
| 47 |
))) |
| 83e0677b Andy McCall ↶ |
48 |
} |
| 49 |
|
| 50 |
|
| 51 |
pub(crate) fn set_policy(&self, sp: &SignedPolicy, command: &str) -> Result<ObjectRef> { |
| 52 |
let r = self.store.put(ObjectType::Policy, &sp.policy.encode())?; |
| 53 |
for sig in &sp.sigs { |
| 54 |
self.store.put_signature(&r, sig)?; |
| 55 |
} |
| 56 |
for id in &sp.policy.identities { |
| 57 |
self.store.put_identity(id)?; |
| 58 |
} |
| 28fc23d7 Andy McCall ↶ |
59 |
let dir = self.oxy.join("policies"); |
| 60 |
fs::create_dir_all(&dir)?; |
| 75167672 Andy McCall |
61 |
write_atomic( |
| 62 |
&dir.join(format!("{:020}", sp.policy.serial)), |
| 63 |
r.to_hex().as_bytes(), |
| 64 |
)?; |
| 83e0677b Andy McCall ↶ |
65 |
let (op_ref, op) = self.head_op()?; |
| 66 |
if op.view.policy != Some(r) { |
| 67 |
let mut view = op.view.clone(); |
| 68 |
view.policy = Some(r); |
| 69 |
self.record_op(&[op_ref], command, view)?; |
| 70 |
} |
| 71 |
Ok(r) |
| 72 |
} |
| 73 |
|
| 28fc23d7 Andy McCall ↶ |
74 |
|
| 75 |
|
| 76 |
|
| 77 |
pub fn policy_history(&self) -> Result<Vec<SignedPolicy>> { |
| 78 |
let mut out: BTreeMap<u64, SignedPolicy> = BTreeMap::new(); |
| 79 |
if let Ok(rd) = fs::read_dir(self.oxy.join("policies")) { |
| 80 |
for e in rd.flatten() { |
| 75167672 Andy McCall |
81 |
let Ok(hex) = fs::read_to_string(e.path()) else { |
| 82 |
continue; |
| 83 |
}; |
| 84 |
let Some(r) = ObjectRef::from_hex(hex.trim()) else { |
| 85 |
continue; |
| 86 |
}; |
| 87 |
let Ok(bytes) = self.store.get_typed(&r, ObjectType::Policy) else { |
| 88 |
continue; |
| 89 |
}; |
| 28fc23d7 Andy McCall ↶ |
90 |
let policy = Policy::decode(&bytes)?; |
| 75167672 Andy McCall |
91 |
out.insert( |
| 92 |
policy.serial, |
| 93 |
SignedPolicy { |
| 94 |
policy, |
| 95 |
sigs: self.store.signatures(&r)?, |
| 96 |
}, |
| 97 |
); |
| 28fc23d7 Andy McCall ↶ |
98 |
} |
| 99 |
} |
| 100 |
if let Some((_, sp)) = self.current_policy()? { |
| 101 |
out.insert(sp.policy.serial, sp); |
| 102 |
} |
| 103 |
Ok(out.into_values().collect()) |
| 104 |
} |
| 105 |
|
| 106 |
|
| 107 |
|
| 108 |
|
| 75167672 Andy McCall |
109 |
pub(crate) fn adopt_policy_chain( |
| 110 |
&self, |
| 111 |
chain: &[SignedPolicy], |
| 112 |
newest: &SignedPolicy, |
| 113 |
command: &str, |
| 114 |
) -> std::result::Result<(), String> { |
| 115 |
let mut current = self |
| 116 |
.current_policy() |
| 117 |
.map_err(|e| e.to_string())? |
| 118 |
.map(|(_, sp)| sp.policy); |
| 28fc23d7 Andy McCall ↶ |
119 |
let mut steps: Vec<&SignedPolicy> = chain |
| 120 |
.iter() |
| 75167672 Andy McCall |
121 |
.filter(|sp| { |
| 122 |
current.as_ref().is_none_or(|c| sp.policy.serial > c.serial) |
| 123 |
&& sp.policy.serial < newest.policy.serial |
| 124 |
}) |
| 28fc23d7 Andy McCall ↶ |
125 |
.collect(); |
| 126 |
steps.sort_by_key(|sp| sp.policy.serial); |
| 127 |
steps.push(newest); |
| 128 |
for sp in steps { |
| 129 |
self.check_policy(sp, current.as_ref())?; |
| 75167672 Andy McCall |
130 |
self.set_policy( |
| 131 |
sp, |
| 132 |
&format!("{command} (policy serial {})", sp.policy.serial), |
| 133 |
) |
| 134 |
.map_err(|e| e.to_string())?; |
| 28fc23d7 Andy McCall ↶ |
135 |
current = Some(sp.policy.clone()); |
| 136 |
} |
| 137 |
Ok(()) |
| 138 |
} |
| 139 |
|
| 83e0677b Andy McCall ↶ |
140 |
|
| 141 |
|
| 75167672 Andy McCall |
142 |
pub fn check_policy( |
| 143 |
&self, |
| 144 |
sp: &SignedPolicy, |
| 145 |
trusted: Option<&Policy>, |
| 146 |
) -> std::result::Result<(), String> { |
| 83e0677b Andy McCall ↶ |
147 |
if sp.policy.repo != self.id().map_err(|e| e.to_string())? { |
| 148 |
return Err("policy is for a different repository".into()); |
| 149 |
} |
| 150 |
if let Some(t) = trusted { |
| 151 |
if sp.policy.serial <= t.serial { |
| 75167672 Andy McCall |
152 |
return Err(format!( |
| 153 |
"policy serial {} is not greater than the current {}", |
| 154 |
sp.policy.serial, t.serial |
| 155 |
)); |
| 83e0677b Andy McCall ↶ |
156 |
} |
| 157 |
} |
| 28fc23d7 Andy McCall ↶ |
158 |
match trusted { |
| 159 |
Some(t) => sp.policy.verify_signatures(&sp.sigs, t)?, |
| 160 |
None => vec![sp.policy.verify_bootstrap(&sp.sigs)?], |
| 161 |
}; |
| 83e0677b Andy McCall ↶ |
162 |
Ok(()) |
| 163 |
} |
| 164 |
|
| 28fc23d7 Andy McCall ↶ |
165 |
|
| 166 |
|
| 167 |
fn sign_policy(&self, policy: Policy, command: &str) -> Result<PolicyUpdate> { |
| 83e0677b Andy McCall ↶ |
168 |
let Some((key, sk)) = self.secret_key()? else { |
| 169 |
return fail("no secret key: run `oxy identity create` first"); |
| 170 |
}; |
| 28fc23d7 Andy McCall ↶ |
171 |
self.sign_policy_with(policy, &[(key, &sk)], command) |
| 172 |
} |
| 173 |
|
| 174 |
|
| 175 |
|
| 176 |
|
| 75167672 Andy McCall |
177 |
pub(crate) fn sign_policy_with( |
| 178 |
&self, |
| 179 |
policy: Policy, |
| 180 |
signers: &[(KeyId, &SecretKey)], |
| 181 |
command: &str, |
| 182 |
) -> Result<PolicyUpdate> { |
| 183 |
let trusted = self |
| 184 |
.current_policy()? |
| 185 |
.map(|(_, sp)| sp.policy) |
| 186 |
.unwrap_or_else(|| policy.clone()); |
| 187 |
if !signers |
| 188 |
.first() |
| 189 |
.is_some_and(|(k, _)| trusted.roots.contains(k)) |
| 190 |
{ |
| 28fc23d7 Andy McCall ↶ |
191 |
return fail("your key is not a root key of the current policy"); |
| 192 |
} |
| 193 |
let target = policy.hash(); |
| 75167672 Andy McCall |
194 |
let sigs = signers |
| 195 |
.iter() |
| 196 |
.map(|(k, sk)| sk.sign(Domain::Policy, k, &target)) |
| 197 |
.collect(); |
| 28fc23d7 Andy McCall ↶ |
198 |
self.gather(SignedPolicy { policy, sigs }, &trusted, command) |
| 199 |
} |
| 200 |
|
| 201 |
|
| 202 |
fn gather(&self, sp: SignedPolicy, trusted: &Policy, command: &str) -> Result<PolicyUpdate> { |
| 203 |
let applied = sp.policy.verify_signatures(&sp.sigs, trusted).is_ok(); |
| 204 |
if applied { |
| 205 |
self.set_policy(&sp, command)?; |
| 206 |
} |
| 75167672 Andy McCall |
207 |
Ok(PolicyUpdate { |
| 208 |
policy: sp.policy, |
| 209 |
sigs: sp.sigs, |
| 210 |
applied, |
| 211 |
}) |
| 28fc23d7 Andy McCall ↶ |
212 |
} |
| 213 |
|
| 214 |
|
| 215 |
|
| 216 |
pub fn policy_sign(&self, proposal: SignedPolicy) -> Result<PolicyUpdate> { |
| 217 |
let Some((key, sk)) = self.secret_key()? else { |
| 218 |
return fail("no secret key: run `oxy identity create` first"); |
| 219 |
}; |
| 220 |
let Some((_, current)) = self.current_policy()? else { |
| 221 |
return fail("this repository has no policy; run `oxy policy init` or fetch one"); |
| 222 |
}; |
| 223 |
if proposal.policy.repo != self.id()? { |
| 224 |
return fail("the proposal is for a different repository"); |
| 83e0677b Andy McCall ↶ |
225 |
} |
| 28fc23d7 Andy McCall ↶ |
226 |
if proposal.policy.serial <= current.policy.serial { |
| 75167672 Andy McCall |
227 |
return fail(format!( |
| 228 |
"the proposal's serial {} is not greater than the current {}", |
| 229 |
proposal.policy.serial, current.policy.serial |
| 230 |
)); |
| 83e0677b Andy McCall ↶ |
231 |
} |
| 28fc23d7 Andy McCall ↶ |
232 |
if !current.policy.roots.contains(&key) { |
| 233 |
return fail("your key is not a root key of the current policy"); |
| 234 |
} |
| 235 |
let mut sigs: Vec<Signature> = proposal.sigs.into_iter().filter(|s| s.key != key).collect(); |
| 236 |
sigs.push(sk.sign(Domain::Policy, &key, &proposal.policy.hash())); |
| 75167672 Andy McCall |
237 |
self.gather( |
| 238 |
SignedPolicy { |
| 239 |
policy: proposal.policy, |
| 240 |
sigs, |
| 241 |
}, |
| 242 |
¤t.policy, |
| 243 |
"oxy policy sign", |
| 244 |
) |
| 83e0677b Andy McCall ↶ |
245 |
} |
| 246 |
|
| 247 |
|
| 28fc23d7 Andy McCall ↶ |
248 |
pub fn policy_init(&self) -> Result<PolicyUpdate> { |
| 83e0677b Andy McCall ↶ |
249 |
if self.current_policy()?.is_some() { |
| 250 |
return fail("this repository already has a policy"); |
| 251 |
} |
| 252 |
let Some((key, _)) = self.secret_key()? else { |
| 253 |
return fail("no secret key: run `oxy identity create` first"); |
| 254 |
}; |
| 255 |
let identity = self.identity(&key)?; |
| 75167672 Andy McCall |
256 |
let policy = Policy::single_root( |
| 257 |
self.id()?, |
| 258 |
identity, |
| 259 |
Hlc::now().wall_ms + POLICY_LIFETIME_MS, |
| 260 |
); |
| 28fc23d7 Andy McCall ↶ |
261 |
self.sign_policy(policy, "oxy policy init") |
| 262 |
} |
| 263 |
|
| 264 |
|
| 265 |
pub fn policy_root_add(&self, identity: Identity) -> Result<PolicyUpdate> { |
| 266 |
let mut p = self.next_policy()?; |
| 267 |
let key = identity.hash(); |
| 268 |
if p.roots.contains(&key) { |
| 269 |
return fail(format!("{} is already a root", key.short(12))); |
| 270 |
} |
| 271 |
p.roots.push(key); |
| 272 |
p.roots.sort(); |
| 273 |
p.revoked.retain(|k| k != &key); |
| 274 |
if p.identity(&key).is_none() { |
| 275 |
p.identities.push(identity); |
| 276 |
} |
| 277 |
self.sign_policy(p, &format!("oxy policy root add {}", key.short(12))) |
| 83e0677b Andy McCall ↶ |
278 |
} |
| 279 |
|
| 28fc23d7 Andy McCall ↶ |
280 |
|
| 281 |
pub fn policy_root_remove(&self, key: &KeyId) -> Result<PolicyUpdate> { |
| 282 |
let mut p = self.next_policy()?; |
| 283 |
if !p.roots.contains(key) { |
| 284 |
return fail(format!("{} is not a root", key.short(12))); |
| 285 |
} |
| 286 |
p.roots.retain(|k| k != key); |
| 287 |
if (p.roots.len() as u32) < p.root_threshold { |
| 75167672 Andy McCall |
288 |
return fail(format!( |
| 289 |
"removing it would leave {} root(s) for a threshold of {}", |
| 290 |
p.roots.len(), |
| 291 |
p.root_threshold |
| 292 |
)); |
| 28fc23d7 Andy McCall ↶ |
293 |
} |
| 294 |
self.sign_policy(p, &format!("oxy policy root remove {}", key.short(12))) |
| 295 |
} |
| 296 |
|
| 297 |
|
| 298 |
pub fn policy_set_threshold(&self, n: u32) -> Result<PolicyUpdate> { |
| 299 |
let mut p = self.next_policy()?; |
| 300 |
if n == 0 || n as usize > p.roots.len() { |
| 75167672 Andy McCall |
301 |
return fail(format!( |
| 302 |
"the threshold must be between 1 and the number of roots ({})", |
| 303 |
p.roots.len() |
| 304 |
)); |
| 28fc23d7 Andy McCall ↶ |
305 |
} |
| 306 |
p.root_threshold = n; |
| 307 |
self.sign_policy(p, &format!("oxy policy root threshold {n}")) |
| 308 |
} |
| 309 |
|
| 310 |
pub(crate) fn next_policy(&self) -> Result<Policy> { |
| 83e0677b Andy McCall ↶ |
311 |
let Some((_, sp)) = self.current_policy()? else { |
| 312 |
return fail("this repository has no policy; run `oxy policy init`"); |
| 313 |
}; |
| 314 |
let mut p = sp.policy; |
| 315 |
p.serial += 1; |
| 316 |
p.expires = Hlc::now().wall_ms + POLICY_LIFETIME_MS; |
| 317 |
Ok(p) |
| 318 |
} |
| 319 |
|
| 320 |
|
| 28fc23d7 Andy McCall ↶ |
321 |
pub fn policy_grant(&self, identity: Identity, pattern: Option<&str>) -> Result<PolicyUpdate> { |
| 83e0677b Andy McCall ↶ |
322 |
let mut p = self.next_policy()?; |
| 323 |
let key = identity.hash(); |
| 324 |
let pattern = pattern.unwrap_or("*"); |
| 325 |
p.revoked.retain(|k| k != &key); |
| 326 |
match p.rules.iter_mut().find(|r| r.pattern == pattern) { |
| 327 |
Some(rule) => { |
| 328 |
if !rule.keys.contains(&key) { |
| 329 |
rule.keys.push(key); |
| 330 |
rule.keys.sort(); |
| 331 |
} |
| 332 |
} |
| 75167672 Andy McCall |
333 |
None => p.rules.push(Rule { |
| 334 |
pattern: pattern.into(), |
| 335 |
keys: vec![key], |
| 336 |
threshold: 1, |
| 337 |
require: Require::Any, |
| 338 |
allow_conflicts: true, |
| 339 |
}), |
| 83e0677b Andy McCall ↶ |
340 |
} |
| 341 |
if p.identity(&key).is_none() { |
| 342 |
p.identities.push(identity); |
| 343 |
} |
| 28fc23d7 Andy McCall ↶ |
344 |
self.sign_policy(p, &format!("oxy policy grant {}", key.short(12))) |
| 83e0677b Andy McCall ↶ |
345 |
} |
| 346 |
|
| 28fc23d7 Andy McCall ↶ |
347 |
|
| 348 |
|
| 349 |
|
| 350 |
pub fn policy_revoke(&self, key: &KeyId) -> Result<PolicyUpdate> { |
| 83e0677b Andy McCall ↶ |
351 |
let mut p = self.next_policy()?; |
| 352 |
if p.roots.contains(key) { |
| 28fc23d7 Andy McCall ↶ |
353 |
let own = self.secret_key()?.map(|(k, _)| k); |
| 354 |
if own == Some(*key) { |
| 355 |
return fail("a root cannot revoke itself; have another root revoke it, or remove it with `oxy policy root remove` first"); |
| 356 |
} |
| 357 |
p.roots.retain(|k| k != key); |
| 358 |
if (p.roots.len() as u32) < p.root_threshold { |
| 75167672 Andy McCall |
359 |
return fail(format!( |
| 360 |
"revoking it would leave {} root(s) for a threshold of {}", |
| 361 |
p.roots.len(), |
| 362 |
p.root_threshold |
| 363 |
)); |
| 28fc23d7 Andy McCall ↶ |
364 |
} |
| 83e0677b Andy McCall ↶ |
365 |
} |
| 366 |
for r in &mut p.rules { |
| 367 |
r.keys.retain(|k| k != key); |
| 368 |
} |
| 369 |
p.rules.retain(|r| !r.keys.is_empty()); |
| 370 |
if !p.revoked.contains(key) { |
| 371 |
p.revoked.push(*key); |
| 372 |
p.revoked.sort(); |
| 373 |
} |
| 28fc23d7 Andy McCall ↶ |
374 |
self.sign_policy(p, &format!("oxy policy revoke {}", key.short(12))) |
| 83e0677b Andy McCall ↶ |
375 |
} |
| 376 |
|
| 377 |
|
| 378 |
|
| 28fc23d7 Andy McCall ↶ |
379 |
|
| 380 |
|
| 381 |
|
| 75167672 Andy McCall |
382 |
pub fn change_allowed( |
| 383 |
&self, |
| 384 |
hash: &ChangeHash, |
| 385 |
policy: &Policy, |
| 386 |
reference: &str, |
| 387 |
) -> std::result::Result<(), String> { |
| 388 |
let rule = policy |
| 389 |
.rule_for(reference) |
| 390 |
.ok_or_else(|| format!("no policy rule matches {reference:?}"))?; |
| 83e0677b Andy McCall ↶ |
391 |
match self.authenticate_change(hash).map_err(|e| e.to_string())? { |
| 392 |
SignatureStatus::Valid { key, sealed, .. } => { |
| 393 |
if policy.is_revoked(&key) { |
| 75167672 Andy McCall |
394 |
return Err(format!( |
| 395 |
"change {} is signed by revoked key {}", |
| 396 |
hash.short(12), |
| 397 |
key.short(12) |
| 398 |
)); |
| 83e0677b Andy McCall ↶ |
399 |
} |
| 75167672 Andy McCall |
400 |
let author = self |
| 401 |
.store |
| 402 |
.get_change(hash) |
| 403 |
.map(|c| c.author) |
| 404 |
.map_err(|e| e.to_string())?; |
| 28fc23d7 Andy McCall ↶ |
405 |
let principal = if author.is_zero() { key } else { author }; |
| 75167672 Andy McCall |
406 |
let covered = rule |
| 407 |
.keys |
| 408 |
.iter() |
| 409 |
.any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal)); |
| 28fc23d7 Andy McCall ↶ |
410 |
if !covered { |
| 75167672 Andy McCall |
411 |
return Err(format!( |
| 412 |
"change {} is signed by {}, which may not update {reference:?}", |
| 413 |
hash.short(12), |
| 414 |
key.short(12) |
| 415 |
)); |
| 83e0677b Andy McCall ↶ |
416 |
} |
| 417 |
match (rule.require, sealed) { |
| 75167672 Andy McCall |
418 |
(Require::Signed, true) => Err(format!( |
| 419 |
"{reference:?} requires individually signed changes; {} is only sealed", |
| 420 |
hash.short(12) |
| 421 |
)), |
| 422 |
(Require::Sealed, false) => Err(format!( |
| 423 |
"{reference:?} requires sealed changes; {} is individually signed", |
| 424 |
hash.short(12) |
| 425 |
)), |
| 83e0677b Andy McCall ↶ |
426 |
_ => Ok(()), |
| 427 |
} |
| 428 |
} |
| 75167672 Andy McCall |
429 |
SignatureStatus::Unsigned => { |
| 430 |
Err(format!("change {} is not signed or sealed", hash.short(12))) |
| 431 |
} |
| 83e0677b Andy McCall ↶ |
432 |
SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))), |
| 433 |
} |
| 434 |
} |
| 435 |
|
| 436 |
|
| 75167672 Andy McCall |
437 |
pub(crate) fn state_allowed( |
| 438 |
&self, |
| 439 |
old_heads: &[ChangeHash], |
| 440 |
new_heads: &[ChangeHash], |
| 441 |
policy: &Policy, |
| 442 |
reference: &str, |
| 443 |
) -> std::result::Result<(), String> { |
| 444 |
let added = self |
| 445 |
.negotiate(old_heads, new_heads) |
| 446 |
.map_err(|e| e.to_string())?; |
| 83e0677b Andy McCall ↶ |
447 |
for h in &added { |
| 448 |
self.change_allowed(h, policy, reference)?; |
| 449 |
} |
| 450 |
if let Some(rule) = policy.rule_for(reference) { |
| 451 |
if !rule.allow_conflicts && !added.is_empty() { |
| 452 |
let st = self.load_state(new_heads).map_err(|e| e.to_string())?; |
| 453 |
if st.materialize().is_conflicted() { |
| 454 |
return Err(format!("{reference:?} does not allow conflicted states")); |
| 455 |
} |
| 456 |
} |
| 457 |
} |
| 458 |
Ok(()) |
| 459 |
} |
| 460 |
} |