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