1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7pub use wyrd_spec::auth::{
8 BifrostPermissionScope, BifrostSchemaScope, BifrostTableScope, GatewayAccess, PermissionScope,
9 PermissionScopeError,
10};
11use wyrd_spec::card::agent::BuiltinTool;
12use wyrd_spec::ids::CardUid;
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(try_from = "PermissionWire")]
22pub struct Permission {
23 pub resource: Resource,
25 pub action: Action,
27 pub scope: PermissionScope,
29}
30
31#[derive(Deserialize)]
39#[serde(deny_unknown_fields)]
40struct PermissionWire {
41 resource: Resource,
43 action: Action,
45 scope: PermissionScope,
47}
48
49impl TryFrom<PermissionWire> for Permission {
50 type Error = PermissionScopeError;
51
52 fn try_from(wire: PermissionWire) -> Result<Self, Self::Error> {
61 let permission = Self {
62 resource: wire.resource,
63 action: wire.action,
64 scope: wire.scope,
65 };
66 permission.validate()?;
67 Ok(permission)
68 }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum Resource {
75 Cards,
77 Services,
79 Operators,
81 Verifier,
83 Drift,
85 Artifacts,
87 Audit,
89 Policy,
91 Triggers,
93 ServiceAccounts,
95 Users,
97 IdentityConnections,
106 Tenants,
113 PlatformIdentity,
121 PlatformCredentials,
130 BifrostTable,
132 BifrostRecord,
134 BifrostQuery,
136 BifrostPeer,
142 Gateway,
145 GatewayPayload,
147 Tool,
153 AnyOf(Vec<Resource>),
155 Wildcard,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum Action {
163 Read,
165 Write,
167 Delete,
169 Invoke,
171 Install,
173 Lock,
175 Run,
177 Suspend,
179 Recover,
185 AnyOf(Vec<Action>),
187 Wildcard,
189}
190
191impl Resource {
192 #[must_use]
194 pub fn covers(&self, other: &Resource) -> bool {
195 match (self, other) {
196 (Self::Wildcard, _) => true,
197 (Self::AnyOf(resources), other) => {
198 resources.iter().any(|resource| resource.covers(other))
199 }
200 (self_resource, other_resource) => self_resource == other_resource,
201 }
202 }
203
204 fn members(&self) -> Vec<&Self> {
207 match self {
208 Self::AnyOf(resources) => resources.iter().flat_map(Self::members).collect(),
209 resource => vec![resource],
210 }
211 }
212
213 #[must_use]
219 pub const fn accepts_bifrost_scope(&self) -> bool {
220 matches!(self, Self::BifrostQuery | Self::BifrostRecord)
221 }
222
223 #[must_use]
228 pub fn as_str(&self) -> Option<&'static str> {
229 Some(match self {
230 Self::Cards => "cards",
231 Self::Services => "services",
232 Self::Operators => "operators",
233 Self::Verifier => "verifier",
234 Self::Drift => "drift",
235 Self::Artifacts => "artifacts",
236 Self::Audit => "audit",
237 Self::Policy => "policy",
238 Self::Triggers => "triggers",
239 Self::ServiceAccounts => "service_accounts",
240 Self::Users => "users",
241 Self::IdentityConnections => "identity_connections",
242 Self::Tenants => "tenants",
243 Self::PlatformIdentity => "platform_identity",
244 Self::PlatformCredentials => "platform_credentials",
245 Self::BifrostTable => "bifrost_table",
246 Self::BifrostRecord => "bifrost_record",
247 Self::BifrostQuery => "bifrost_query",
248 Self::BifrostPeer => "bifrost_peer",
249 Self::Gateway => "gateway",
250 Self::GatewayPayload => "gateway_payload",
251 Self::Tool => "tool",
252 Self::Wildcard => "wildcard",
253 Self::AnyOf(_) => return None,
254 })
255 }
256}
257
258impl Action {
259 #[must_use]
261 pub fn covers(&self, other: &Action) -> bool {
262 match (self, other) {
263 (Self::Wildcard, _) => true,
264 (Self::AnyOf(actions), other) => actions.iter().any(|action| action.covers(other)),
265 (self_action, other_action) => self_action == other_action,
266 }
267 }
268
269 fn members(&self) -> Vec<&Self> {
272 match self {
273 Self::AnyOf(actions) => actions.iter().flat_map(Self::members).collect(),
274 action => vec![action],
275 }
276 }
277
278 #[must_use]
283 pub fn as_str(&self) -> Option<&'static str> {
284 Some(match self {
285 Self::Read => "read",
286 Self::Write => "write",
287 Self::Delete => "delete",
288 Self::Invoke => "invoke",
289 Self::Install => "install",
290 Self::Lock => "lock",
291 Self::Run => "run",
292 Self::Suspend => "suspend",
293 Self::Recover => "recover",
294 Self::Wildcard => "wildcard",
295 Self::AnyOf(_) => return None,
296 })
297 }
298}
299
300impl Permission {
301 #[must_use]
303 pub fn covers(&self, required: &Permission) -> bool {
304 self.resource.covers(&required.resource)
305 && self.action.covers(&required.action)
306 && self.scope.covers(&required.scope)
307 }
308
309 #[must_use]
316 pub fn subsumes(&self, other: &Permission) -> bool {
317 self.covers(other) && !(self.scope == PermissionScope::All && other.scope.is_gateway())
318 }
319
320 pub fn validate(&self) -> Result<(), PermissionScopeError> {
340 self.scope.validate()?;
341 let (resource_accepts, required_action) = match &self.scope {
342 PermissionScope::All => return Ok(()),
343 PermissionScope::Bifrost(_) => (
344 self.resource.accepts_bifrost_scope(),
345 if self.resource == Resource::BifrostRecord {
346 Action::Write
347 } else {
348 Action::Read
349 },
350 ),
351 PermissionScope::Gateway(_) => (self.resource == Resource::Gateway, Action::Invoke),
352 PermissionScope::Verifier(_) => (self.resource == Resource::Verifier, Action::Run),
353 PermissionScope::Tool(_) => (self.resource == Resource::Tool, Action::Invoke),
354 };
355 if !resource_accepts {
356 return Err(PermissionScopeError::InvalidIdentifier {
357 field: "resource",
358 value: self.resource.as_str().unwrap_or("any_of").to_owned(),
359 });
360 }
361 if self.action != required_action {
362 return Err(PermissionScopeError::InvalidIdentifier {
363 field: "action",
364 value: self.action.as_str().unwrap_or("any_of").to_owned(),
365 });
366 }
367 Ok(())
368 }
369
370 #[must_use]
372 pub const fn card_read() -> Self {
373 Self {
374 resource: Resource::Cards,
375 action: Action::Read,
376 scope: PermissionScope::All,
377 }
378 }
379
380 #[must_use]
382 pub const fn card_write() -> Self {
383 Self {
384 resource: Resource::Cards,
385 action: Action::Write,
386 scope: PermissionScope::All,
387 }
388 }
389
390 #[must_use]
392 pub const fn card_delete() -> Self {
393 Self {
394 resource: Resource::Cards,
395 action: Action::Delete,
396 scope: PermissionScope::All,
397 }
398 }
399
400 #[must_use]
402 pub const fn artifact_read() -> Self {
403 Self {
404 resource: Resource::Artifacts,
405 action: Action::Read,
406 scope: PermissionScope::All,
407 }
408 }
409
410 #[must_use]
412 pub const fn artifact_write() -> Self {
413 Self {
414 resource: Resource::Artifacts,
415 action: Action::Write,
416 scope: PermissionScope::All,
417 }
418 }
419
420 #[must_use]
422 pub const fn service_install() -> Self {
423 Self {
424 resource: Resource::Services,
425 action: Action::Install,
426 scope: PermissionScope::All,
427 }
428 }
429
430 #[must_use]
432 pub const fn service_accounts_write() -> Self {
433 Self {
434 resource: Resource::ServiceAccounts,
435 action: Action::Write,
436 scope: PermissionScope::All,
437 }
438 }
439
440 #[must_use]
442 pub const fn identity_connections_write() -> Self {
443 Self {
444 resource: Resource::IdentityConnections,
445 action: Action::Write,
446 scope: PermissionScope::All,
447 }
448 }
449
450 #[must_use]
452 pub const fn operators_read() -> Self {
453 Self {
454 resource: Resource::Operators,
455 action: Action::Read,
456 scope: PermissionScope::All,
457 }
458 }
459
460 #[must_use]
462 pub const fn operators_write() -> Self {
463 Self {
464 resource: Resource::Operators,
465 action: Action::Write,
466 scope: PermissionScope::All,
467 }
468 }
469
470 #[must_use]
472 pub const fn operator_invoke() -> Self {
473 Self {
474 resource: Resource::Operators,
475 action: Action::Invoke,
476 scope: PermissionScope::All,
477 }
478 }
479
480 #[must_use]
485 pub const fn verifier_run(verifier: CardUid) -> Self {
486 Self {
487 resource: Resource::Verifier,
488 action: Action::Run,
489 scope: PermissionScope::Verifier(verifier),
490 }
491 }
492
493 #[must_use]
498 pub const fn verifier_run_any() -> Self {
499 Self {
500 resource: Resource::Verifier,
501 action: Action::Run,
502 scope: PermissionScope::All,
503 }
504 }
505
506 #[must_use]
508 pub const fn trigger_write() -> Self {
509 Self {
510 resource: Resource::Triggers,
511 action: Action::Write,
512 scope: PermissionScope::All,
513 }
514 }
515
516 #[must_use]
518 pub const fn audit_read() -> Self {
519 Self {
520 resource: Resource::Audit,
521 action: Action::Read,
522 scope: PermissionScope::All,
523 }
524 }
525
526 #[must_use]
528 pub const fn policy_lock() -> Self {
529 Self {
530 resource: Resource::Policy,
531 action: Action::Lock,
532 scope: PermissionScope::All,
533 }
534 }
535
536 #[must_use]
538 pub const fn tenant_create() -> Self {
539 Self {
540 resource: Resource::Tenants,
541 action: Action::Write,
542 scope: PermissionScope::All,
543 }
544 }
545
546 #[must_use]
548 pub const fn tenant_read() -> Self {
549 Self {
550 resource: Resource::Tenants,
551 action: Action::Read,
552 scope: PermissionScope::All,
553 }
554 }
555
556 #[must_use]
558 pub const fn tenant_suspend() -> Self {
559 Self {
560 resource: Resource::Tenants,
561 action: Action::Suspend,
562 scope: PermissionScope::All,
563 }
564 }
565
566 #[must_use]
572 pub const fn tenant_recover_admin() -> Self {
573 Self {
574 resource: Resource::Tenants,
575 action: Action::Recover,
576 scope: PermissionScope::All,
577 }
578 }
579
580 #[must_use]
585 pub const fn platform_identity_read() -> Self {
586 Self {
587 resource: Resource::PlatformIdentity,
588 action: Action::Read,
589 scope: PermissionScope::All,
590 }
591 }
592
593 #[must_use]
600 pub const fn platform_identity_write() -> Self {
601 Self {
602 resource: Resource::PlatformIdentity,
603 action: Action::Write,
604 scope: PermissionScope::All,
605 }
606 }
607
608 #[must_use]
613 pub const fn platform_credential_read() -> Self {
614 Self {
615 resource: Resource::PlatformCredentials,
616 action: Action::Read,
617 scope: PermissionScope::All,
618 }
619 }
620
621 #[must_use]
628 pub const fn platform_credential_write() -> Self {
629 Self {
630 resource: Resource::PlatformCredentials,
631 action: Action::Write,
632 scope: PermissionScope::All,
633 }
634 }
635
636 #[must_use]
638 pub const fn users_manage() -> Self {
639 Self {
640 resource: Resource::Users,
641 action: Action::Write,
642 scope: PermissionScope::All,
643 }
644 }
645
646 #[must_use]
648 pub const fn bifrost_record_write() -> Self {
649 Self {
650 resource: Resource::BifrostRecord,
651 action: Action::Write,
652 scope: PermissionScope::All,
653 }
654 }
655
656 #[must_use]
658 pub const fn bifrost_peer_invoke() -> Self {
659 Self {
660 resource: Resource::BifrostPeer,
661 action: Action::Invoke,
662 scope: PermissionScope::All,
663 }
664 }
665
666 #[must_use]
668 pub const fn bifrost_table_read() -> Self {
669 Self {
670 resource: Resource::BifrostTable,
671 action: Action::Read,
672 scope: PermissionScope::All,
673 }
674 }
675
676 #[must_use]
678 pub const fn bifrost_table_write() -> Self {
679 Self {
680 resource: Resource::BifrostTable,
681 action: Action::Write,
682 scope: PermissionScope::All,
683 }
684 }
685
686 #[must_use]
688 pub const fn bifrost_table_install() -> Self {
689 Self {
690 resource: Resource::BifrostTable,
691 action: Action::Install,
692 scope: PermissionScope::All,
693 }
694 }
695
696 #[must_use]
698 pub const fn bifrost_query_read() -> Self {
699 Self {
700 resource: Resource::BifrostQuery,
701 action: Action::Read,
702 scope: PermissionScope::All,
703 }
704 }
705
706 #[must_use]
708 pub const fn gateway_read() -> Self {
709 Self {
710 resource: Resource::Gateway,
711 action: Action::Read,
712 scope: PermissionScope::All,
713 }
714 }
715
716 #[must_use]
718 pub const fn gateway_write() -> Self {
719 Self {
720 resource: Resource::Gateway,
721 action: Action::Write,
722 scope: PermissionScope::All,
723 }
724 }
725
726 #[must_use]
728 pub const fn gateway_delete() -> Self {
729 Self {
730 resource: Resource::Gateway,
731 action: Action::Delete,
732 scope: PermissionScope::All,
733 }
734 }
735
736 #[must_use]
738 pub const fn gateway_invoke(access: GatewayAccess) -> Self {
739 Self {
740 resource: Resource::Gateway,
741 action: Action::Invoke,
742 scope: PermissionScope::Gateway(access),
743 }
744 }
745
746 #[must_use]
752 pub const fn gateway_invoke_any() -> Self {
753 Self {
754 resource: Resource::Gateway,
755 action: Action::Invoke,
756 scope: PermissionScope::All,
757 }
758 }
759
760 #[must_use]
762 pub const fn gateway_payload_read() -> Self {
763 Self {
764 resource: Resource::GatewayPayload,
765 action: Action::Read,
766 scope: PermissionScope::All,
767 }
768 }
769
770 #[must_use]
776 pub const fn tool_invoke(tool: BuiltinTool) -> Self {
777 Self {
778 resource: Resource::Tool,
779 action: Action::Invoke,
780 scope: PermissionScope::Tool(tool),
781 }
782 }
783
784 #[must_use]
786 pub const fn tool_invoke_any() -> Self {
787 Self {
788 resource: Resource::Tool,
789 action: Action::Invoke,
790 scope: PermissionScope::All,
791 }
792 }
793
794 #[must_use]
796 pub const fn wildcard() -> Self {
797 Self {
798 resource: Resource::Wildcard,
799 action: Action::Wildcard,
800 scope: PermissionScope::All,
801 }
802 }
803}
804
805impl fmt::Display for Permission {
812 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813 let resource = self.resource.as_str().ok_or(fmt::Error)?;
814 let action = self.action.as_str().ok_or(fmt::Error)?;
815 write!(f, "{resource}:{action}")
816 }
817}
818
819#[derive(Debug, thiserror::Error)]
821#[error("permission must be formatted as resource:action")]
822pub struct PermissionParseError;
823
824impl FromStr for Permission {
831 type Err = PermissionParseError;
832
833 fn from_str(value: &str) -> Result<Self, Self::Err> {
834 let Some((resource, action)) = value.split_once(':') else {
835 return Err(PermissionParseError);
836 };
837 Ok(Self {
838 resource: parse_resource(resource)?,
839 action: parse_action(action)?,
840 scope: PermissionScope::All,
841 })
842 }
843}
844
845fn parse_resource(value: &str) -> Result<Resource, PermissionParseError> {
846 Ok(match value {
847 "cards" => Resource::Cards,
848 "services" => Resource::Services,
849 "operators" => Resource::Operators,
850 "verifier" => Resource::Verifier,
851 "drift" => Resource::Drift,
852 "artifacts" => Resource::Artifacts,
853 "audit" => Resource::Audit,
854 "policy" => Resource::Policy,
855 "triggers" => Resource::Triggers,
856 "service_accounts" => Resource::ServiceAccounts,
857 "users" => Resource::Users,
858 "identity_connections" => Resource::IdentityConnections,
859 "bifrost_table" => Resource::BifrostTable,
860 "bifrost_record" => Resource::BifrostRecord,
861 "bifrost_peer" => Resource::BifrostPeer,
862 "bifrost_query" => Resource::BifrostQuery,
863 "gateway" => Resource::Gateway,
864 "gateway_payload" => Resource::GatewayPayload,
865 "tool" => Resource::Tool,
866 "wildcard" => Resource::Wildcard,
867 _ => return Err(PermissionParseError),
868 })
869}
870
871fn parse_action(value: &str) -> Result<Action, PermissionParseError> {
872 Ok(match value {
873 "read" => Action::Read,
874 "write" => Action::Write,
875 "delete" => Action::Delete,
876 "invoke" => Action::Invoke,
877 "install" => Action::Install,
878 "lock" => Action::Lock,
879 "run" => Action::Run,
880 "wildcard" => Action::Wildcard,
881 _ => return Err(PermissionParseError),
882 })
883}
884
885#[derive(Debug, thiserror::Error)]
890#[error("shared authority for {permission} depends on deployment access and cannot be represented")]
891pub struct UnrepresentableIntersection {
892 pub permission: Permission,
894}
895
896#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
898#[serde(transparent)]
899pub struct PermissionSet(Vec<Permission>);
900
901impl PermissionSet {
902 #[must_use]
904 pub fn new() -> Self {
905 Self(Vec::new())
906 }
907
908 pub fn insert(&mut self, permission: Permission) {
913 if self.0.iter().any(|existing| existing.subsumes(&permission)) {
914 return;
915 }
916 self.0.retain(|existing| !permission.subsumes(existing));
917 self.0.push(permission);
918 }
919
920 #[must_use]
922 pub fn contains(&self, required: &Permission) -> bool {
923 self.0.iter().any(|permission| permission.covers(required))
924 }
925
926 #[must_use]
932 pub fn contains_object_scoped(&self, required: &Permission) -> bool {
933 self.0.iter().any(|permission| {
934 permission.scope != PermissionScope::All && permission.covers(required)
935 })
936 }
937
938 #[must_use]
946 pub fn covers_operation(&self, resource: &Resource, action: &Action) -> bool {
947 self.0.iter().any(|permission| {
948 permission.resource.covers(resource) && permission.action.covers(action)
949 })
950 }
951
952 pub fn intersection(&self, other: &Self) -> Result<Self, UnrepresentableIntersection> {
976 let mut meet = Self::new();
977 let mut open_only = Vec::new();
978 for left in &self.0 {
979 for right in &other.0 {
980 let Some(scope) = narrower(&left.scope, &right.scope, PermissionScope::covers)
981 else {
982 continue;
983 };
984 let conditional = scope.is_gateway()
985 && (left.scope == PermissionScope::All || right.scope == PermissionScope::All);
986 for resource in meets(
987 &left.resource.members(),
988 &right.resource.members(),
989 Resource::covers,
990 ) {
991 for action in meets(
992 &left.action.members(),
993 &right.action.members(),
994 Action::covers,
995 ) {
996 let permission = Permission {
997 resource: resource.clone(),
998 action: action.clone(),
999 scope: scope.clone(),
1000 };
1001 if permission.validate().is_err() {
1002 continue;
1003 }
1004 if conditional {
1005 open_only.push(permission);
1006 } else {
1007 meet.insert(permission);
1008 }
1009 }
1010 }
1011 }
1012 }
1013 match open_only
1014 .into_iter()
1015 .find(|permission| !meet.contains(permission))
1016 {
1017 Some(permission) => Err(UnrepresentableIntersection { permission }),
1018 None => Ok(meet),
1019 }
1020 }
1021
1022 pub fn iter(&self) -> impl Iterator<Item = &Permission> {
1024 self.0.iter()
1025 }
1026
1027 #[must_use]
1029 pub fn len(&self) -> usize {
1030 self.0.len()
1031 }
1032
1033 #[must_use]
1035 pub fn is_empty(&self) -> bool {
1036 self.0.is_empty()
1037 }
1038}
1039
1040fn narrower<'a, T>(left: &'a T, right: &'a T, covers: fn(&T, &T) -> bool) -> Option<&'a T> {
1046 if covers(left, right) {
1047 Some(right)
1048 } else if covers(right, left) {
1049 Some(left)
1050 } else {
1051 None
1052 }
1053}
1054
1055fn meets<'a, T>(left: &[&'a T], right: &[&'a T], covers: fn(&T, &T) -> bool) -> Vec<&'a T> {
1057 left.iter()
1058 .flat_map(|l| right.iter().filter_map(move |r| narrower(*l, *r, covers)))
1059 .collect()
1060}
1061
1062impl FromIterator<Permission> for PermissionSet {
1063 fn from_iter<T: IntoIterator<Item = Permission>>(iter: T) -> Self {
1064 let mut set = Self::new();
1065 for permission in iter {
1066 set.insert(permission);
1067 }
1068 set
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::{Action, Permission, PermissionScope, PermissionSet, Resource};
1075 use serde_json::json;
1076 use wyrd_spec::auth::{BifrostPermissionScope, BifrostSchemaScope, BifrostTableScope};
1077
1078 #[test]
1079 fn serde_round_trip_every_variant() {
1080 let resources = [
1081 Resource::Cards,
1082 Resource::Services,
1083 Resource::Operators,
1084 Resource::Verifier,
1085 Resource::Drift,
1086 Resource::Artifacts,
1087 Resource::Audit,
1088 Resource::Policy,
1089 Resource::Triggers,
1090 Resource::ServiceAccounts,
1091 Resource::Users,
1092 Resource::IdentityConnections,
1093 Resource::AnyOf(vec![Resource::Operators, Resource::Triggers]),
1094 Resource::Wildcard,
1095 ];
1096 for resource in resources {
1097 let value = serde_json::to_value(&resource).expect("resource serializes");
1098 let round_trip: Resource =
1099 serde_json::from_value(value).expect("resource deserializes");
1100 assert_eq!(round_trip, resource);
1101 }
1102
1103 let actions = [
1104 Action::Read,
1105 Action::Write,
1106 Action::Delete,
1107 Action::Invoke,
1108 Action::Install,
1109 Action::Lock,
1110 Action::Run,
1111 Action::AnyOf(vec![Action::Read, Action::Write]),
1112 Action::Wildcard,
1113 ];
1114 for action in actions {
1115 let value = serde_json::to_value(&action).expect("action serializes");
1116 let round_trip: Action = serde_json::from_value(value).expect("action deserializes");
1117 assert_eq!(round_trip, action);
1118 }
1119 }
1120
1121 #[test]
1122 fn bifrost_permissions_round_trip_through_wire_strings() {
1123 for (permission, wire) in [
1124 (Permission::bifrost_record_write(), "bifrost_record:write"),
1125 (Permission::bifrost_table_read(), "bifrost_table:read"),
1126 (Permission::bifrost_table_write(), "bifrost_table:write"),
1127 (Permission::bifrost_query_read(), "bifrost_query:read"),
1128 (Permission::bifrost_peer_invoke(), "bifrost_peer:invoke"),
1129 ] {
1130 assert_eq!(permission.to_string(), wire);
1131 assert_eq!(wire.parse::<Permission>().expect("wire parses"), permission);
1132 let json = serde_json::to_value(&permission).expect("serializes");
1133 assert_eq!(
1134 serde_json::from_value::<Permission>(json).expect("deserializes"),
1135 permission
1136 );
1137 }
1138 }
1139
1140 #[test]
1141 fn bifrost_record_write_is_distinct_from_table_write() {
1142 assert!(!Permission::bifrost_table_write().covers(&Permission::bifrost_record_write()));
1143 assert!(!Permission::bifrost_record_write().covers(&Permission::bifrost_table_write()));
1144 assert!(Permission::wildcard().covers(&Permission::bifrost_record_write()));
1145 }
1146
1147 #[test]
1148 fn covers_wildcard_resource() {
1149 let permission = Permission {
1150 resource: Resource::Wildcard,
1151 action: Action::Read,
1152 scope: PermissionScope::All,
1153 };
1154
1155 assert!(permission.covers(&Permission::card_read()));
1156 assert!(!permission.covers(&Permission::card_write()));
1157 }
1158
1159 #[test]
1160 fn covers_wildcard_action() {
1161 let permission = Permission {
1162 resource: Resource::Cards,
1163 action: Action::Wildcard,
1164 scope: PermissionScope::All,
1165 };
1166
1167 assert!(permission.covers(&Permission::card_read()));
1168 assert!(permission.covers(&Permission::card_write()));
1169 assert!(!permission.covers(&Permission::artifact_write()));
1170 }
1171
1172 #[test]
1178 fn covers_double_wildcard() {
1179 let permission = Permission::wildcard();
1180
1181 assert!(permission.covers(&Permission::card_read()));
1182 assert!(permission.covers(&Permission::card_write()));
1183 assert!(permission.covers(&Permission::bifrost_record_write()));
1184 }
1185
1186 #[test]
1187 fn covers_anyof_resource() {
1188 let permission = Permission {
1189 resource: Resource::AnyOf(vec![Resource::Operators, Resource::Triggers]),
1190 action: Action::Invoke,
1191 scope: PermissionScope::All,
1192 };
1193
1194 assert!(permission.covers(&Permission::operator_invoke()));
1195 assert!(permission.covers(&Permission {
1196 resource: Resource::Triggers,
1197 action: Action::Invoke,
1198 scope: PermissionScope::All,
1199 }));
1200 assert!(!permission.covers(&Permission {
1201 resource: Resource::Cards,
1202 action: Action::Invoke,
1203 scope: PermissionScope::All,
1204 }));
1205 }
1206
1207 #[test]
1208 fn permission_set_subsumption_drops_redundant() {
1209 let mut set = PermissionSet::from_iter([Permission::card_read()]);
1210 set.insert(Permission::wildcard());
1211
1212 assert_eq!(set.len(), 1);
1213 assert_eq!(set.iter().next(), Some(&Permission::wildcard()));
1214 }
1215
1216 #[test]
1217 fn permission_set_subsumption_skips_covered() {
1218 let mut set = PermissionSet::from_iter([Permission::wildcard()]);
1219 set.insert(Permission::card_read());
1220
1221 assert_eq!(set.len(), 1);
1222 assert_eq!(set.iter().next(), Some(&Permission::wildcard()));
1223 }
1224
1225 #[test]
1231 fn permission_set_contains_resolves_through_wildcard() {
1232 let set = PermissionSet::from_iter([Permission::wildcard()]);
1233
1234 assert!(set.contains(&Permission::card_read()));
1235 assert!(set.contains(&Permission::bifrost_record_write()));
1236 }
1237
1238 #[test]
1239 fn permission_set_jsonb_round_trip() {
1240 let permissions = vec![
1241 Permission::card_write(),
1242 Permission::card_read(),
1243 Permission {
1244 resource: Resource::AnyOf(vec![Resource::Operators, Resource::Triggers]),
1245 action: Action::Invoke,
1246 scope: PermissionScope::All,
1247 },
1248 Permission::wildcard(),
1249 ];
1250 let value = serde_json::to_value(&permissions).expect("permissions serialize");
1251
1252 assert_eq!(
1253 value,
1254 json!([
1255 {"resource": "cards", "action": "write", "scope": "all"},
1256 {"resource": "cards", "action": "read", "scope": "all"},
1257 {"resource": {"any_of": ["operators", "triggers"]}, "action": "invoke", "scope": "all"},
1258 {"resource": "wildcard", "action": "wildcard", "scope": "all"}
1259 ])
1260 );
1261
1262 let round_trip: Vec<Permission> =
1263 serde_json::from_value(value).expect("permissions deserialize");
1264 assert_eq!(round_trip, permissions);
1265 }
1266
1267 #[test]
1275 fn gateway_permissions_are_typed_and_scope_checked() {
1276 use super::GatewayAccess;
1277 use wyrd_spec::ids::{ModelId, ProviderDeploymentName};
1278 let openai = ProviderDeploymentName::new("openai").expect("valid deployment");
1279 let provider = GatewayAccess::Deployment {
1280 deployment: openai.clone(),
1281 };
1282 let model = |name: &str| GatewayAccess::Model {
1283 deployment: openai.clone(),
1284 model: ModelId::new(name).expect("valid model"),
1285 };
1286 let required = Permission::gateway_invoke(model("gpt-4o"));
1287 assert!(PermissionSet::from_iter([Permission::gateway_invoke_any()]).contains(&required));
1288 assert!(
1289 !PermissionSet::from_iter([Permission::gateway_invoke_any()])
1290 .contains_object_scoped(&required),
1291 "the unscoped grant is not a grant of the deployment"
1292 );
1293 assert!(
1294 PermissionSet::from_iter([Permission::gateway_invoke(provider.clone())])
1295 .contains_object_scoped(&required)
1296 );
1297
1298 assert_eq!(
1299 Permission::gateway_payload_read().to_string(),
1300 "gateway_payload:read"
1301 );
1302 assert_eq!(
1303 "gateway:write".parse::<Permission>().expect("parses"),
1304 Permission::gateway_write()
1305 );
1306 assert!(
1307 Permission::gateway_invoke(provider.clone())
1308 .covers(&Permission::gateway_invoke(model("gpt-4o")))
1309 );
1310 assert!(
1311 !Permission::gateway_invoke(model("gpt-4o"))
1312 .covers(&Permission::gateway_invoke(model("gpt-4.1")))
1313 );
1314 assert!(
1315 !Permission::gateway_invoke(provider.clone()).covers(&Permission::gateway_read()),
1316 "invoke never grants admin metadata reads"
1317 );
1318 assert_eq!(
1319 serde_json::to_value(Permission::gateway_invoke(model("gpt-4o"))).expect("serializes"),
1320 json!({
1321 "resource": "gateway",
1322 "action": "invoke",
1323 "scope": {"gateway": {"model": {"deployment": "openai", "model": "gpt-4o"}}}
1324 })
1325 );
1326 for (resource, action) in [("gateway", "read"), ("cards", "invoke")] {
1327 assert!(
1328 serde_json::from_value::<Permission>(json!({
1329 "resource": resource,
1330 "action": action,
1331 "scope": {"gateway": {"deployment": {"deployment": "openai"}}}
1332 }))
1333 .is_err(),
1334 "{resource}:{action}"
1335 );
1336 }
1337 }
1338
1339 #[test]
1342 fn verifier_run_is_uid_scoped() {
1343 use wyrd_spec::ids::CardUid;
1344 let uid = |value: &str| CardUid::new(value).expect("valid uuid7");
1345 let bound = uid("01927b4c-0000-7000-8000-000000000001");
1346 let other = uid("01927b4c-0000-7000-8000-000000000002");
1347
1348 assert_eq!(
1349 "verifier:run".parse::<Permission>().expect("parses"),
1350 Permission::verifier_run_any()
1351 );
1352 assert!(Permission::verifier_run_any().covers(&Permission::verifier_run(bound.clone())));
1353 assert!(
1354 Permission::verifier_run(bound.clone())
1355 .covers(&Permission::verifier_run(bound.clone()))
1356 );
1357 assert!(
1358 !Permission::verifier_run(other).covers(&Permission::verifier_run(bound.clone())),
1359 "an unrelated UID grant does not cover"
1360 );
1361 assert_eq!(
1362 serde_json::to_value(Permission::verifier_run(bound.clone())).expect("serializes"),
1363 json!({"resource": "verifier", "action": "run", "scope": {"verifier": bound.as_str()}})
1364 );
1365 for (resource, action) in [("verifier", "read"), ("cards", "run")] {
1366 assert!(
1367 serde_json::from_value::<Permission>(json!({
1368 "resource": resource,
1369 "action": action,
1370 "scope": {"verifier": bound.as_str()}
1371 }))
1372 .is_err(),
1373 "{resource}:{action}"
1374 );
1375 }
1376 }
1377
1378 #[test]
1385 fn tool_invoke_is_exact_name_scoped_and_independent() {
1386 use wyrd_spec::card::agent::BuiltinTool;
1387 let query = Permission::tool_invoke(BuiltinTool::BifrostQuery);
1388 let read = Permission::tool_invoke(BuiltinTool::FsRead);
1389 let global = Permission::tool_invoke_any();
1390
1391 let decode = |value: serde_json::Value| serde_json::from_value::<Permission>(value);
1392 let global_json = json!({"resource": "tool", "action": "invoke", "scope": "all"});
1393 let exact_json =
1394 json!({"resource": "tool", "action": "invoke", "scope": {"tool": "bifrost.query"}});
1395 assert_eq!(decode(global_json.clone()).expect("global"), global);
1396 assert_eq!(decode(exact_json.clone()).expect("exact"), query);
1397 assert_eq!(
1398 serde_json::to_value(&global).expect("serialize"),
1399 global_json
1400 );
1401 assert_eq!(serde_json::to_value(&query).expect("serialize"), exact_json);
1402 assert_eq!("tool:invoke".parse::<Permission>().expect("parses"), global);
1403 for tool in BuiltinTool::ALL {
1404 let value =
1405 json!({"resource": "tool", "action": "invoke", "scope": {"tool": tool.as_str()}});
1406 assert_eq!(
1407 decode(value).expect("built-in"),
1408 Permission::tool_invoke(tool)
1409 );
1410 }
1411 for name in [
1412 "bifrost",
1413 "bifrost.*",
1414 "fs.read/etc",
1415 "/usr/bin/python",
1416 "FS.READ",
1417 "http.fetch",
1418 ] {
1419 let value = json!({"resource": "tool", "action": "invoke", "scope": {"tool": name}});
1420 assert!(decode(value).is_err(), "{name}");
1421 }
1422 for (resource, action) in [
1423 ("tool", "read"),
1424 ("tool", "run"),
1425 ("cards", "invoke"),
1426 ("bifrost_query", "read"),
1427 ("wildcard", "invoke"),
1428 ] {
1429 let value =
1430 json!({"resource": resource, "action": action, "scope": {"tool": "fs.read"}});
1431 assert!(decode(value).is_err(), "{resource}:{action}");
1432 }
1433
1434 assert!(global.covers(&query));
1435 assert!(query.covers(&query));
1436 assert!(!query.covers(&read));
1437 assert!(!query.covers(&global));
1438 assert!(Permission::wildcard().covers(&query));
1439 for domain in [
1440 Permission::card_read(),
1441 Permission::bifrost_query_read(),
1442 Permission::verifier_run_any(),
1443 ] {
1444 assert!(!domain.covers(&query), "{domain}");
1445 assert!(!global.covers(&domain), "{domain}");
1446 }
1447
1448 let set = |grants: &[Permission]| grants.iter().cloned().collect::<PermissionSet>();
1449 let meet = |left: &[Permission], right: &[Permission]| {
1450 set(left).intersection(&set(right)).expect("representable")
1451 };
1452 assert!(meet(std::slice::from_ref(&query), std::slice::from_ref(&read)).is_empty());
1453 for (left, right) in [(&global, &query), (&query, &global)] {
1454 let shared = meet(std::slice::from_ref(left), std::slice::from_ref(right));
1455 assert_eq!(shared.iter().collect::<Vec<_>>(), vec![&query]);
1456 }
1457 let attenuated = meet(&[Permission::wildcard()], std::slice::from_ref(&query));
1458 assert!(attenuated.contains(&query) && !attenuated.contains(&read));
1459
1460 let role: Vec<Permission> =
1461 serde_json::from_value(json!([global_json, exact_json, {"resource": "bifrost_query", "action": "read", "scope": "all"}]))
1462 .expect("role permissions decode");
1463 let role = role.into_iter().collect::<PermissionSet>();
1464 assert!(role.contains(&query) && role.contains(&read));
1465 }
1466
1467 #[test]
1480 fn gateway_grants_survive_merge_and_intersect_exactly() {
1481 use super::GatewayAccess;
1482 use wyrd_spec::ids::{ModelId, ProviderDeploymentName};
1483 let restricted = ProviderDeploymentName::new("restricted").expect("valid deployment");
1484 let deployment = Permission::gateway_invoke(GatewayAccess::Deployment {
1485 deployment: restricted.clone(),
1486 });
1487 let model = Permission::gateway_invoke(GatewayAccess::Model {
1488 deployment: restricted,
1489 model: ModelId::new("gpt-4o").expect("valid model"),
1490 });
1491 let any_resource_invoke = Permission {
1492 resource: Resource::AnyOf(vec![Resource::Gateway, Resource::Cards]),
1493 action: Action::Wildcard,
1494 scope: PermissionScope::All,
1495 };
1496 for broad in [
1497 Permission::gateway_invoke_any(),
1498 Permission::wildcard(),
1499 any_resource_invoke,
1500 ] {
1501 for set in [
1502 PermissionSet::from_iter([broad.clone(), deployment.clone()]),
1503 PermissionSet::from_iter([deployment.clone(), broad.clone()]),
1504 ] {
1505 assert!(set.contains_object_scoped(&model), "{broad} kept explicit");
1506 assert!(set.contains(&Permission::gateway_invoke_any()));
1507 }
1508 }
1509
1510 let all = PermissionSet::from_iter([Permission::gateway_invoke_any()]);
1511 let explicit = PermissionSet::from_iter([deployment.clone()]);
1512 let narrow = PermissionSet::from_iter([model.clone()]);
1513 assert_eq!(all.intersection(&all).expect("representable"), all);
1514 assert_eq!(
1515 explicit.intersection(&narrow).expect("representable"),
1516 narrow
1517 );
1518 assert!(all.intersection(&explicit).is_err());
1519 assert!(explicit.intersection(&all).is_err());
1520 let wildcard = PermissionSet::from_iter([Permission::wildcard()]);
1521 assert!(wildcard.intersection(&narrow).is_err());
1522
1523 let mixed = PermissionSet::from_iter([Permission::gateway_invoke_any(), deployment]);
1524 assert_eq!(mixed.intersection(&narrow).expect("representable"), narrow);
1525 assert_eq!(narrow.intersection(&mixed).expect("representable"), narrow);
1526 assert_eq!(
1527 mixed.intersection(&all).expect("representable"),
1528 all,
1529 "both hold all; only one holds the explicit grant"
1530 );
1531 assert!(
1532 PermissionSet::from_iter([Permission::card_read()])
1533 .intersection(&explicit)
1534 .expect("no overlap is the representable empty result")
1535 .is_empty()
1536 );
1537 }
1538
1539 fn logs_schema_grant() -> Permission {
1541 Permission {
1542 resource: Resource::BifrostQuery,
1543 action: Action::Read,
1544 scope: PermissionScope::Bifrost(BifrostPermissionScope::Schema(BifrostSchemaScope {
1545 catalog: "bifrost".to_owned(),
1546 schema: "logs".to_owned(),
1547 })),
1548 }
1549 }
1550
1551 fn table_requirement(schema: &str, uid: uuid::Uuid) -> Permission {
1553 Permission {
1554 resource: Resource::BifrostQuery,
1555 action: Action::Read,
1556 scope: PermissionScope::Bifrost(BifrostPermissionScope::Table(BifrostTableScope {
1557 catalog: "bifrost".to_owned(),
1558 schema: schema.to_owned(),
1559 table_uid: uid,
1560 })),
1561 }
1562 }
1563
1564 #[test]
1571 fn intersection_keeps_only_the_narrower_shared_authority() {
1572 let uid = uuid::Uuid::from_u128(0x77);
1573 let set = |permissions: Vec<Permission>| permissions.into_iter().collect::<PermissionSet>();
1574
1575 let wildcard = set(vec![Permission::wildcard()]);
1576 let target = set(vec![Permission::card_read(), logs_schema_grant()]);
1577 assert_eq!(
1578 wildcard.intersection(&target).expect("representable"),
1579 target
1580 );
1581 assert_eq!(
1582 target.intersection(&wildcard).expect("representable"),
1583 target
1584 );
1585
1586 let schema = set(vec![logs_schema_grant()]);
1587 let table = set(vec![table_requirement("logs", uid)]);
1588 assert_eq!(schema.intersection(&table).expect("representable"), table);
1589 assert_eq!(table.intersection(&schema).expect("representable"), table);
1590
1591 let other_table = set(vec![table_requirement("logs", uuid::Uuid::from_u128(0x78))]);
1592 assert!(
1593 table
1594 .intersection(&other_table)
1595 .expect("representable")
1596 .is_empty()
1597 );
1598 assert!(
1599 set(vec![table_requirement("traces", uid)])
1600 .intersection(&schema)
1601 .expect("representable")
1602 .is_empty()
1603 );
1604
1605 let all_reads = set(vec![Permission {
1606 resource: Resource::Wildcard,
1607 action: Action::Read,
1608 scope: PermissionScope::All,
1609 }]);
1610 let cards_any = set(vec![Permission {
1611 resource: Resource::AnyOf(vec![Resource::Cards, Resource::Audit]),
1612 action: Action::Wildcard,
1613 scope: PermissionScope::All,
1614 }]);
1615 assert_eq!(
1616 all_reads.intersection(&cards_any).expect("representable"),
1617 set(vec![Permission::card_read(), Permission::audit_read()])
1618 );
1619
1620 assert!(
1621 set(vec![Permission::card_write()])
1622 .intersection(&set(vec![
1623 Permission::card_read(),
1624 Permission::bifrost_record_write()
1625 ]))
1626 .expect("representable")
1627 .is_empty()
1628 );
1629 }
1630
1631 #[test]
1638 fn scoped_permission_json_matches_the_approved_projection() {
1639 let uid = uuid::Uuid::from_u128(0x99);
1640 let cases = [
1641 (
1642 Permission::bifrost_query_read(),
1643 json!({"resource": "bifrost_query", "action": "read", "scope": "all"}),
1644 ),
1645 (
1646 logs_schema_grant(),
1647 json!({
1648 "resource": "bifrost_query",
1649 "action": "read",
1650 "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": "logs"}}}
1651 }),
1652 ),
1653 (
1654 table_requirement("traces", uid),
1655 json!({
1656 "resource": "bifrost_query",
1657 "action": "read",
1658 "scope": {"bifrost": {"table": {
1659 "catalog": "bifrost",
1660 "schema": "traces",
1661 "table_uid": uid.to_string(),
1662 }}}
1663 }),
1664 ),
1665 ];
1666
1667 for (permission, wire) in cases {
1668 assert_eq!(
1669 serde_json::to_value(&permission).expect("permission serializes"),
1670 wire
1671 );
1672 assert_eq!(
1673 serde_json::from_value::<Permission>(wire).expect("permission deserializes"),
1674 permission
1675 );
1676 }
1677 }
1678
1679 #[test]
1689 fn bifrost_scope_applies_to_record_write_and_query_read_only() {
1690 let calls = uuid::Uuid::from_u128(0xca11);
1691 let spans = uuid::Uuid::from_u128(0x5a45);
1692 let table_write = |uid| Permission {
1693 resource: Resource::BifrostRecord,
1694 action: Action::Write,
1695 scope: PermissionScope::Bifrost(BifrostPermissionScope::Table(BifrostTableScope {
1696 catalog: "bifrost".to_owned(),
1697 schema: "gateway".to_owned(),
1698 table_uid: uid,
1699 })),
1700 };
1701 table_write(calls)
1702 .validate()
1703 .expect("scoped record write is valid");
1704 assert!(table_write(calls).covers(&table_write(calls)));
1705 assert!(!table_write(calls).covers(&table_write(spans)));
1706 assert!(
1707 !table_write(calls).covers(&Permission::bifrost_record_write()),
1708 "a table-scoped write never covers the unscoped requirement"
1709 );
1710 assert!(Permission::bifrost_record_write().covers(&table_write(spans)));
1711 let wire = serde_json::to_value(table_write(calls)).expect("serializes");
1712 assert_eq!(
1713 serde_json::from_value::<Permission>(wire).expect("round-trips"),
1714 table_write(calls)
1715 );
1716 for (resource, action) in [
1717 (Resource::BifrostRecord, Action::Read),
1718 (Resource::BifrostQuery, Action::Write),
1719 (Resource::BifrostTable, Action::Write),
1720 ] {
1721 let permission = Permission {
1722 resource,
1723 action,
1724 ..table_write(calls)
1725 };
1726 assert!(permission.validate().is_err(), "{permission}");
1727 }
1728 }
1729
1730 #[test]
1733 fn scope_is_required_and_has_no_compatibility_decoder() {
1734 let error = serde_json::from_value::<Permission>(json!({
1735 "resource": "bifrost_query",
1736 "action": "read"
1737 }))
1738 .expect_err("the unscoped two-field form is not accepted");
1739
1740 assert!(error.to_string().contains("scope"), "{error}");
1741 }
1742
1743 #[test]
1750 fn bifrost_scope_is_rejected_on_an_unrelated_resource() {
1751 for resource in ["cards", "wildcard"] {
1752 let error = serde_json::from_value::<Permission>(json!({
1753 "resource": resource,
1754 "action": "read",
1755 "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": "logs"}}}
1756 }))
1757 .expect_err("a Bifrost object scope needs a Bifrost object resource");
1758
1759 assert!(error.to_string().contains("resource"), "{error}");
1760 }
1761 }
1762
1763 #[test]
1774 fn bifrost_scope_is_rejected_on_a_non_read_action() {
1775 for action in [
1776 json!("write"),
1777 json!("wildcard"),
1778 json!({"any_of": ["read", "write"]}),
1779 ] {
1780 let error = serde_json::from_value::<Permission>(json!({
1781 "resource": "bifrost_query",
1782 "action": action,
1783 "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": "logs"}}}
1784 }))
1785 .expect_err("a Bifrost object scope only applies to an exact read");
1786
1787 assert!(error.to_string().contains("action"), "{error}");
1788 }
1789
1790 serde_json::from_value::<Permission>(json!({
1791 "resource": "bifrost_query",
1792 "action": "read",
1793 "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": "logs"}}}
1794 }))
1795 .expect("exact query read stays valid for Bifrost scope");
1796 }
1797
1798 #[test]
1804 fn malformed_scope_identity_is_rejected_at_decode() {
1805 assert!(
1806 serde_json::from_value::<Permission>(json!({
1807 "resource": "bifrost_query",
1808 "action": "read",
1809 "scope": {"bifrost": {"schema": {"catalog": "bifrost", "schema": ""}}}
1810 }))
1811 .is_err(),
1812 "an empty schema segment is not an object identity"
1813 );
1814 }
1815
1816 #[test]
1819 fn coverage_is_three_axis() {
1820 let logs = uuid::Uuid::from_u128(1);
1821 let traces = uuid::Uuid::from_u128(2);
1822
1823 assert!(logs_schema_grant().covers(&table_requirement("logs", logs)));
1825 assert!(logs_schema_grant().covers(&table_requirement("logs", traces)));
1826 assert!(!logs_schema_grant().covers(&table_requirement("traces", traces)));
1827
1828 let exact = table_requirement("traces", traces);
1830 assert!(exact.covers(&table_requirement("traces", traces)));
1831 assert!(!exact.covers(&table_requirement("traces", logs)));
1832
1833 assert!(Permission::bifrost_query_read().covers(&table_requirement("logs", logs)));
1835 assert!(!Permission::bifrost_table_read().covers(&table_requirement("logs", logs)));
1836
1837 assert!(Permission::wildcard().covers(&table_requirement("logs", logs)));
1839
1840 assert!(!logs_schema_grant().covers(&Permission::bifrost_query_read()));
1842 }
1843
1844 #[test]
1847 fn covers_operation_admits_a_scoped_grant_without_authorizing_an_object() {
1848 let set = PermissionSet::from_iter([logs_schema_grant()]);
1849
1850 assert!(set.covers_operation(&Resource::BifrostQuery, &Action::Read));
1851 assert!(!set.covers_operation(&Resource::BifrostTable, &Action::Read));
1852 assert!(!set.contains(&Permission::bifrost_query_read()));
1853 assert!(set.contains(&table_requirement("logs", uuid::Uuid::from_u128(7))));
1854 assert!(!set.contains(&table_requirement("traces", uuid::Uuid::from_u128(7))));
1855 }
1856
1857 #[test]
1860 fn permission_set_keeps_scopes_that_do_not_subsume_each_other() {
1861 let mut set = PermissionSet::from_iter([logs_schema_grant()]);
1862 set.insert(table_requirement("traces", uuid::Uuid::from_u128(3)));
1863
1864 assert_eq!(set.len(), 2);
1865
1866 set.insert(Permission::bifrost_query_read());
1867
1868 assert_eq!(set.len(), 1);
1869 assert_eq!(set.iter().next(), Some(&Permission::bifrost_query_read()));
1870 }
1871
1872 #[test]
1873 fn permission_wire_token_roundtrips() {
1874 let parsed = "cards:write"
1875 .parse::<Permission>()
1876 .expect("permission parses");
1877
1878 assert_eq!(parsed, Permission::card_write());
1879 assert_eq!(parsed.to_string(), "cards:write");
1880 }
1881
1882 #[test]
1891 fn retired_permission_is_unknown() {
1892 for action in ["run", "invoke"] {
1893 assert!(
1894 format!("evals:{action}").parse::<Permission>().is_err(),
1895 "evals:{action}"
1896 );
1897 assert!(
1898 serde_json::from_value::<Permission>(
1899 json!({"resource": "evals", "action": action, "scope": "all"})
1900 )
1901 .is_err(),
1902 "structured evals:{action}"
1903 );
1904 assert!(
1905 serde_json::from_value::<Permission>(json!({
1906 "resource": {"any_of": ["operators", "evals"]},
1907 "action": action,
1908 "scope": "all"
1909 }))
1910 .is_err(),
1911 "any_of evals:{action}"
1912 );
1913 }
1914 let verifier = "verifier:run"
1915 .parse::<Permission>()
1916 .expect("verifier:run parses");
1917 assert_eq!(verifier, Permission::verifier_run_any());
1918 assert!(verifier.covers(&Permission::verifier_run(
1919 wyrd_spec::ids::CardUid::new("01927b4c-0000-7000-8000-000000000001").expect("uid")
1920 )));
1921 assert_eq!(
1922 "operators:invoke"
1923 .parse::<Permission>()
1924 .expect("unrelated grant parses"),
1925 Permission::operator_invoke()
1926 );
1927 }
1928}