1#![allow(
23 clippy::ptr_arg,
24 clippy::match_like_matches_macro,
25 clippy::collapsible_if,
26 clippy::collapsible_match,
27 clippy::redundant_pattern_matching
28)]
29
30use planarian_schema::{
31 Actor, ApproverClass, Authorization, Capabilities, ChangeId, ConfigDiff, Decision, DerivedFacts, GenerationId,
32 Grant, Hash, ModelId, PolicyVersion, PrincipalId, Recovery, RejectReason, Scope, Sensitivities, Tier, Transition,
33 UnitId,
34};
35use serde::{Deserialize, Serialize};
36use vstd::prelude::*;
37
38pub mod proofs;
39
40pub const POLICY_REVISION: &str = "phase-a.3";
42
43verus! {
44
45pub open spec fn has_str(v: Seq<String>, s: Seq<char>) -> bool {
48 exists|j: int| 0 <= j < v.len() && (#[trigger] v[j])@ == s
49}
50
51pub open spec fn has_change(v: Seq<ChangeId>, c: ChangeId) -> bool {
52 exists|j: int| 0 <= j < v.len() && (#[trigger] v[j]).0@ == c.0@
53}
54
55pub open spec fn has_model(v: Seq<ModelId>, m: ModelId) -> bool {
56 exists|j: int| 0 <= j < v.len() && (#[trigger] v[j]).0@ == m.0@
57}
58
59pub open spec fn under(path: Seq<char>, prefix: Seq<char>) -> bool {
61 path == prefix || (prefix.len() < path.len() && path.subrange(0, prefix.len() as int) == prefix
62 && path[prefix.len() as int] == '.')
63}
64
65fn str_eq(a: &str, b: &str) -> (r: bool)
66 ensures
67 r == (a@ == b@),
68{
69 let n = a.unicode_len();
70 if n != b.unicode_len() {
71 return false;
72 }
73 let mut i: usize = 0;
74 while i < n
75 invariant
76 n == a@.len(),
77 n == b@.len(),
78 i <= n,
79 forall|j: int| 0 <= j < i ==> a@[j] == b@[j],
80 decreases n - i,
81 {
82 if a.get_char(i) != b.get_char(i) {
83 return false;
84 }
85 i += 1;
86 }
87 assert(a@ =~= b@);
88 true
89}
90
91pub fn contains_str(v: &Vec<String>, s: &str) -> (r: bool)
92 ensures
93 r == has_str(v@, s@),
94{
95 let mut i: usize = 0;
96 while i < v.len()
97 invariant
98 i <= v@.len(),
99 forall|j: int| 0 <= j < i ==> (#[trigger] v@[j])@ != s@,
100 decreases v.len() - i,
101 {
102 if str_eq(v[i].as_str(), s) {
103 return true;
104 }
105 i += 1;
106 }
107 false
108}
109
110pub fn contains_change(v: &Vec<ChangeId>, c: &ChangeId) -> (r: bool)
111 ensures
112 r == has_change(v@, *c),
113{
114 let mut i: usize = 0;
115 while i < v.len()
116 invariant
117 i <= v@.len(),
118 forall|j: int| 0 <= j < i ==> (#[trigger] v@[j]).0@ != c.0@,
119 decreases v.len() - i,
120 {
121 if str_eq(v[i].0.as_str(), c.0.as_str()) {
122 return true;
123 }
124 i += 1;
125 }
126 false
127}
128
129pub fn contains_model(v: &Vec<ModelId>, m: &ModelId) -> (r: bool)
130 ensures
131 r == has_model(v@, *m),
132{
133 let mut i: usize = 0;
134 while i < v.len()
135 invariant
136 i <= v@.len(),
137 forall|j: int| 0 <= j < i ==> (#[trigger] v@[j]).0@ != m.0@,
138 decreases v.len() - i,
139 {
140 if str_eq(v[i].0.as_str(), m.0.as_str()) {
141 return true;
142 }
143 i += 1;
144 }
145 false
146}
147
148pub fn is_under(path: &str, prefix: &str) -> (r: bool)
149 ensures
150 r == under(path@, prefix@),
151{
152 let n = path.unicode_len();
153 let m = prefix.unicode_len();
154 if m > n {
155 return false;
156 }
157 let mut i: usize = 0;
158 while i < m
159 invariant
160 n == path@.len(),
161 m == prefix@.len(),
162 m <= n,
163 i <= m,
164 forall|j: int| 0 <= j < i ==> path@[j] == prefix@[j],
165 decreases m - i,
166 {
167 if path.get_char(i) != prefix.get_char(i) {
168 assert(path@.subrange(0, m as int)[i as int] != prefix@[i as int]);
169 return false;
170 }
171 i += 1;
172 }
173 assert(path@.subrange(0, m as int) =~= prefix@);
174 if m == n {
175 assert(path@ =~= prefix@);
176 true
177 } else {
178 path.get_char(m) == '.'
179 }
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
186#[verifier::external_derive]
187pub struct Settings {
188 pub max_tier: Tier,
190 pub user_controllable_units: Vec<String>,
193 pub user_controllable_changes: Vec<ChangeId>,
195 pub user_controllable_options: Vec<String>,
197}
198
199#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
201#[verifier::external_derive]
202pub struct Tables {
203 pub catalog: Vec<ChangeId>,
205 pub runtime_allowlist: Vec<String>,
207 pub option_sensitivity: Vec<(String, Sensitivities)>,
210 pub model_catalog: Vec<ModelId>,
212}
213
214pub open spec fn routine() -> Sensitivities {
215 Sensitivities { routine: true, security: false, user_data: false, external: false, core: false }
216}
217
218pub open spec fn lookup(tbl: Seq<(String, Sensitivities)>, path: Seq<char>) -> (int, Sensitivities)
221 decreases tbl.len(),
222{
223 if tbl.len() == 0 {
224 (-1, routine())
225 } else {
226 let prev = lookup(tbl.drop_last(), path);
227 let k = tbl.last().0@;
228 if under(path, k) && k.len() >= prev.0 {
229 (k.len() as int, tbl.last().1)
230 } else {
231 prev
232 }
233 }
234}
235
236pub open spec fn path_non_routine(tb: Tables, path: Seq<char>) -> bool {
237 !lookup(tb.option_sensitivity@, path).1.routine_only()
238}
239
240pub open spec fn diff_non_routine(tb: Tables, d: ConfigDiff) -> bool {
242 exists|j: int| 0 <= j < d.options.entries@.len() && path_non_routine(tb, (#[trigger] d.options.entries@[j]).0@)
243}
244
245pub open spec fn diff_locked(locked: Seq<String>, d: ConfigDiff) -> bool {
247 exists|j: int| 0 <= j < d.options.entries@.len() && has_str(locked, (#[trigger] d.options.entries@[j]).0@)
248}
249
250impl Tables {
251 pub fn option_sensitivity(&self, path: &str) -> (r: Sensitivities)
252 ensures
253 r == lookup(self.option_sensitivity@, path@).1,
254 {
255 let tbl = &self.option_sensitivity;
256 let mut best: Option<usize> = None;
257 let mut sens = Sensitivities { routine: true, security: false, user_data: false, external: false, core: false };
258 let mut i: usize = 0;
259 while i < tbl.len()
260 invariant
261 i <= tbl@.len(),
262 lookup(tbl@.subrange(0, i as int), path@) == (
263 match best {
264 None => -1int,
265 Some(b) => b as int,
266 },
267 sens,
268 ),
269 decreases tbl.len() - i,
270 {
271 let k = tbl[i].0.as_str();
272 let len = k.unicode_len();
273 let longer = match best {
274 None => true,
275 Some(b) => len >= b,
276 };
277 if is_under(path, k) && longer {
278 best = Some(len);
279 sens = tbl[i].1;
280 }
281 assert(tbl@.subrange(0, i as int + 1).drop_last() =~= tbl@.subrange(0, i as int));
282 i += 1;
283 }
284 assert(tbl@.subrange(0, tbl@.len() as int) =~= tbl@);
285 sens
286 }
287}
288
289pub fn any_non_routine(tb: &Tables, d: &ConfigDiff) -> (r: bool)
290 ensures
291 r == diff_non_routine(*tb, *d),
292{
293 let mut i: usize = 0;
294 while i < d.options.entries.len()
295 invariant
296 i <= d.options.entries@.len(),
297 forall|j: int| 0 <= j < i ==> !path_non_routine(*tb, (#[trigger] d.options.entries@[j]).0@),
298 decreases d.options.entries.len() - i,
299 {
300 if !tb.option_sensitivity(d.options.entries[i].0.as_str()).only_routine() {
301 assert(path_non_routine(*tb, d.options.entries@[i as int].0@));
302 return true;
303 }
304 i += 1;
305 }
306 false
307}
308
309pub fn any_locked(locked: &Vec<String>, d: &ConfigDiff) -> (r: bool)
310 ensures
311 r == diff_locked(locked@, *d),
312{
313 let mut i: usize = 0;
314 while i < d.options.entries.len()
315 invariant
316 i <= d.options.entries@.len(),
317 forall|j: int| 0 <= j < i ==> !has_str(locked@, (#[trigger] d.options.entries@[j]).0@),
318 decreases d.options.entries.len() - i,
319 {
320 if contains_str(locked, d.options.entries[i].0.as_str()) {
321 assert(has_str(locked@, d.options.entries@[i as int].0@));
322 return true;
323 }
324 i += 1;
325 }
326 false
327}
328
329pub open spec fn is_user(a: Actor) -> bool {
332 match a {
333 Actor::User { .. } => true,
334 _ => false,
335 }
336}
337
338pub open spec fn is_admin(a: Authorization) -> bool {
342 is_user(a.actor) && a.capabilities.administrator
343}
344
345pub open spec fn is_local(a: Actor) -> bool {
347 match a {
348 Actor::User { seat, .. } => seat is Some,
349 _ => false,
350 }
351}
352
353pub open spec fn own_session(a: Actor, f: DerivedFacts) -> bool {
355 match a {
356 Actor::User { session, .. } => match session {
357 Some(s) => f.affected_sessions@.len() == 1 && f.affected_sessions@[0]@ == s@,
358 None => false,
359 },
360 _ => false,
361 }
362}
363
364pub open spec fn unit_ok(u: UnitId, uid: u32, c: Capabilities, s: Settings) -> bool {
367 match u.scope {
368 Scope::User { uid: owner, .. } => c.own_scope && owner == uid,
369 Scope::System => c.user_controllable && has_str(s.user_controllable_units@, u.name@),
370 }
371}
372
373pub open spec fn may_affect(u: UnitId, uid: u32) -> bool {
375 match u.scope {
376 Scope::User { uid: owner, .. } => owner == uid,
377 Scope::System => true,
378 }
379}
380
381pub open spec fn all_may_affect(units: Seq<UnitId>, uid: u32) -> bool {
382 forall|j: int| 0 <= j < units.len() ==> may_affect(#[trigger] units[j], uid)
383}
384
385pub open spec fn is_override(t: Transition) -> bool {
387 match t {
388 Transition::SetOverride(_) | Transition::LiftOverride(_) | Transition::StopAndSuppress { .. } => true,
389 _ => false,
390 }
391}
392
393pub open spec fn authorized_spec(t: Transition, f: DerivedFacts, a: Authorization, s: Settings) -> bool {
402 match a.actor {
403 Actor::User { uid, .. } => a.capabilities.administrator || match t {
404 Transition::RestartUnit(u) => unit_ok(u, uid, a.capabilities, s),
405 Transition::StopUnit(u) => unit_ok(u, uid, a.capabilities, s),
406 Transition::LiftOverride(u) => unit_ok(u, uid, a.capabilities, s) || f.sensitivity.security,
407 Transition::SetOverride(o) => unit_ok(o.unit, uid, a.capabilities, s) || f.sensitivity.security,
408 Transition::StopAndSuppress { unit, .. } => unit_ok(unit, uid, a.capabilities, s)
409 || f.sensitivity.security,
410 Transition::ApplyChange { change, .. } => a.capabilities.user_controllable && has_change(
411 s.user_controllable_changes@,
412 change,
413 ) && all_may_affect(f.affected_units@, uid),
414 Transition::SetRuntimeOption { path, .. } => a.capabilities.user_controllable && has_str(
415 s.user_controllable_options@,
416 path@,
417 ),
418 _ => false,
419 },
420 Actor::Agent(_) | Actor::Model(_) => false,
423 Actor::Unattributed => false,
424 _ => match t {
425 Transition::Resolve { .. } => false,
426 _ => true,
427 },
428 }
429}
430
431fn unit_ok_exec(u: &UnitId, uid: u32, c: &Capabilities, s: &Settings) -> (r: bool)
432 ensures
433 r == unit_ok(*u, uid, *c, *s),
434{
435 match &u.scope {
436 Scope::User { uid: owner, .. } => c.own_scope && *owner == uid,
437 Scope::System => c.user_controllable && contains_str(&s.user_controllable_units, u.name.as_str()),
438 }
439}
440
441#[verifier::loop_isolation(false)]
442pub fn authorized(t: &Transition, facts: &DerivedFacts, authz: &Authorization, settings: &Settings) -> (r: bool)
443 ensures
444 r == authorized_spec(*t, *facts, *authz, *settings),
445{
446 let uid = match &authz.actor {
447 Actor::User { uid, .. } => *uid,
448 Actor::Agent(_) | Actor::Model(_) => return false,
449 Actor::Unattributed => return false,
450 _ => {
451 return match t {
452 Transition::Resolve { .. } => false,
453 _ => true,
454 };
455 },
456 };
457 let c = &authz.capabilities;
458 if c.administrator {
459 return true;
460 }
461 let security = facts.sensitivity.security;
462 match t {
463 Transition::RestartUnit(u) => unit_ok_exec(u, uid, c, settings),
464 Transition::StopUnit(u) => unit_ok_exec(u, uid, c, settings),
465 Transition::LiftOverride(u) => unit_ok_exec(u, uid, c, settings) || security,
466 Transition::SetOverride(o) => unit_ok_exec(&o.unit, uid, c, settings) || security,
467 Transition::StopAndSuppress { unit, .. } => unit_ok_exec(unit, uid, c, settings) || security,
468 Transition::ApplyChange { change, .. } => {
469 if !c.user_controllable || !contains_change(&settings.user_controllable_changes, change) {
470 return false;
471 }
472 let mut i: usize = 0;
473 while i < facts.affected_units.len()
474 invariant
475 i <= facts.affected_units@.len(),
476 forall|j: int| 0 <= j < i ==> may_affect(#[trigger] facts.affected_units@[j], uid),
477 decreases facts.affected_units.len() - i,
478 {
479 let ok = match &facts.affected_units[i].scope {
480 Scope::User { uid: owner, .. } => *owner == uid,
481 Scope::System => true,
482 };
483 if !ok {
484 assert(!may_affect(facts.affected_units@[i as int], uid));
485 return false;
486 }
487 i += 1;
488 }
489 true
490 },
491 Transition::SetRuntimeOption { path, .. } => c.user_controllable && contains_str(
492 &settings.user_controllable_options,
493 path.as_str(),
494 ),
495 _ => false,
496 }
497}
498
499pub open spec fn sec_or_ext(s: Sensitivities) -> bool {
502 s.security || s.external
503}
504
505pub open spec fn ask_if(c: bool, t: Tier) -> Tier {
506 if c {
507 Tier::AskFirst
508 } else {
509 t
510 }
511}
512
513pub open spec fn admin_if(c: bool, k: ApproverClass) -> ApproverClass {
515 if c {
516 ApproverClass::Administrator
517 } else {
518 k
519 }
520}
521
522pub open spec fn allow(t: Tier) -> Decision {
523 Decision::Allow { tier: t, approver: ApproverClass::AnyApprover }
524}
525
526pub open spec fn vetted(g: GenerationId, known_good: GenerationId, live_tested: Option<GenerationId>) -> bool {
528 live_tested == Some(g) || g == known_good
529}
530
531pub struct Input<'a> {
533 pub transition: &'a Transition,
534 pub facts: &'a DerivedFacts,
535 pub known_good: GenerationId,
536 pub live_tested: Option<GenerationId>,
537 pub cumulative: &'a ConfigDiff,
539 pub authz: &'a Authorization,
540 pub settings: &'a Settings,
541 pub tables: &'a Tables,
542 pub locked: &'a Vec<String>,
544 pub revert_to: Option<GenerationId>,
546}
547
548pub open spec fn override_spec(f: DerivedFacts, a: Authorization) -> Decision {
552 let held = f.sensitivity.security && !is_admin(a);
553 Decision::Allow {
554 tier: ask_if(!is_user(a.actor) || f.sensitivity.external || held, Tier::Runtime),
555 approver: admin_if(held, ApproverClass::AnyApprover),
556 }
557}
558
559pub open spec fn base_spec(i: Input) -> Decision {
563 let f = *i.facts;
564 match *i.transition {
565 Transition::RestartUnit(_) => allow(
571 ask_if(
572 (f.sensitivity.security && !f.restores_unattributed_stop && !f.restarts_held_unit)
573 || (f.sensitivity.external && !f.restarts_failed_unit && !f.restarts_held_unit),
574 Tier::Runtime,
575 ),
576 ),
577 Transition::StopUnit(_) => allow(ask_if(sec_or_ext(f.sensitivity), Tier::Runtime)),
578 Transition::SetOverride(_) => override_spec(f, *i.authz),
579 Transition::LiftOverride(_) => override_spec(f, *i.authz),
580 Transition::StopAndSuppress { .. } => override_spec(f, *i.authz),
581 Transition::BindDriver { .. } => allow(ask_if(f.sensitivity.security, Tier::Tested)),
582 Transition::ApplyChange { .. } => allow(
583 ask_if(!f.sensitivity.routine_only() || f.touches_manifest || f.touches_canonical_probe, Tier::Tested),
584 ),
585 Transition::SwitchGeneration(g) => allow(ask_if(!vetted(g, i.known_good, i.live_tested), Tier::Tested)),
586 Transition::Revert { to } => allow(
587 ask_if(
588 !vetted(to, i.known_good, i.live_tested) || match i.revert_to {
589 Some(r) => r != to,
590 None => false,
591 },
592 Tier::Tested,
593 ),
594 ),
595 Transition::Rollback => allow(ask_if(sec_or_ext(f.removes_sensitivity), Tier::Runtime)),
601 Transition::LoadModel(m) => if has_model(i.tables.model_catalog@, m) {
602 allow(Tier::Tested)
603 } else {
604 Decision::Reject(RejectReason::NotInCatalog)
605 },
606 Transition::RestoreSnapshot(_) => allow(Tier::AskFirst),
607 Transition::CollectGarbage { .. } => allow(Tier::Runtime),
608 Transition::SetRuntimeOption { path, .. } => if has_str(i.tables.runtime_allowlist@, path@) {
609 allow(Tier::Runtime)
610 } else {
611 Decision::Reject(RejectReason::NotInCatalog)
612 },
613 Transition::Resolve { .. } => allow(Tier::Runtime),
614 }
615}
616
617pub open spec fn floor_spec(f: DerivedFacts, a: Authorization) -> bool {
625 f.recovery == Recovery::NonReversible || ((f.disruption.session_loss || f.disruption.data_interruption)
626 && !(is_user(a.actor) && own_session(a.actor, f))) || (f.disruption.connectivity_loss && !is_local(
627 a.actor,
628 )) || (f.other_sessions_affected && (f.disruption.session_loss || f.disruption.connectivity_loss)
629 && !is_admin(a))
630}
631
632pub open spec fn cumulative_spec(i: Input) -> bool {
637 match i.facts.config_diff {
638 Some(d) => diff_non_routine(*i.tables, d) || diff_non_routine(*i.tables, *i.cumulative),
639 None => false,
640 }
641}
642
643pub open spec fn uncapped_spec(i: Input) -> Decision {
645 let f = *i.facts;
646 if !authorized_spec(*i.transition, f, *i.authz, *i.settings) {
647 Decision::Reject(RejectReason::Unauthorized)
648 } else if f.touches_core || f.sensitivity.core {
649 Decision::Reject(RejectReason::CoreTarget)
650 } else if match *i.transition {
651 Transition::ApplyChange { change, .. } => !has_change(i.tables.catalog@, change),
652 _ => false,
653 } {
654 Decision::Reject(RejectReason::NotInCatalog)
655 } else if match f.config_diff {
656 Some(d) => diff_locked(i.locked@, d),
657 None => false,
658 } {
659 Decision::Reject(RejectReason::LockedOption)
660 } else {
661 match base_spec(i) {
662 Decision::Allow { tier, approver } => Decision::Allow {
663 tier: ask_if(
664 floor_spec(f, *i.authz) || cumulative_spec(i) || f.touches_canonical_probe
665 || f.below_security_floor,
666 tier,
667 ),
668 approver: admin_if(f.below_security_floor, approver),
670 },
671 other => other,
672 }
673 }
674}
675
676pub open spec fn cap(d: Decision, max: Tier) -> Decision {
679 match d {
680 Decision::Allow { tier, approver } => if tier.rank() > max.rank() {
681 Decision::Allow { tier: Tier::AskFirst, approver }
682 } else {
683 Decision::Allow { tier, approver }
684 },
685 other => other,
686 }
687}
688
689pub open spec fn decide_spec(i: Input) -> Decision {
690 cap(uncapped_spec(i), i.settings.max_tier)
691}
692
693fn is_user_exec(a: &Actor) -> (r: bool)
694 ensures
695 r == is_user(*a),
696{
697 match a {
698 Actor::User { .. } => true,
699 _ => false,
700 }
701}
702
703fn is_admin_exec(a: &Authorization) -> (r: bool)
704 ensures
705 r == is_admin(*a),
706{
707 is_user_exec(&a.actor) && a.capabilities.administrator
708}
709
710fn is_local_exec(a: &Actor) -> (r: bool)
711 ensures
712 r == is_local(*a),
713{
714 match a {
715 Actor::User { seat, .. } => match seat {
716 Some(_) => true,
717 None => false,
718 },
719 _ => false,
720 }
721}
722
723fn own_session_exec(a: &Actor, f: &DerivedFacts) -> (r: bool)
724 ensures
725 r == own_session(*a, *f),
726{
727 match a {
728 Actor::User { session, .. } => match session {
729 Some(s) => f.affected_sessions.len() == 1 && str_eq(f.affected_sessions[0].as_str(), s.as_str()),
730 None => false,
731 },
732 _ => false,
733 }
734}
735
736fn sec_or_ext_exec(s: &Sensitivities) -> (r: bool)
737 ensures
738 r == sec_or_ext(*s),
739{
740 s.security || s.external
741}
742
743fn ask_if_exec(c: bool, t: Tier) -> (r: Tier)
744 ensures
745 r == ask_if(c, t),
746{
747 if c {
748 Tier::AskFirst
749 } else {
750 t
751 }
752}
753
754fn admin_if_exec(c: bool, k: ApproverClass) -> (r: ApproverClass)
755 ensures
756 r == admin_if(c, k),
757{
758 if c {
759 ApproverClass::Administrator
760 } else {
761 k
762 }
763}
764
765fn allow_exec(t: Tier) -> (r: Decision)
766 ensures
767 r == allow(t),
768{
769 Decision::Allow { tier: t, approver: ApproverClass::AnyApprover }
770}
771
772fn gen_eq(a: GenerationId, b: GenerationId) -> (r: bool)
773 ensures
774 r == (a == b),
775{
776 a.0 == b.0
777}
778
779fn vetted_exec(g: GenerationId, known_good: GenerationId, live_tested: Option<GenerationId>) -> (r: bool)
780 ensures
781 r == vetted(g, known_good, live_tested),
782{
783 let live = match live_tested {
784 Some(l) => gen_eq(l, g),
785 None => false,
786 };
787 live || gen_eq(g, known_good)
788}
789
790fn override_exec(f: &DerivedFacts, a: &Authorization) -> (r: Decision)
791 ensures
792 r == override_spec(*f, *a),
793{
794 let held = f.sensitivity.security && !is_admin_exec(a);
795 Decision::Allow {
796 tier: ask_if_exec(!is_user_exec(&a.actor) || f.sensitivity.external || held, Tier::Runtime),
797 approver: admin_if_exec(held, ApproverClass::AnyApprover),
798 }
799}
800
801fn base_tier(i: &Input) -> (r: Decision)
802 ensures
803 r == base_spec(*i),
804{
805 let f = i.facts;
806 match i.transition {
807 Transition::RestartUnit(_) => allow_exec(
808 ask_if_exec(
809 (f.sensitivity.security && !f.restores_unattributed_stop && !f.restarts_held_unit)
810 || (f.sensitivity.external && !f.restarts_failed_unit && !f.restarts_held_unit),
811 Tier::Runtime,
812 ),
813 ),
814 Transition::StopUnit(_) => allow_exec(ask_if_exec(sec_or_ext_exec(&f.sensitivity), Tier::Runtime)),
815 Transition::SetOverride(_) => override_exec(f, i.authz),
816 Transition::LiftOverride(_) => override_exec(f, i.authz),
817 Transition::StopAndSuppress { .. } => override_exec(f, i.authz),
818 Transition::BindDriver { .. } => allow_exec(ask_if_exec(f.sensitivity.security, Tier::Tested)),
819 Transition::ApplyChange { .. } => allow_exec(
820 ask_if_exec(
821 !f.sensitivity.only_routine() || f.touches_manifest || f.touches_canonical_probe,
822 Tier::Tested,
823 ),
824 ),
825 Transition::SwitchGeneration(g) => allow_exec(
826 ask_if_exec(!vetted_exec(*g, i.known_good, i.live_tested), Tier::Tested),
827 ),
828 Transition::Revert { to } => {
829 let mismatched = match i.revert_to {
830 Some(r) => !gen_eq(r, *to),
831 None => false,
832 };
833 allow_exec(ask_if_exec(!vetted_exec(*to, i.known_good, i.live_tested) || mismatched, Tier::Tested))
834 },
835 Transition::Rollback => allow_exec(ask_if_exec(sec_or_ext_exec(&f.removes_sensitivity), Tier::Runtime)),
836 Transition::LoadModel(m) => if contains_model(&i.tables.model_catalog, m) {
837 allow_exec(Tier::Tested)
838 } else {
839 Decision::Reject(RejectReason::NotInCatalog)
840 },
841 Transition::RestoreSnapshot(_) => allow_exec(Tier::AskFirst),
842 Transition::CollectGarbage { .. } => allow_exec(Tier::Runtime),
843 Transition::SetRuntimeOption { path, .. } => if contains_str(&i.tables.runtime_allowlist, path.as_str()) {
844 allow_exec(Tier::Runtime)
845 } else {
846 Decision::Reject(RejectReason::NotInCatalog)
847 },
848 Transition::Resolve { .. } => allow_exec(Tier::Runtime),
849 }
850}
851
852fn floors(f: &DerivedFacts, a: &Authorization) -> (r: bool)
853 ensures
854 r == floor_spec(*f, *a),
855{
856 let user = is_user_exec(&a.actor);
857 let non_reversible = match f.recovery {
858 Recovery::NonReversible => true,
859 _ => false,
860 };
861 let session = f.disruption.session_loss || f.disruption.data_interruption;
862 let connectivity = f.disruption.connectivity_loss;
863 let reaches_others = f.other_sessions_affected && (f.disruption.session_loss || f.disruption.connectivity_loss);
864 non_reversible || (session && !(user && own_session_exec(&a.actor, f))) || (connectivity && !is_local_exec(
865 &a.actor,
866 )) || (reaches_others && !is_admin_exec(a))
867}
868
869pub fn decide(i: &Input) -> (r: Decision)
870 ensures
871 r == decide_spec(*i),
872{
873 let f = i.facts;
874 if !authorized(i.transition, f, i.authz, i.settings) {
875 return Decision::Reject(RejectReason::Unauthorized);
876 }
877 if f.touches_core || f.sensitivity.core {
878 return Decision::Reject(RejectReason::CoreTarget);
879 }
880 if let Transition::ApplyChange { change, .. } = i.transition {
881 if !contains_change(&i.tables.catalog, change) {
882 return Decision::Reject(RejectReason::NotInCatalog);
883 }
884 }
885 if let Some(d) = &f.config_diff {
886 if any_locked(i.locked, d) {
887 return Decision::Reject(RejectReason::LockedOption);
888 }
889 }
890 let (base, class) = match base_tier(i) {
891 Decision::Allow { tier, approver } => (tier, approver),
892 Decision::Reject(r) => return Decision::Reject(r),
893 };
894 let cumulative = match &f.config_diff {
895 Some(d) => any_non_routine(i.tables, d) || any_non_routine(i.tables, i.cumulative),
896 None => false,
897 };
898 let tier = ask_if_exec(
899 floors(f, i.authz) || cumulative || f.touches_canonical_probe || f.below_security_floor,
900 base,
901 );
902 let approver = admin_if_exec(f.below_security_floor, class);
903 if tier.rank_of() > i.settings.max_tier.rank_of() {
904 Decision::Allow { tier: Tier::AskFirst, approver }
905 } else {
906 Decision::Allow { tier, approver }
907 }
908}
909
910pub open spec fn may_approve_spec(approver: Authorization, proposer: PrincipalId, class: ApproverClass) -> bool {
917 is_user(approver.actor) && approver.principal != proposer && approver.capabilities.approve && match class {
918 ApproverClass::AnyApprover => true,
919 ApproverClass::Administrator => approver.capabilities.administrator,
920 }
921}
922
923pub fn may_approve(approver: &Authorization, proposer: PrincipalId, class: ApproverClass) -> (r: bool)
924 ensures
925 r == may_approve_spec(*approver, proposer, class),
926{
927 let holds_class = match class {
928 ApproverClass::AnyApprover => true,
929 ApproverClass::Administrator => approver.capabilities.administrator,
930 };
931 is_user_exec(&approver.actor) && approver.principal.0 != proposer.0 && approver.capabilities.approve
932 && holds_class
933}
934
935pub open spec fn may_grant_spec(approver: Authorization, proposer: PrincipalId, grant: Grant) -> bool {
938 may_approve_spec(approver, proposer, ApproverClass::Administrator) && approver.principal != grant.to
939 && approver.capabilities.has(grant.capability)
940}
941
942pub fn may_grant(approver: &Authorization, proposer: PrincipalId, grant: &Grant) -> (r: bool)
943 ensures
944 r == may_grant_spec(*approver, proposer, *grant),
945{
946 may_approve(approver, proposer, ApproverClass::Administrator) && approver.principal.0 != grant.to.0
947 && approver.capabilities.contains(grant.capability)
948}
949
950} impl Default for Settings {
953 fn default() -> Self {
955 Settings {
956 max_tier: Tier::Tested,
957 user_controllable_units: ["pipewire", "wireplumber"].map(String::from).into(),
958 user_controllable_changes: ["font-scale", "wifi-select"].map(|s| ChangeId(s.into())).into(),
959 user_controllable_options: ["font.scale", "audio.volume", "display.brightness", "wifi.network"]
960 .map(String::from)
961 .into(),
962 }
963 }
964}
965
966impl Tables {
967 pub fn diff_sensitivity(&self, diff: &ConfigDiff) -> Sensitivities {
968 diff.options.keys().fold(Sensitivities::default(), |acc, p| acc.union(&self.option_sensitivity(p)))
969 }
970
971 pub fn canonical(&self) -> Tables {
974 let sorted = |mut v: Vec<String>| {
975 v.sort();
976 v.dedup();
977 v
978 };
979 let mut catalog = self.catalog.clone();
980 catalog.sort();
981 catalog.dedup();
982 let mut model_catalog = self.model_catalog.clone();
983 model_catalog.sort();
984 model_catalog.dedup();
985 Tables {
986 catalog,
987 runtime_allowlist: sorted(self.runtime_allowlist.clone()),
988 option_sensitivity: self.option_sensitivity.clone(),
990 model_catalog,
991 }
992 }
993}
994
995pub fn policy_version(tables: &Tables) -> PolicyVersion {
996 let mut bytes = format!("planarian-policy {} {}\n", env!("CARGO_PKG_VERSION"), POLICY_REVISION).into_bytes();
997 bytes.extend(serde_json::to_vec(&tables.canonical()).expect("tables serialize"));
998 PolicyVersion(Hash::of(&bytes))
999}
1000
1001pub fn authorization_of(actor: Actor) -> Authorization {
1003 Authorization { principal: actor.principal(), actor, capabilities: Capabilities::default() }
1004}
1005
1006verus! {
1007
1008pub open spec fn administrator_spec(uid: Option<u32>, in_admin_group: bool) -> bool {
1011 in_admin_group || uid == Some(0u32)
1012}
1013
1014pub fn is_administrator(uid: Option<u32>, in_admin_group: bool) -> (r: bool)
1015 ensures
1016 r == administrator_spec(uid, in_admin_group),
1017{
1018 in_admin_group || match uid {
1019 Some(u) => u == 0,
1020 None => false,
1021 }
1022}
1023
1024} pub fn user_authorization(actor: Actor, admin: bool) -> Authorization {
1031 use planarian_schema::Capability::*;
1032 let admin = is_administrator(actor.uid(), admin);
1033 let caps = if admin {
1034 Capabilities::of(&[OwnScope, UserControllable, Administrator, Approve])
1035 } else {
1036 Capabilities::of(&[OwnScope, UserControllable])
1037 };
1038 Authorization { principal: actor.principal(), actor, capabilities: caps }
1039}
1040
1041impl std::fmt::Debug for Input<'_> {
1042 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1043 f.debug_struct("Input")
1044 .field("transition", self.transition)
1045 .field("facts", self.facts)
1046 .field("authz", self.authz)
1047 .field("max_tier", &self.settings.max_tier)
1048 .finish_non_exhaustive()
1049 }
1050}
1051
1052#[cfg(test)]
1053mod tests;