| 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 | 3736× | pub fn current_policy(&self) -> Result<Option<(ObjectRef, SignedPolicy)>> { |
| 37 | 3736× | let Some(r) = self.view()?.policy else { |
| 38 | 3325× | return Ok(None); |
| 39 | | }; |
| 40 | 411× | let policy = Policy::decode(&self.store.get_typed(&r, ObjectType::Policy)?)?; |
| 41 | | Ok(Some(( |
| 42 | 411× | r, |
| 43 | | SignedPolicy { |
| 44 | 411× | policy, |
| 45 | 411× | sigs: self.store.signatures(&r)?, |
| 46 | | }, |
| 47 | | ))) |
| 48 | 3736× | } |
| 49 | |
|
| 50 | | |
| 51 | 82× | pub(crate) fn set_policy(&self, sp: &SignedPolicy, command: &str) -> Result<ObjectRef> { |
| 52 | 82× | let r = self.store.put(ObjectType::Policy, &sp.policy.encode())?; |
| 53 | 106× | for sig in &sp.sigs { |
| 54 | 106× | self.store.put_signature(&r, sig)?; |
| 55 | | } |
| 56 | 175× | for id in &sp.policy.identities { |
| 57 | 175× | self.store.put_identity(id)?; |
| 58 | | } |
| 59 | 82× | let dir = self.oxy.join("policies"); |
| 60 | 82× | fs::create_dir_all(&dir)?; |
| 61 | 82× | write_atomic( |
| 62 | 82× | &dir.join(format!("{:020}", sp.policy.serial)), |
| 63 | 82× | r.to_hex().as_bytes(), |
| 64 | 0× | )?; |
| 65 | 82× | let (op_ref, op) = self.head_op()?; |
| 66 | 82× | if op.view.policy != Some(r) { |
| 67 | 82× | let mut view = op.view.clone(); |
| 68 | 82× | view.policy = Some(r); |
| 69 | 82× | self.record_op(&[op_ref], command, view)?; |
| 70 | 0× | } |
| 71 | 82× | Ok(r) |
| 72 | 82× | } |
| 73 | |
|
| 74 | | |
| 75 | | |
| 76 | | |
| 77 | 1065× | pub fn policy_history(&self) -> Result<Vec<SignedPolicy>> { |
| 78 | 1065× | let mut out: BTreeMap<u64, SignedPolicy> = BTreeMap::new(); |
| 79 | 1065× | if let Ok(rd) = fs::read_dir(self.oxy.join("policies")) { |
| 80 | 176× | for e in rd.flatten() { |
| 81 | 176× | let Ok(hex) = fs::read_to_string(e.path()) else { |
| 82 | 0× | continue; |
| 83 | | }; |
| 84 | 176× | let Some(r) = ObjectRef::from_hex(hex.trim()) else { |
| 85 | 0× | continue; |
| 86 | | }; |
| 87 | 176× | let Ok(bytes) = self.store.get_typed(&r, ObjectType::Policy) else { |
| 88 | 0× | continue; |
| 89 | | }; |
| 90 | 176× | let policy = Policy::decode(&bytes)?; |
| 91 | 176× | out.insert( |
| 92 | 176× | policy.serial, |
| 93 | | SignedPolicy { |
| 94 | 176× | policy, |
| 95 | 176× | sigs: self.store.signatures(&r)?, |
| 96 | | }, |
| 97 | | ); |
| 98 | | } |
| 99 | 983× | } |
| 100 | 1065× | if let Some((_, sp)) = self.current_policy()? { |
| 101 | 82× | out.insert(sp.policy.serial, sp); |
| 102 | 983× | } |
| 103 | 1065× | Ok(out.into_values().collect()) |
| 104 | 1065× | } |
| 105 | |
|
| 106 | | |
| 107 | | |
| 108 | | |
| 109 | 40× | pub(crate) fn adopt_policy_chain( |
| 110 | 40× | &self, |
| 111 | 40× | chain: &[SignedPolicy], |
| 112 | 40× | newest: &SignedPolicy, |
| 113 | 40× | command: &str, |
| 114 | 40× | ) -> std::result::Result<(), String> { |
| 115 | 40× | let mut current = self |
| 116 | 40× | .current_policy() |
| 117 | 40× | .map_err(|e| e.to_string())? |
| 118 | 40× | .map(|(_, sp)| sp.policy); |
| 119 | 40× | let mut steps: Vec<&SignedPolicy> = chain |
| 120 | 40× | .iter() |
| 121 | 40× | .filter(|sp| { |
| 122 | 27× | current.as_ref().is_none_or(|c| sp.policy.serial > c.serial) |
| 123 | 16× | && sp.policy.serial < newest.policy.serial |
| 124 | 27× | }) |
| 125 | 40× | .collect(); |
| 126 | 40× | steps.sort_by_key(|sp| sp.policy.serial); |
| 127 | 40× | steps.push(newest); |
| 128 | 56× | for sp in steps { |
| 129 | 56× | self.check_policy(sp, current.as_ref())?; |
| 130 | 54× | self.set_policy( |
| 131 | 54× | sp, |
| 132 | 54× | &format!("{command} (policy serial {})", sp.policy.serial), |
| 133 | | ) |
| 134 | 54× | .map_err(|e| e.to_string())?; |
| 135 | 54× | current = Some(sp.policy.clone()); |
| 136 | | } |
| 137 | 38× | Ok(()) |
| 138 | 40× | } |
| 139 | |
|
| 140 | | |
| 141 | | |
| 142 | 56× | pub fn check_policy( |
| 143 | 56× | &self, |
| 144 | 56× | sp: &SignedPolicy, |
| 145 | 56× | trusted: Option<&Policy>, |
| 146 | 56× | ) -> std::result::Result<(), String> { |
| 147 | 56× | if sp.policy.repo != self.id().map_err(|e| e.to_string())? { |
| 148 | 0× | return Err("policy is for a different repository".into()); |
| 149 | 56× | } |
| 150 | 56× | if let Some(t) = trusted { |
| 151 | 35× | 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 | 34× | } |
| 157 | 21× | } |
| 158 | 55× | match trusted { |
| 159 | 34× | Some(t) => sp.policy.verify_signatures(&sp.sigs, t)?, |
| 160 | 21× | None => vec![sp.policy.verify_bootstrap(&sp.sigs)?], |
| 161 | | }; |
| 162 | 54× | Ok(()) |
| 163 | 56× | } |
| 164 | |
|
| 165 | | |
| 166 | | |
| 167 | 20× | fn sign_policy(&self, policy: Policy, command: &str) -> Result<PolicyUpdate> { |
| 168 | 20× | let Some((key, sk)) = self.secret_key()? else { |
| 169 | 0× | return fail("no secret key: run `oxy identity create` first"); |
| 170 | | }; |
| 171 | 20× | self.sign_policy_with(policy, &[(key, &sk)], command) |
| 172 | 20× | } |
| 173 | |
|
| 174 | | |
| 175 | | |
| 176 | | |
| 177 | 28× | pub(crate) fn sign_policy_with( |
| 178 | 28× | &self, |
| 179 | 28× | policy: Policy, |
| 180 | 28× | signers: &[(KeyId, &SecretKey)], |
| 181 | 28× | command: &str, |
| 182 | 28× | ) -> Result<PolicyUpdate> { |
| 183 | 28× | let trusted = self |
| 184 | 28× | .current_policy()? |
| 185 | 28× | .map(|(_, sp)| sp.policy) |
| 186 | 28× | .unwrap_or_else(|| policy.clone()); |
| 187 | 28× | if !signers |
| 188 | 28× | .first() |
| 189 | 28× | .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 | 28× | } |
| 193 | 28× | let target = policy.hash(); |
| 194 | 28× | let sigs = signers |
| 195 | 28× | .iter() |
| 196 | 36× | .map(|(k, sk)| sk.sign(Domain::Policy, k, &target)) |
| 197 | 28× | .collect(); |
| 198 | 28× | self.gather(SignedPolicy { policy, sigs }, &trusted, command) |
| 199 | 28× | } |
| 200 | |
|
| 201 | | |
| 202 | 30× | fn gather(&self, sp: SignedPolicy, trusted: &Policy, command: &str) -> Result<PolicyUpdate> { |
| 203 | 30× | let applied = sp.policy.verify_signatures(&sp.sigs, trusted).is_ok(); |
| 204 | 30× | if applied { |
| 205 | 28× | self.set_policy(&sp, command)?; |
| 206 | 2× | } |
| 207 | 30× | Ok(PolicyUpdate { |
| 208 | 30× | policy: sp.policy, |
| 209 | 30× | sigs: sp.sigs, |
| 210 | 30× | applied, |
| 211 | 30× | }) |
| 212 | 30× | } |
| 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 | 11× | pub fn policy_init(&self) -> Result<PolicyUpdate> { |
| 249 | 11× | if self.current_policy()?.is_some() { |
| 250 | 0× | return fail("this repository already has a policy"); |
| 251 | 11× | } |
| 252 | 11× | let Some((key, _)) = self.secret_key()? else { |
| 253 | 0× | return fail("no secret key: run `oxy identity create` first"); |
| 254 | | }; |
| 255 | 11× | let identity = self.identity(&key)?; |
| 256 | 11× | let policy = Policy::single_root( |
| 257 | 11× | self.id()?, |
| 258 | 11× | identity, |
| 259 | 11× | Hlc::now().wall_ms + POLICY_LIFETIME_MS, |
| 260 | | ); |
| 261 | 11× | self.sign_policy(policy, "oxy policy init") |
| 262 | 11× | } |
| 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 | 21× | pub(crate) fn next_policy(&self) -> Result<Policy> { |
| 311 | 21× | let Some((_, sp)) = self.current_policy()? else { |
| 312 | 0× | return fail("this repository has no policy; run `oxy policy init`"); |
| 313 | | }; |
| 314 | 21× | let mut p = sp.policy; |
| 315 | 21× | p.serial += 1; |
| 316 | 21× | p.expires = Hlc::now().wall_ms + POLICY_LIFETIME_MS; |
| 317 | 21× | Ok(p) |
| 318 | 21× | } |
| 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× | subjects: vec![], |
| 340 | 0× | }), |
| 341 | | } |
| 342 | 2× | if p.identity(&key).is_none() { |
| 343 | 2× | p.identities.push(identity); |
| 344 | 2× | } |
| 345 | 2× | self.sign_policy(p, &format!("oxy policy grant {}", key.short(12))) |
| 346 | 2× | } |
| 347 | |
|
| 348 | | |
| 349 | | |
| 350 | | |
| 351 | | |
| 352 | 3× | pub fn policy_grant_subject( |
| 353 | 3× | &self, |
| 354 | 3× | subject: &str, |
| 355 | 3× | issuer: Identity, |
| 356 | 3× | scope: crate::identity::Scope, |
| 357 | 3× | pattern: Option<&str>, |
| 358 | 3× | ) -> Result<PolicyUpdate> { |
| 359 | 3× | if subject.trim().is_empty() { |
| 360 | 0× | return fail("a subject pattern is needed, such as repo:OWNER/NAME:bookmark:main"); |
| 361 | 3× | } |
| 362 | 3× | let mut p = self.next_policy()?; |
| 363 | 3× | let key = issuer.hash(); |
| 364 | 3× | let pattern = pattern.unwrap_or("*"); |
| 365 | 3× | let grant = crate::policy::SubjectGrant { |
| 366 | 3× | pattern: subject.into(), |
| 367 | 3× | issuer: key, |
| 368 | 3× | scope, |
| 369 | 3× | }; |
| 370 | 3× | match p.rules.iter_mut().find(|r| r.pattern == pattern) { |
| 371 | 3× | Some(rule) => { |
| 372 | 3× | rule.subjects |
| 373 | 3× | .retain(|g| !(g.pattern == grant.pattern && g.issuer == grant.issuer)); |
| 374 | 3× | rule.subjects.push(grant); |
| 375 | | } |
| 376 | 0× | None => p.rules.push(Rule { |
| 377 | 0× | pattern: pattern.into(), |
| 378 | 0× | keys: vec![], |
| 379 | 0× | threshold: 1, |
| 380 | 0× | require: Require::Any, |
| 381 | 0× | allow_conflicts: true, |
| 382 | 0× | subjects: vec![grant], |
| 383 | 0× | }), |
| 384 | | } |
| 385 | 3× | if p.identity(&key).is_none() { |
| 386 | 3× | p.identities.push(issuer); |
| 387 | 3× | } |
| 388 | 3× | self.sign_policy(p, &format!("oxy policy grant-subject {subject}")) |
| 389 | 3× | } |
| 390 | |
|
| 391 | | |
| 392 | | |
| 393 | | |
| 394 | 4× | pub fn policy_revoke(&self, key: &KeyId) -> Result<PolicyUpdate> { |
| 395 | 4× | let mut p = self.next_policy()?; |
| 396 | 4× | if p.roots.contains(key) { |
| 397 | 2× | let own = self.secret_key()?.map(|(k, _)| k); |
| 398 | 2× | if own == Some(*key) { |
| 399 | 1× | return fail("a root cannot revoke itself; have another root revoke it, or remove it with `oxy policy root remove` first"); |
| 400 | 1× | } |
| 401 | 2× | p.roots.retain(|k| k != key); |
| 402 | 1× | if (p.roots.len() as u32) < p.root_threshold { |
| 403 | 1× | return fail(format!( |
| 404 | | "revoking it would leave {} root(s) for a threshold of {}", |
| 405 | 1× | p.roots.len(), |
| 406 | | p.root_threshold |
| 407 | | )); |
| 408 | 0× | } |
| 409 | 2× | } |
| 410 | 2× | for r in &mut p.rules { |
| 411 | 4× | r.keys.retain(|k| k != key); |
| 412 | | } |
| 413 | | |
| 414 | 2× | p.rules |
| 415 | 2× | .retain(|r| !r.keys.is_empty() || !r.subjects.is_empty()); |
| 416 | 2× | if !p.revoked.contains(key) { |
| 417 | 2× | p.revoked.push(*key); |
| 418 | 2× | p.revoked.sort(); |
| 419 | 2× | } |
| 420 | 2× | self.sign_policy(p, &format!("oxy policy revoke {}", key.short(12))) |
| 421 | 4× | } |
| 422 | |
|
| 423 | | |
| 424 | | |
| 425 | | |
| 426 | | |
| 427 | | |
| 428 | 21× | pub fn change_allowed( |
| 429 | 21× | &self, |
| 430 | 21× | hash: &ChangeHash, |
| 431 | 21× | policy: &Policy, |
| 432 | 21× | reference: &str, |
| 433 | 21× | ) -> std::result::Result<(), String> { |
| 434 | 21× | let rule = policy |
| 435 | 21× | .rule_for(reference) |
| 436 | 21× | .ok_or_else(|| format!("no policy rule matches {reference:?}"))?; |
| 437 | 21× | match self.authenticate_change(hash).map_err(|e| e.to_string())? { |
| 438 | 21× | SignatureStatus::Valid { key, sealed, .. } => { |
| 439 | 21× | if policy.is_revoked(&key) { |
| 440 | 0× | return Err(format!( |
| 441 | 0× | "change {} is signed by revoked key {}", |
| 442 | 0× | hash.short(12), |
| 443 | 0× | key.short(12) |
| 444 | 0× | )); |
| 445 | 21× | } |
| 446 | 21× | let author = self |
| 447 | 21× | .store |
| 448 | 21× | .get_change(hash) |
| 449 | 21× | .map(|c| c.author) |
| 450 | 21× | .map_err(|e| e.to_string())?; |
| 451 | 21× | let principal = if author.is_zero() { key } else { author }; |
| 452 | | |
| 453 | | |
| 454 | 21× | let covered = rule |
| 455 | 21× | .keys |
| 456 | 21× | .iter() |
| 457 | 23× | .any(|k| policy.succeeds(k, &key) || policy.succeeds(k, &principal)) |
| 458 | 1× | || self.certified_authorized( |
| 459 | 1× | policy, |
| 460 | 1× | &key, |
| 461 | 1× | reference, |
| 462 | | crate::identity::Scope::CHANGES, |
| 463 | 1× | crate::objects::Hlc::now().wall_ms, |
| 464 | | ); |
| 465 | 21× | if !covered { |
| 466 | 0× | return Err(format!( |
| 467 | 0× | "change {} is signed by {}, which may not update {reference:?}", |
| 468 | 0× | hash.short(12), |
| 469 | 0× | key.short(12) |
| 470 | 0× | )); |
| 471 | 21× | } |
| 472 | 21× | match (rule.require, sealed) { |
| 473 | 0× | (Require::Signed, true) => Err(format!( |
| 474 | 0× | "{reference:?} requires individually signed changes; {} is only sealed", |
| 475 | 0× | hash.short(12) |
| 476 | 0× | )), |
| 477 | 0× | (Require::Sealed, false) => Err(format!( |
| 478 | 0× | "{reference:?} requires sealed changes; {} is individually signed", |
| 479 | 0× | hash.short(12) |
| 480 | 0× | )), |
| 481 | 21× | _ => Ok(()), |
| 482 | | } |
| 483 | | } |
| 484 | | SignatureStatus::Unsigned => { |
| 485 | 0× | Err(format!("change {} is not signed or sealed", hash.short(12))) |
| 486 | | } |
| 487 | 0× | SignatureStatus::Invalid(e) => Err(format!("change {}: {e}", hash.short(12))), |
| 488 | | } |
| 489 | 21× | } |
| 490 | |
|
| 491 | | |
| 492 | 30× | pub(crate) fn state_allowed( |
| 493 | 30× | &self, |
| 494 | 30× | old_heads: &[ChangeHash], |
| 495 | 30× | new_heads: &[ChangeHash], |
| 496 | 30× | policy: &Policy, |
| 497 | 30× | reference: &str, |
| 498 | 30× | ) -> std::result::Result<(), String> { |
| 499 | 30× | let added = self |
| 500 | 30× | .negotiate(old_heads, new_heads) |
| 501 | 30× | .map_err(|e| e.to_string())?; |
| 502 | 30× | for h in &added { |
| 503 | 20× | self.change_allowed(h, policy, reference)?; |
| 504 | | } |
| 505 | 30× | if let Some(rule) = policy.rule_for(reference) { |
| 506 | 30× | if !rule.allow_conflicts && !added.is_empty() { |
| 507 | 0× | let st = self.load_state(new_heads).map_err(|e| e.to_string())?; |
| 508 | 0× | if st.materialize().is_conflicted() { |
| 509 | 0× | return Err(format!("{reference:?} does not allow conflicted states")); |
| 510 | 0× | } |
| 511 | 30× | } |
| 512 | 0× | } |
| 513 | 30× | Ok(()) |
| 514 | 30× | } |
| 515 | | } |