1use super::policies::{EvalContext, ResourceRef};
44use super::registry::{ConfigRegistry, ConfigRegistryEntry, EvidenceRequirement};
45use super::store::AuthStore;
46use super::UserId;
47
48pub const RESOURCE_TYPE_POLICY: &str = "policy";
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum PolicyOp {
60 Put,
62 Drop,
64 Attach,
66 Detach,
68}
69
70impl PolicyOp {
71 pub fn action(self) -> &'static str {
75 match self {
76 Self::Put => "policy:put",
77 Self::Drop => "policy:drop",
78 Self::Attach => "policy:attach",
79 Self::Detach => "policy:detach",
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum ManagedPolicyDecision {
87 PassThrough { policy_id: String, op: PolicyOp },
90 Allow {
95 entry_id: String,
96 entry_version: u64,
97 op: PolicyOp,
98 matched_action: String,
99 matched_resource: String,
100 evidence: EvidenceRequirement,
101 },
102 Deny {
105 entry_id: String,
106 entry_version: u64,
107 op: PolicyOp,
108 matched_action: String,
109 matched_resource: String,
110 reason: DenyReason,
111 },
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum DenyReason {
120 NotStructurallyEligible {
123 is_system_owned: bool,
124 is_platform_scoped: bool,
125 },
126 PolicyDenied,
130}
131
132impl std::fmt::Display for DenyReason {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 Self::NotStructurallyEligible {
136 is_system_owned,
137 is_platform_scoped,
138 } => write!(
139 f,
140 "caller is not structurally eligible for managed policy \
141 (system_owned={is_system_owned}, platform_scoped={is_platform_scoped})"
142 ),
143 Self::PolicyDenied => {
144 write!(f, "managed policy required IAM permission was denied")
145 }
146 }
147 }
148}
149
150impl ManagedPolicyDecision {
151 pub fn permitted(&self) -> bool {
154 matches!(self, Self::PassThrough { .. } | Self::Allow { .. })
155 }
156}
157
158pub struct ManagedPolicyGate<'a> {
160 registry: &'a ConfigRegistry,
161}
162
163impl<'a> ManagedPolicyGate<'a> {
164 pub fn new(registry: &'a ConfigRegistry) -> Self {
165 Self { registry }
166 }
167
168 pub fn check_mutation(
172 &self,
173 auth: &AuthStore,
174 actor: &UserId,
175 ctx: &EvalContext,
176 policy_id: &str,
177 op: PolicyOp,
178 ) -> ManagedPolicyDecision {
179 let Some(entry) = self.lookup_governing_entry(policy_id) else {
180 return ManagedPolicyDecision::PassThrough {
181 policy_id: policy_id.to_string(),
182 op,
183 };
184 };
185 if !entry.managed {
186 return ManagedPolicyDecision::PassThrough {
187 policy_id: policy_id.to_string(),
188 op,
189 };
190 }
191
192 let (kind, name) = split_required_resource(&entry.required_resource, policy_id);
193 let matched_resource = format!("{kind}:{name}");
194 let matched_action = op.action().to_string();
195
196 if !(ctx.principal_is_system_owned && ctx.principal_is_platform_scoped) {
197 return ManagedPolicyDecision::Deny {
198 entry_id: entry.id.clone(),
199 entry_version: entry.version,
200 op,
201 matched_action,
202 matched_resource,
203 reason: DenyReason::NotStructurallyEligible {
204 is_system_owned: ctx.principal_is_system_owned,
205 is_platform_scoped: ctx.principal_is_platform_scoped,
206 },
207 };
208 }
209
210 let resource = ResourceRef::new(kind, name);
211 if !auth.check_policy_authz(actor, op.action(), &resource, ctx) {
212 return ManagedPolicyDecision::Deny {
213 entry_id: entry.id.clone(),
214 entry_version: entry.version,
215 op,
216 matched_action,
217 matched_resource,
218 reason: DenyReason::PolicyDenied,
219 };
220 }
221
222 ManagedPolicyDecision::Allow {
223 entry_id: entry.id,
224 entry_version: entry.version,
225 op,
226 matched_action,
227 matched_resource,
228 evidence: entry.evidence_requirement,
229 }
230 }
231
232 fn lookup_governing_entry(&self, policy_id: &str) -> Option<ConfigRegistryEntry> {
238 let e = self.registry.get_active(policy_id)?;
239 if e.resource_type == RESOURCE_TYPE_POLICY {
240 Some(e)
241 } else {
242 None
243 }
244 }
245}
246
247fn split_required_resource<'a>(s: &'a str, policy_id: &'a str) -> (&'a str, &'a str) {
253 match s.split_once(':') {
254 Some((k, n)) if !k.is_empty() => (k, n),
255 _ => ("policy", policy_id),
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::auth::policies::Policy;
263 use crate::auth::registry::{ConfigRegistryDraft, Mutability, Sensitivity};
264 use crate::auth::store::PrincipalRef;
265 use crate::auth::{AuthConfig, Role};
266 use std::sync::Arc;
267
268 fn store() -> Arc<AuthStore> {
269 Arc::new(AuthStore::new(AuthConfig::default()))
270 }
271
272 fn registry_admin_ctx() -> EvalContext {
273 EvalContext {
274 principal_tenant: None,
275 current_tenant: None,
276 peer_ip: None,
277 mfa_present: false,
278 now_ms: 1_700_000_000_000,
279 principal_is_admin_role: true,
280 principal_is_system_owned: false,
281 principal_is_platform_scoped: true,
282 }
283 }
284
285 fn allow_all_registry(id: &str) -> Policy {
286 Policy::from_json_str(&format!(
287 r#"{{
288 "id": "{id}",
289 "version": 1,
290 "statements": [{{
291 "effect": "allow",
292 "actions": ["red.registry:*"],
293 "resources": ["registry:*"]
294 }}]
295 }}"#
296 ))
297 .unwrap()
298 }
299
300 fn allow_all_policies(id: &str) -> Policy {
301 Policy::from_json_str(&format!(
302 r#"{{
303 "id": "{id}",
304 "version": 1,
305 "statements": [{{
306 "effect": "allow",
307 "actions": ["policy:*"],
308 "resources": ["*"]
309 }}]
310 }}"#
311 ))
312 .unwrap()
313 }
314
315 fn allow_policy_action(id: &str, action: &str, resource_glob: &str) -> Policy {
316 Policy::from_json_str(&format!(
317 r#"{{
318 "id": "{id}",
319 "version": 1,
320 "statements": [{{
321 "effect": "allow",
322 "actions": ["{action}"],
323 "resources": ["{resource_glob}"]
324 }}]
325 }}"#
326 ))
327 .unwrap()
328 }
329
330 fn deny_policy_action(id: &str, action: &str, resource_glob: &str) -> Policy {
331 Policy::from_json_str(&format!(
332 r#"{{
333 "id": "{id}",
334 "version": 1,
335 "statements": [{{
336 "effect": "deny",
337 "actions": ["{action}"],
338 "resources": ["{resource_glob}"]
339 }}]
340 }}"#
341 ))
342 .unwrap()
343 }
344
345 fn seed_registry_admin(store: &Arc<AuthStore>) -> UserId {
346 store.create_user("seeder", "p", Role::Admin).unwrap();
347 let uid = UserId::platform("seeder");
348 store.put_policy(allow_all_registry("p-reg-allow")).unwrap();
349 store
350 .attach_policy(PrincipalRef::User(uid.clone()), "p-reg-allow")
351 .unwrap();
352 uid
353 }
354
355 fn managed_policy_draft(id: &str) -> ConfigRegistryDraft {
356 ConfigRegistryDraft {
357 id: id.to_string(),
358 resource_type: RESOURCE_TYPE_POLICY.into(),
359 schema: "iam-policy/v1".into(),
360 mutability: Mutability::MutableViaGovernance,
361 sensitivity: Sensitivity::Internal,
362 managed: true,
363 required_action: "policy:*".into(),
367 required_resource: format!("policy:{id}"),
368 evidence_requirement: EvidenceRequirement::Metadata,
369 }
370 }
371
372 fn unmanaged_policy_draft(id: &str) -> ConfigRegistryDraft {
373 let mut d = managed_policy_draft(id);
374 d.managed = false;
375 d
376 }
377
378 #[test]
381 fn policy_can_be_installed_as_managed_through_registry() {
382 let store = store();
386 let seeder = seed_registry_admin(&store);
387 let reg = ConfigRegistry::new();
388 let entry = reg
389 .register(
390 &store,
391 &seeder,
392 ®istry_admin_ctx(),
393 managed_policy_draft("p-baseline-readonly"),
394 1_000,
395 )
396 .expect("register");
397 assert!(entry.managed);
398 assert_eq!(entry.resource_type, RESOURCE_TYPE_POLICY);
399
400 let other = reg
402 .register(
403 &store,
404 &seeder,
405 ®istry_admin_ctx(),
406 unmanaged_policy_draft("p-tenant-custom"),
407 1_000,
408 )
409 .unwrap();
410 assert!(!other.managed);
411 }
412
413 #[test]
416 fn ordinary_allow_all_user_cannot_put_drop_attach_or_detach_managed_policy() {
417 let store = store();
421 let seeder = seed_registry_admin(&store);
422 store.create_user("alice", "p", Role::Admin).unwrap();
423 let alice = UserId::platform("alice");
424 store
425 .put_policy(allow_all_policies("p-alice-allow"))
426 .unwrap();
427 store
428 .attach_policy(PrincipalRef::User(alice.clone()), "p-alice-allow")
429 .unwrap();
430
431 let reg = ConfigRegistry::new();
432 reg.register(
433 &store,
434 &seeder,
435 ®istry_admin_ctx(),
436 managed_policy_draft("p-baseline-readonly"),
437 1_000,
438 )
439 .unwrap();
440
441 let gate = ManagedPolicyGate::new(®);
442 let ctx = EvalContext {
443 principal_tenant: None,
444 current_tenant: None,
445 peer_ip: None,
446 mfa_present: false,
447 now_ms: 1_700_000_000_001,
448 principal_is_admin_role: true,
449 principal_is_system_owned: false, principal_is_platform_scoped: true,
451 };
452 for op in [
453 PolicyOp::Put,
454 PolicyOp::Drop,
455 PolicyOp::Attach,
456 PolicyOp::Detach,
457 ] {
458 let decision = gate.check_mutation(&store, &alice, &ctx, "p-baseline-readonly", op);
459 match decision {
460 ManagedPolicyDecision::Deny {
461 entry_id,
462 matched_action,
463 matched_resource,
464 reason,
465 op: got_op,
466 ..
467 } => {
468 assert_eq!(entry_id, "p-baseline-readonly");
469 assert_eq!(got_op, op);
470 assert_eq!(matched_action, op.action());
471 assert_eq!(matched_resource, "policy:p-baseline-readonly");
472 assert!(
473 matches!(
474 reason,
475 DenyReason::NotStructurallyEligible {
476 is_system_owned: false,
477 is_platform_scoped: true
478 }
479 ),
480 "op={op:?} got {reason:?}"
481 );
482 }
483 other => panic!("expected Deny for {op:?}, got {other:?}"),
484 }
485 }
486 }
487
488 #[test]
491 fn re_putting_managed_policy_without_managed_metadata_still_blocked() {
492 let store = store();
498 let seeder = seed_registry_admin(&store);
499 store.create_user("alice", "p", Role::Admin).unwrap();
500 let alice = UserId::platform("alice");
501 store
502 .put_policy(allow_all_policies("p-alice-allow"))
503 .unwrap();
504 store
505 .attach_policy(PrincipalRef::User(alice.clone()), "p-alice-allow")
506 .unwrap();
507
508 let reg = ConfigRegistry::new();
509 reg.register(
510 &store,
511 &seeder,
512 ®istry_admin_ctx(),
513 managed_policy_draft("p-baseline-readonly"),
514 1_000,
515 )
516 .unwrap();
517
518 let gate = ManagedPolicyGate::new(®);
519 let ctx = EvalContext {
520 principal_tenant: None,
521 current_tenant: None,
522 peer_ip: None,
523 mfa_present: false,
524 now_ms: 1_700_000_000_002,
525 principal_is_admin_role: true,
526 principal_is_system_owned: false,
527 principal_is_platform_scoped: true,
528 };
529
530 let d1 = gate.check_mutation(&store, &alice, &ctx, "p-baseline-readonly", PolicyOp::Put);
532 assert!(matches!(d1, ManagedPolicyDecision::Deny { .. }), "{d1:?}");
533
534 let d2 = gate.check_mutation(&store, &alice, &ctx, "p-baseline-readonly", PolicyOp::Put);
539 assert!(matches!(d2, ManagedPolicyDecision::Deny { .. }), "{d2:?}");
540
541 assert_eq!(reg.get_active("p-baseline-readonly").unwrap().version, 1);
542 assert!(reg.get_active("p-baseline-readonly").unwrap().managed);
543 }
544
545 #[test]
548 fn structurally_eligible_caller_with_matching_policy_is_allowed() {
549 let store = store();
550 let seeder = seed_registry_admin(&store);
551 store
552 .create_system_user("ops", "p", Role::Admin, None)
553 .expect("create system-owned user");
554 let ops = UserId::platform("ops");
555 store
557 .put_policy(allow_policy_action(
558 "p-ops-policy",
559 "policy:*",
560 "policy:p-baseline-readonly",
561 ))
562 .unwrap();
563 store
564 .attach_policy(PrincipalRef::User(ops.clone()), "p-ops-policy")
565 .unwrap();
566
567 let reg = ConfigRegistry::new();
568 reg.register(
569 &store,
570 &seeder,
571 ®istry_admin_ctx(),
572 managed_policy_draft("p-baseline-readonly"),
573 1_000,
574 )
575 .unwrap();
576
577 let gate = ManagedPolicyGate::new(®);
578 let ctx = EvalContext {
579 principal_tenant: None,
580 current_tenant: None,
581 peer_ip: None,
582 mfa_present: false,
583 now_ms: 1_700_000_000_003,
584 principal_is_admin_role: true,
585 principal_is_system_owned: true,
586 principal_is_platform_scoped: true,
587 };
588 for op in [
589 PolicyOp::Put,
590 PolicyOp::Drop,
591 PolicyOp::Attach,
592 PolicyOp::Detach,
593 ] {
594 let decision = gate.check_mutation(&store, &ops, &ctx, "p-baseline-readonly", op);
595 assert!(
596 matches!(decision, ManagedPolicyDecision::Allow { .. }),
597 "op={op:?} got {decision:?}"
598 );
599 assert!(decision.permitted());
600 }
601 }
602
603 #[test]
604 fn structurally_eligible_caller_without_matching_policy_is_policy_denied() {
605 let store = store();
609 let seeder = seed_registry_admin(&store);
610 store
611 .create_system_user("ops", "p", Role::Write, None)
612 .unwrap();
613 let ops = UserId::platform("ops");
614 store
617 .put_policy(
618 Policy::from_json_str(
619 r#"{"id":"p-unrelated","version":1,"statements":[{"effect":"allow","actions":["select"],"resources":["table:public.x"]}]}"#,
620 )
621 .unwrap(),
622 )
623 .unwrap();
624 store
625 .attach_policy(PrincipalRef::User(ops.clone()), "p-unrelated")
626 .unwrap();
627
628 let reg = ConfigRegistry::new();
629 reg.register(
630 &store,
631 &seeder,
632 ®istry_admin_ctx(),
633 managed_policy_draft("p-baseline-readonly"),
634 1_000,
635 )
636 .unwrap();
637
638 let gate = ManagedPolicyGate::new(®);
639 let ctx = EvalContext {
640 principal_tenant: None,
641 current_tenant: None,
642 peer_ip: None,
643 mfa_present: false,
644 now_ms: 1_700_000_000_004,
645 principal_is_admin_role: false,
646 principal_is_system_owned: true,
647 principal_is_platform_scoped: true,
648 };
649 let decision =
650 gate.check_mutation(&store, &ops, &ctx, "p-baseline-readonly", PolicyOp::Put);
651 match decision {
652 ManagedPolicyDecision::Deny { reason, .. } => {
653 assert!(matches!(reason, DenyReason::PolicyDenied), "got {reason:?}");
654 }
655 other => panic!("expected Deny(PolicyDenied), got {other:?}"),
656 }
657 }
658
659 #[test]
660 fn explicit_deny_overrides_structural_allow() {
661 let store = store();
665 let seeder = seed_registry_admin(&store);
666 store
667 .create_system_user("ops", "p", Role::Admin, None)
668 .unwrap();
669 let ops = UserId::platform("ops");
670 store.put_policy(allow_all_policies("p-allow")).unwrap();
671 store
672 .put_policy(deny_policy_action(
673 "p-deny",
674 "policy:put",
675 "policy:p-baseline-readonly",
676 ))
677 .unwrap();
678 store
679 .attach_policy(PrincipalRef::User(ops.clone()), "p-allow")
680 .unwrap();
681 store
682 .attach_policy(PrincipalRef::User(ops.clone()), "p-deny")
683 .unwrap();
684
685 let reg = ConfigRegistry::new();
686 reg.register(
687 &store,
688 &seeder,
689 ®istry_admin_ctx(),
690 managed_policy_draft("p-baseline-readonly"),
691 1_000,
692 )
693 .unwrap();
694
695 let gate = ManagedPolicyGate::new(®);
696 let ctx = EvalContext {
697 principal_tenant: None,
698 current_tenant: None,
699 peer_ip: None,
700 mfa_present: false,
701 now_ms: 1_700_000_000_005,
702 principal_is_admin_role: true,
703 principal_is_system_owned: true,
704 principal_is_platform_scoped: true,
705 };
706 let decision =
709 gate.check_mutation(&store, &ops, &ctx, "p-baseline-readonly", PolicyOp::Put);
710 match decision {
711 ManagedPolicyDecision::Deny { reason, .. } => {
712 assert!(matches!(reason, DenyReason::PolicyDenied), "got {reason:?}");
713 }
714 other => panic!("expected Deny(PolicyDenied), got {other:?}"),
715 }
716 let drop_decision =
719 gate.check_mutation(&store, &ops, &ctx, "p-baseline-readonly", PolicyOp::Drop);
720 assert!(
721 matches!(drop_decision, ManagedPolicyDecision::Allow { .. }),
722 "got {drop_decision:?}"
723 );
724 }
725
726 #[test]
729 fn deny_carries_resource_and_reason_for_audit_hook() {
730 let store = store();
734 let seeder = seed_registry_admin(&store);
735 store.create_user("alice", "p", Role::Admin).unwrap();
736 let alice = UserId::platform("alice");
737
738 let reg = ConfigRegistry::new();
739 let mut draft = managed_policy_draft("p-tenant-isolation");
740 draft.evidence_requirement = EvidenceRequirement::Full;
741 reg.register(&store, &seeder, ®istry_admin_ctx(), draft, 1_000)
742 .unwrap();
743
744 let gate = ManagedPolicyGate::new(®);
745 let ctx = EvalContext {
746 principal_tenant: None,
747 current_tenant: None,
748 peer_ip: None,
749 mfa_present: false,
750 now_ms: 1_700_000_000_006,
751 principal_is_admin_role: true,
752 principal_is_system_owned: false,
753 principal_is_platform_scoped: true,
754 };
755 let decision =
756 gate.check_mutation(&store, &alice, &ctx, "p-tenant-isolation", PolicyOp::Attach);
757 match decision {
758 ManagedPolicyDecision::Deny {
759 entry_id,
760 entry_version,
761 op,
762 matched_action,
763 matched_resource,
764 reason,
765 } => {
766 assert_eq!(entry_id, "p-tenant-isolation");
767 assert_eq!(entry_version, 1);
768 assert_eq!(op, PolicyOp::Attach);
769 assert_eq!(matched_action, "policy:attach");
770 assert_eq!(matched_resource, "policy:p-tenant-isolation");
771 let rendered = reason.to_string();
772 assert!(
773 rendered.contains("structurally eligible"),
774 "reason should be audit-renderable: {rendered}"
775 );
776 }
777 other => panic!("expected Deny, got {other:?}"),
778 }
779 }
780
781 #[test]
784 fn unmanaged_policy_passes_through() {
785 let store = store();
788 let seeder = seed_registry_admin(&store);
789 let reg = ConfigRegistry::new();
790 reg.register(
791 &store,
792 &seeder,
793 ®istry_admin_ctx(),
794 unmanaged_policy_draft("p-tenant-custom"),
795 1_000,
796 )
797 .unwrap();
798
799 let gate = ManagedPolicyGate::new(®);
800 let alice = UserId::platform("alice");
801 let d = gate.check_mutation(
802 &store,
803 &alice,
804 &EvalContext::default(),
805 "p-tenant-custom",
806 PolicyOp::Put,
807 );
808 assert!(matches!(d, ManagedPolicyDecision::PassThrough { .. }));
809 assert!(d.permitted());
810 }
811
812 #[test]
813 fn unknown_policy_passes_through() {
814 let store = store();
815 let _ = seed_registry_admin(&store);
816 let reg = ConfigRegistry::new();
817 let gate = ManagedPolicyGate::new(®);
818 let alice = UserId::platform("alice");
819 let d = gate.check_mutation(
820 &store,
821 &alice,
822 &EvalContext::default(),
823 "p-anything",
824 PolicyOp::Drop,
825 );
826 assert!(matches!(d, ManagedPolicyDecision::PassThrough { .. }));
827 }
828
829 #[test]
830 fn entry_with_unrelated_resource_type_does_not_gate_policy_mutations() {
831 let store = store();
835 let seeder = seed_registry_admin(&store);
836 let reg = ConfigRegistry::new();
837 let mut d = managed_policy_draft("p-baseline-readonly");
838 d.resource_type = "config_key".into();
839 reg.register(&store, &seeder, ®istry_admin_ctx(), d, 1_000)
840 .unwrap();
841
842 let gate = ManagedPolicyGate::new(®);
843 let alice = UserId::platform("alice");
844 let dec = gate.check_mutation(
845 &store,
846 &alice,
847 &EvalContext::default(),
848 "p-baseline-readonly",
849 PolicyOp::Put,
850 );
851 assert!(
852 matches!(dec, ManagedPolicyDecision::PassThrough { .. }),
853 "got {dec:?}"
854 );
855 }
856
857 #[test]
870 fn tenant_scoped_system_owned_user_is_structurally_ineligible_for_managed_policy() {
871 let store = store();
872 let seeder = seed_registry_admin(&store);
873 store
874 .create_system_user("ops", "p", Role::Admin, Some("acme"))
875 .expect("create tenant-scoped system-owned user");
876 let ops_acme = UserId::scoped("acme", "ops");
877 store
880 .put_policy(allow_all_policies("p-ops-acme-allow"))
881 .unwrap();
882 store
883 .attach_policy(PrincipalRef::User(ops_acme.clone()), "p-ops-acme-allow")
884 .unwrap();
885
886 let reg = ConfigRegistry::new();
887 reg.register(
888 &store,
889 &seeder,
890 ®istry_admin_ctx(),
891 managed_policy_draft("p-baseline-readonly"),
892 1_000,
893 )
894 .unwrap();
895
896 let gate = ManagedPolicyGate::new(®);
897 let ctx = EvalContext {
901 principal_tenant: Some("acme".into()),
902 current_tenant: Some("acme".into()),
903 peer_ip: None,
904 mfa_present: false,
905 now_ms: 1_700_000_000_100,
906 principal_is_admin_role: true,
907 principal_is_system_owned: true,
908 principal_is_platform_scoped: false,
909 };
910
911 for op in [
912 PolicyOp::Put,
913 PolicyOp::Drop,
914 PolicyOp::Attach,
915 PolicyOp::Detach,
916 ] {
917 let decision = gate.check_mutation(&store, &ops_acme, &ctx, "p-baseline-readonly", op);
918 match decision {
919 ManagedPolicyDecision::Deny {
920 reason:
921 DenyReason::NotStructurallyEligible {
922 is_system_owned,
923 is_platform_scoped,
924 },
925 op: got_op,
926 ..
927 } => {
928 assert_eq!(got_op, op);
929 assert!(is_system_owned, "system_owned flag should propagate");
930 assert!(
931 !is_platform_scoped,
932 "tenant-scoped principal must report platform_scoped=false"
933 );
934 }
935 other => panic!("expected NotStructurallyEligible Deny for {op:?}, got {other:?}"),
936 }
937 }
938 }
939
940 #[test]
941 fn split_required_resource_handles_bare_string() {
942 assert_eq!(
943 split_required_resource("policy:p-baseline", "p-baseline"),
944 ("policy", "p-baseline")
945 );
946 assert_eq!(
950 split_required_resource("p-anything-else", "p-baseline"),
951 ("policy", "p-baseline")
952 );
953 }
954}