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::ids::CardUid;
12
13#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(try_from = "PermissionWire")]
21pub struct Permission {
22 pub resource: Resource,
24 pub action: Action,
26 pub scope: PermissionScope,
28}
29
30#[derive(Deserialize)]
38#[serde(deny_unknown_fields)]
39struct PermissionWire {
40 resource: Resource,
42 action: Action,
44 scope: PermissionScope,
46}
47
48impl TryFrom<PermissionWire> for Permission {
49 type Error = PermissionScopeError;
50
51 fn try_from(wire: PermissionWire) -> Result<Self, Self::Error> {
60 let permission = Self {
61 resource: wire.resource,
62 action: wire.action,
63 scope: wire.scope,
64 };
65 permission.validate()?;
66 Ok(permission)
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum Resource {
74 Cards,
76 Services,
78 Operators,
80 Evals,
82 Verifier,
84 Drift,
86 Artifacts,
88 Audit,
90 Policy,
92 Triggers,
94 ServiceAccounts,
96 Users,
98 IdentityConnections,
107 Tenants,
114 PlatformIdentity,
122 PlatformCredentials,
131 BifrostTable,
133 BifrostRecord,
135 BifrostQuery,
137 BifrostPeer,
143 Gateway,
146 GatewayPayload,
148 AnyOf(Vec<Resource>),
150 Wildcard,
152}
153
154#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum Action {
158 Read,
160 Write,
162 Delete,
164 Invoke,
166 Install,
168 Lock,
170 Run,
172 Suspend,
174 Recover,
180 AnyOf(Vec<Action>),
182 Wildcard,
184}
185
186impl Resource {
187 #[must_use]
189 pub fn covers(&self, other: &Resource) -> bool {
190 match (self, other) {
191 (Self::Wildcard, _) => true,
192 (Self::AnyOf(resources), other) => {
193 resources.iter().any(|resource| resource.covers(other))
194 }
195 (self_resource, other_resource) => self_resource == other_resource,
196 }
197 }
198
199 fn members(&self) -> Vec<&Self> {
202 match self {
203 Self::AnyOf(resources) => resources.iter().flat_map(Self::members).collect(),
204 resource => vec![resource],
205 }
206 }
207
208 #[must_use]
214 pub const fn accepts_bifrost_scope(&self) -> bool {
215 matches!(self, Self::BifrostQuery | Self::BifrostRecord)
216 }
217
218 #[must_use]
223 pub fn as_str(&self) -> Option<&'static str> {
224 Some(match self {
225 Self::Cards => "cards",
226 Self::Services => "services",
227 Self::Operators => "operators",
228 Self::Evals => "evals",
229 Self::Verifier => "verifier",
230 Self::Drift => "drift",
231 Self::Artifacts => "artifacts",
232 Self::Audit => "audit",
233 Self::Policy => "policy",
234 Self::Triggers => "triggers",
235 Self::ServiceAccounts => "service_accounts",
236 Self::Users => "users",
237 Self::IdentityConnections => "identity_connections",
238 Self::Tenants => "tenants",
239 Self::PlatformIdentity => "platform_identity",
240 Self::PlatformCredentials => "platform_credentials",
241 Self::BifrostTable => "bifrost_table",
242 Self::BifrostRecord => "bifrost_record",
243 Self::BifrostQuery => "bifrost_query",
244 Self::BifrostPeer => "bifrost_peer",
245 Self::Gateway => "gateway",
246 Self::GatewayPayload => "gateway_payload",
247 Self::Wildcard => "wildcard",
248 Self::AnyOf(_) => return None,
249 })
250 }
251}
252
253impl Action {
254 #[must_use]
256 pub fn covers(&self, other: &Action) -> bool {
257 match (self, other) {
258 (Self::Wildcard, _) => true,
259 (Self::AnyOf(actions), other) => actions.iter().any(|action| action.covers(other)),
260 (self_action, other_action) => self_action == other_action,
261 }
262 }
263
264 fn members(&self) -> Vec<&Self> {
267 match self {
268 Self::AnyOf(actions) => actions.iter().flat_map(Self::members).collect(),
269 action => vec![action],
270 }
271 }
272
273 #[must_use]
278 pub fn as_str(&self) -> Option<&'static str> {
279 Some(match self {
280 Self::Read => "read",
281 Self::Write => "write",
282 Self::Delete => "delete",
283 Self::Invoke => "invoke",
284 Self::Install => "install",
285 Self::Lock => "lock",
286 Self::Run => "run",
287 Self::Suspend => "suspend",
288 Self::Recover => "recover",
289 Self::Wildcard => "wildcard",
290 Self::AnyOf(_) => return None,
291 })
292 }
293}
294
295impl Permission {
296 #[must_use]
298 pub fn covers(&self, required: &Permission) -> bool {
299 self.resource.covers(&required.resource)
300 && self.action.covers(&required.action)
301 && self.scope.covers(&required.scope)
302 }
303
304 #[must_use]
311 pub fn subsumes(&self, other: &Permission) -> bool {
312 self.covers(other) && !(self.scope == PermissionScope::All && other.scope.is_gateway())
313 }
314
315 pub fn validate(&self) -> Result<(), PermissionScopeError> {
334 self.scope.validate()?;
335 let (resource_accepts, required_action) = match &self.scope {
336 PermissionScope::All => return Ok(()),
337 PermissionScope::Bifrost(_) => (
338 self.resource.accepts_bifrost_scope(),
339 if self.resource == Resource::BifrostRecord {
340 Action::Write
341 } else {
342 Action::Read
343 },
344 ),
345 PermissionScope::Gateway(_) => (self.resource == Resource::Gateway, Action::Invoke),
346 PermissionScope::Verifier(_) => (self.resource == Resource::Verifier, Action::Run),
347 };
348 if !resource_accepts {
349 return Err(PermissionScopeError::InvalidIdentifier {
350 field: "resource",
351 value: self.resource.as_str().unwrap_or("any_of").to_owned(),
352 });
353 }
354 if self.action != required_action {
355 return Err(PermissionScopeError::InvalidIdentifier {
356 field: "action",
357 value: self.action.as_str().unwrap_or("any_of").to_owned(),
358 });
359 }
360 Ok(())
361 }
362
363 #[must_use]
365 pub const fn card_read() -> Self {
366 Self {
367 resource: Resource::Cards,
368 action: Action::Read,
369 scope: PermissionScope::All,
370 }
371 }
372
373 #[must_use]
375 pub const fn card_write() -> Self {
376 Self {
377 resource: Resource::Cards,
378 action: Action::Write,
379 scope: PermissionScope::All,
380 }
381 }
382
383 #[must_use]
385 pub const fn card_delete() -> Self {
386 Self {
387 resource: Resource::Cards,
388 action: Action::Delete,
389 scope: PermissionScope::All,
390 }
391 }
392
393 #[must_use]
395 pub const fn artifact_read() -> Self {
396 Self {
397 resource: Resource::Artifacts,
398 action: Action::Read,
399 scope: PermissionScope::All,
400 }
401 }
402
403 #[must_use]
405 pub const fn artifact_write() -> Self {
406 Self {
407 resource: Resource::Artifacts,
408 action: Action::Write,
409 scope: PermissionScope::All,
410 }
411 }
412
413 #[must_use]
415 pub const fn service_install() -> Self {
416 Self {
417 resource: Resource::Services,
418 action: Action::Install,
419 scope: PermissionScope::All,
420 }
421 }
422
423 #[must_use]
425 pub const fn service_accounts_write() -> Self {
426 Self {
427 resource: Resource::ServiceAccounts,
428 action: Action::Write,
429 scope: PermissionScope::All,
430 }
431 }
432
433 #[must_use]
435 pub const fn identity_connections_write() -> Self {
436 Self {
437 resource: Resource::IdentityConnections,
438 action: Action::Write,
439 scope: PermissionScope::All,
440 }
441 }
442
443 #[must_use]
445 pub const fn operators_read() -> Self {
446 Self {
447 resource: Resource::Operators,
448 action: Action::Read,
449 scope: PermissionScope::All,
450 }
451 }
452
453 #[must_use]
455 pub const fn operators_write() -> Self {
456 Self {
457 resource: Resource::Operators,
458 action: Action::Write,
459 scope: PermissionScope::All,
460 }
461 }
462
463 #[must_use]
465 pub const fn operator_invoke() -> Self {
466 Self {
467 resource: Resource::Operators,
468 action: Action::Invoke,
469 scope: PermissionScope::All,
470 }
471 }
472
473 #[must_use]
475 pub const fn eval_run() -> Self {
476 Self {
477 resource: Resource::Evals,
478 action: Action::Run,
479 scope: PermissionScope::All,
480 }
481 }
482
483 #[must_use]
488 pub const fn verifier_run(verifier: CardUid) -> Self {
489 Self {
490 resource: Resource::Verifier,
491 action: Action::Run,
492 scope: PermissionScope::Verifier(verifier),
493 }
494 }
495
496 #[must_use]
501 pub const fn verifier_run_any() -> Self {
502 Self {
503 resource: Resource::Verifier,
504 action: Action::Run,
505 scope: PermissionScope::All,
506 }
507 }
508
509 #[must_use]
511 pub const fn trigger_write() -> Self {
512 Self {
513 resource: Resource::Triggers,
514 action: Action::Write,
515 scope: PermissionScope::All,
516 }
517 }
518
519 #[must_use]
521 pub const fn audit_read() -> Self {
522 Self {
523 resource: Resource::Audit,
524 action: Action::Read,
525 scope: PermissionScope::All,
526 }
527 }
528
529 #[must_use]
531 pub const fn policy_lock() -> Self {
532 Self {
533 resource: Resource::Policy,
534 action: Action::Lock,
535 scope: PermissionScope::All,
536 }
537 }
538
539 #[must_use]
541 pub const fn tenant_create() -> Self {
542 Self {
543 resource: Resource::Tenants,
544 action: Action::Write,
545 scope: PermissionScope::All,
546 }
547 }
548
549 #[must_use]
551 pub const fn tenant_read() -> Self {
552 Self {
553 resource: Resource::Tenants,
554 action: Action::Read,
555 scope: PermissionScope::All,
556 }
557 }
558
559 #[must_use]
561 pub const fn tenant_suspend() -> Self {
562 Self {
563 resource: Resource::Tenants,
564 action: Action::Suspend,
565 scope: PermissionScope::All,
566 }
567 }
568
569 #[must_use]
575 pub const fn tenant_recover_admin() -> Self {
576 Self {
577 resource: Resource::Tenants,
578 action: Action::Recover,
579 scope: PermissionScope::All,
580 }
581 }
582
583 #[must_use]
588 pub const fn platform_identity_read() -> Self {
589 Self {
590 resource: Resource::PlatformIdentity,
591 action: Action::Read,
592 scope: PermissionScope::All,
593 }
594 }
595
596 #[must_use]
603 pub const fn platform_identity_write() -> Self {
604 Self {
605 resource: Resource::PlatformIdentity,
606 action: Action::Write,
607 scope: PermissionScope::All,
608 }
609 }
610
611 #[must_use]
616 pub const fn platform_credential_read() -> Self {
617 Self {
618 resource: Resource::PlatformCredentials,
619 action: Action::Read,
620 scope: PermissionScope::All,
621 }
622 }
623
624 #[must_use]
631 pub const fn platform_credential_write() -> Self {
632 Self {
633 resource: Resource::PlatformCredentials,
634 action: Action::Write,
635 scope: PermissionScope::All,
636 }
637 }
638
639 #[must_use]
641 pub const fn users_manage() -> Self {
642 Self {
643 resource: Resource::Users,
644 action: Action::Write,
645 scope: PermissionScope::All,
646 }
647 }
648
649 #[must_use]
651 pub const fn bifrost_record_write() -> Self {
652 Self {
653 resource: Resource::BifrostRecord,
654 action: Action::Write,
655 scope: PermissionScope::All,
656 }
657 }
658
659 #[must_use]
661 pub const fn bifrost_peer_invoke() -> Self {
662 Self {
663 resource: Resource::BifrostPeer,
664 action: Action::Invoke,
665 scope: PermissionScope::All,
666 }
667 }
668
669 #[must_use]
671 pub const fn bifrost_table_read() -> Self {
672 Self {
673 resource: Resource::BifrostTable,
674 action: Action::Read,
675 scope: PermissionScope::All,
676 }
677 }
678
679 #[must_use]
681 pub const fn bifrost_table_write() -> Self {
682 Self {
683 resource: Resource::BifrostTable,
684 action: Action::Write,
685 scope: PermissionScope::All,
686 }
687 }
688
689 #[must_use]
691 pub const fn bifrost_table_install() -> Self {
692 Self {
693 resource: Resource::BifrostTable,
694 action: Action::Install,
695 scope: PermissionScope::All,
696 }
697 }
698
699 #[must_use]
701 pub const fn bifrost_query_read() -> Self {
702 Self {
703 resource: Resource::BifrostQuery,
704 action: Action::Read,
705 scope: PermissionScope::All,
706 }
707 }
708
709 #[must_use]
711 pub const fn gateway_read() -> Self {
712 Self {
713 resource: Resource::Gateway,
714 action: Action::Read,
715 scope: PermissionScope::All,
716 }
717 }
718
719 #[must_use]
721 pub const fn gateway_write() -> Self {
722 Self {
723 resource: Resource::Gateway,
724 action: Action::Write,
725 scope: PermissionScope::All,
726 }
727 }
728
729 #[must_use]
731 pub const fn gateway_delete() -> Self {
732 Self {
733 resource: Resource::Gateway,
734 action: Action::Delete,
735 scope: PermissionScope::All,
736 }
737 }
738
739 #[must_use]
741 pub const fn gateway_invoke(access: GatewayAccess) -> Self {
742 Self {
743 resource: Resource::Gateway,
744 action: Action::Invoke,
745 scope: PermissionScope::Gateway(access),
746 }
747 }
748
749 #[must_use]
755 pub const fn gateway_invoke_any() -> Self {
756 Self {
757 resource: Resource::Gateway,
758 action: Action::Invoke,
759 scope: PermissionScope::All,
760 }
761 }
762
763 #[must_use]
765 pub const fn gateway_payload_read() -> Self {
766 Self {
767 resource: Resource::GatewayPayload,
768 action: Action::Read,
769 scope: PermissionScope::All,
770 }
771 }
772
773 #[must_use]
775 pub const fn wildcard() -> Self {
776 Self {
777 resource: Resource::Wildcard,
778 action: Action::Wildcard,
779 scope: PermissionScope::All,
780 }
781 }
782}
783
784impl fmt::Display for Permission {
791 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792 let resource = self.resource.as_str().ok_or(fmt::Error)?;
793 let action = self.action.as_str().ok_or(fmt::Error)?;
794 write!(f, "{resource}:{action}")
795 }
796}
797
798#[derive(Debug, thiserror::Error)]
800#[error("permission must be formatted as resource:action")]
801pub struct PermissionParseError;
802
803impl FromStr for Permission {
810 type Err = PermissionParseError;
811
812 fn from_str(value: &str) -> Result<Self, Self::Err> {
813 let Some((resource, action)) = value.split_once(':') else {
814 return Err(PermissionParseError);
815 };
816 Ok(Self {
817 resource: parse_resource(resource)?,
818 action: parse_action(action)?,
819 scope: PermissionScope::All,
820 })
821 }
822}
823
824fn parse_resource(value: &str) -> Result<Resource, PermissionParseError> {
825 Ok(match value {
826 "cards" => Resource::Cards,
827 "services" => Resource::Services,
828 "operators" => Resource::Operators,
829 "evals" => Resource::Evals,
830 "verifier" => Resource::Verifier,
831 "drift" => Resource::Drift,
832 "artifacts" => Resource::Artifacts,
833 "audit" => Resource::Audit,
834 "policy" => Resource::Policy,
835 "triggers" => Resource::Triggers,
836 "service_accounts" => Resource::ServiceAccounts,
837 "users" => Resource::Users,
838 "identity_connections" => Resource::IdentityConnections,
839 "bifrost_table" => Resource::BifrostTable,
840 "bifrost_record" => Resource::BifrostRecord,
841 "bifrost_peer" => Resource::BifrostPeer,
842 "bifrost_query" => Resource::BifrostQuery,
843 "gateway" => Resource::Gateway,
844 "gateway_payload" => Resource::GatewayPayload,
845 "wildcard" => Resource::Wildcard,
846 _ => return Err(PermissionParseError),
847 })
848}
849
850fn parse_action(value: &str) -> Result<Action, PermissionParseError> {
851 Ok(match value {
852 "read" => Action::Read,
853 "write" => Action::Write,
854 "delete" => Action::Delete,
855 "invoke" => Action::Invoke,
856 "install" => Action::Install,
857 "lock" => Action::Lock,
858 "run" => Action::Run,
859 "wildcard" => Action::Wildcard,
860 _ => return Err(PermissionParseError),
861 })
862}
863
864#[derive(Debug, thiserror::Error)]
869#[error("shared authority for {permission} depends on deployment access and cannot be represented")]
870pub struct UnrepresentableIntersection {
871 pub permission: Permission,
873}
874
875#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
877#[serde(transparent)]
878pub struct PermissionSet(Vec<Permission>);
879
880impl PermissionSet {
881 #[must_use]
883 pub fn new() -> Self {
884 Self(Vec::new())
885 }
886
887 pub fn insert(&mut self, permission: Permission) {
892 if self.0.iter().any(|existing| existing.subsumes(&permission)) {
893 return;
894 }
895 self.0.retain(|existing| !permission.subsumes(existing));
896 self.0.push(permission);
897 }
898
899 #[must_use]
901 pub fn contains(&self, required: &Permission) -> bool {
902 self.0.iter().any(|permission| permission.covers(required))
903 }
904
905 #[must_use]
911 pub fn contains_object_scoped(&self, required: &Permission) -> bool {
912 self.0.iter().any(|permission| {
913 permission.scope != PermissionScope::All && permission.covers(required)
914 })
915 }
916
917 #[must_use]
925 pub fn covers_operation(&self, resource: &Resource, action: &Action) -> bool {
926 self.0.iter().any(|permission| {
927 permission.resource.covers(resource) && permission.action.covers(action)
928 })
929 }
930
931 pub fn intersection(&self, other: &Self) -> Result<Self, UnrepresentableIntersection> {
955 let mut meet = Self::new();
956 let mut open_only = Vec::new();
957 for left in &self.0 {
958 for right in &other.0 {
959 let Some(scope) = narrower(&left.scope, &right.scope, PermissionScope::covers)
960 else {
961 continue;
962 };
963 let conditional = scope.is_gateway()
964 && (left.scope == PermissionScope::All || right.scope == PermissionScope::All);
965 for resource in meets(
966 &left.resource.members(),
967 &right.resource.members(),
968 Resource::covers,
969 ) {
970 for action in meets(
971 &left.action.members(),
972 &right.action.members(),
973 Action::covers,
974 ) {
975 let permission = Permission {
976 resource: resource.clone(),
977 action: action.clone(),
978 scope: scope.clone(),
979 };
980 if permission.validate().is_err() {
981 continue;
982 }
983 if conditional {
984 open_only.push(permission);
985 } else {
986 meet.insert(permission);
987 }
988 }
989 }
990 }
991 }
992 match open_only
993 .into_iter()
994 .find(|permission| !meet.contains(permission))
995 {
996 Some(permission) => Err(UnrepresentableIntersection { permission }),
997 None => Ok(meet),
998 }
999 }
1000
1001 pub fn iter(&self) -> impl Iterator<Item = &Permission> {
1003 self.0.iter()
1004 }
1005
1006 #[must_use]
1008 pub fn len(&self) -> usize {
1009 self.0.len()
1010 }
1011
1012 #[must_use]
1014 pub fn is_empty(&self) -> bool {
1015 self.0.is_empty()
1016 }
1017}
1018
1019fn narrower<'a, T>(left: &'a T, right: &'a T, covers: fn(&T, &T) -> bool) -> Option<&'a T> {
1025 if covers(left, right) {
1026 Some(right)
1027 } else if covers(right, left) {
1028 Some(left)
1029 } else {
1030 None
1031 }
1032}
1033
1034fn meets<'a, T>(left: &[&'a T], right: &[&'a T], covers: fn(&T, &T) -> bool) -> Vec<&'a T> {
1036 left.iter()
1037 .flat_map(|l| right.iter().filter_map(move |r| narrower(*l, *r, covers)))
1038 .collect()
1039}
1040
1041impl FromIterator<Permission> for PermissionSet {
1042 fn from_iter<T: IntoIterator<Item = Permission>>(iter: T) -> Self {
1043 let mut set = Self::new();
1044 for permission in iter {
1045 set.insert(permission);
1046 }
1047 set
1048 }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use super::{Action, Permission, PermissionScope, PermissionSet, Resource};
1054 use serde_json::json;
1055 use wyrd_spec::auth::{BifrostPermissionScope, BifrostSchemaScope, BifrostTableScope};
1056
1057 #[test]
1058 fn serde_round_trip_every_variant() {
1059 let resources = [
1060 Resource::Cards,
1061 Resource::Services,
1062 Resource::Operators,
1063 Resource::Evals,
1064 Resource::Verifier,
1065 Resource::Drift,
1066 Resource::Artifacts,
1067 Resource::Audit,
1068 Resource::Policy,
1069 Resource::Triggers,
1070 Resource::ServiceAccounts,
1071 Resource::Users,
1072 Resource::IdentityConnections,
1073 Resource::AnyOf(vec![Resource::Operators, Resource::Evals]),
1074 Resource::Wildcard,
1075 ];
1076 for resource in resources {
1077 let value = serde_json::to_value(&resource).expect("resource serializes");
1078 let round_trip: Resource =
1079 serde_json::from_value(value).expect("resource deserializes");
1080 assert_eq!(round_trip, resource);
1081 }
1082
1083 let actions = [
1084 Action::Read,
1085 Action::Write,
1086 Action::Delete,
1087 Action::Invoke,
1088 Action::Install,
1089 Action::Lock,
1090 Action::Run,
1091 Action::AnyOf(vec![Action::Read, Action::Write]),
1092 Action::Wildcard,
1093 ];
1094 for action in actions {
1095 let value = serde_json::to_value(&action).expect("action serializes");
1096 let round_trip: Action = serde_json::from_value(value).expect("action deserializes");
1097 assert_eq!(round_trip, action);
1098 }
1099 }
1100
1101 #[test]
1102 fn bifrost_permissions_round_trip_through_wire_strings() {
1103 for (permission, wire) in [
1104 (Permission::bifrost_record_write(), "bifrost_record:write"),
1105 (Permission::bifrost_table_read(), "bifrost_table:read"),
1106 (Permission::bifrost_table_write(), "bifrost_table:write"),
1107 (Permission::bifrost_query_read(), "bifrost_query:read"),
1108 (Permission::bifrost_peer_invoke(), "bifrost_peer:invoke"),
1109 ] {
1110 assert_eq!(permission.to_string(), wire);
1111 assert_eq!(wire.parse::<Permission>().expect("wire parses"), permission);
1112 let json = serde_json::to_value(&permission).expect("serializes");
1113 assert_eq!(
1114 serde_json::from_value::<Permission>(json).expect("deserializes"),
1115 permission
1116 );
1117 }
1118 }
1119
1120 #[test]
1121 fn bifrost_record_write_is_distinct_from_table_write() {
1122 assert!(!Permission::bifrost_table_write().covers(&Permission::bifrost_record_write()));
1123 assert!(!Permission::bifrost_record_write().covers(&Permission::bifrost_table_write()));
1124 assert!(Permission::wildcard().covers(&Permission::bifrost_record_write()));
1125 }
1126
1127 #[test]
1128 fn covers_wildcard_resource() {
1129 let permission = Permission {
1130 resource: Resource::Wildcard,
1131 action: Action::Read,
1132 scope: PermissionScope::All,
1133 };
1134
1135 assert!(permission.covers(&Permission::card_read()));
1136 assert!(!permission.covers(&Permission::card_write()));
1137 }
1138
1139 #[test]
1140 fn covers_wildcard_action() {
1141 let permission = Permission {
1142 resource: Resource::Cards,
1143 action: Action::Wildcard,
1144 scope: PermissionScope::All,
1145 };
1146
1147 assert!(permission.covers(&Permission::card_read()));
1148 assert!(permission.covers(&Permission::card_write()));
1149 assert!(!permission.covers(&Permission::artifact_write()));
1150 }
1151
1152 #[test]
1158 fn covers_double_wildcard() {
1159 let permission = Permission::wildcard();
1160
1161 assert!(permission.covers(&Permission::card_read()));
1162 assert!(permission.covers(&Permission::card_write()));
1163 assert!(permission.covers(&Permission::bifrost_record_write()));
1164 }
1165
1166 #[test]
1167 fn covers_anyof_resource() {
1168 let permission = Permission {
1169 resource: Resource::AnyOf(vec![Resource::Operators, Resource::Evals]),
1170 action: Action::Invoke,
1171 scope: PermissionScope::All,
1172 };
1173
1174 assert!(permission.covers(&Permission::operator_invoke()));
1175 assert!(permission.covers(&Permission {
1176 resource: Resource::Evals,
1177 action: Action::Invoke,
1178 scope: PermissionScope::All,
1179 }));
1180 assert!(!permission.covers(&Permission {
1181 resource: Resource::Cards,
1182 action: Action::Invoke,
1183 scope: PermissionScope::All,
1184 }));
1185 }
1186
1187 #[test]
1188 fn permission_set_subsumption_drops_redundant() {
1189 let mut set = PermissionSet::from_iter([Permission::card_read()]);
1190 set.insert(Permission::wildcard());
1191
1192 assert_eq!(set.len(), 1);
1193 assert_eq!(set.iter().next(), Some(&Permission::wildcard()));
1194 }
1195
1196 #[test]
1197 fn permission_set_subsumption_skips_covered() {
1198 let mut set = PermissionSet::from_iter([Permission::wildcard()]);
1199 set.insert(Permission::card_read());
1200
1201 assert_eq!(set.len(), 1);
1202 assert_eq!(set.iter().next(), Some(&Permission::wildcard()));
1203 }
1204
1205 #[test]
1211 fn permission_set_contains_resolves_through_wildcard() {
1212 let set = PermissionSet::from_iter([Permission::wildcard()]);
1213
1214 assert!(set.contains(&Permission::card_read()));
1215 assert!(set.contains(&Permission::bifrost_record_write()));
1216 }
1217
1218 #[test]
1219 fn permission_set_jsonb_round_trip() {
1220 let permissions = vec![
1221 Permission::card_write(),
1222 Permission::card_read(),
1223 Permission {
1224 resource: Resource::AnyOf(vec![Resource::Operators, Resource::Evals]),
1225 action: Action::Invoke,
1226 scope: PermissionScope::All,
1227 },
1228 Permission::wildcard(),
1229 ];
1230 let value = serde_json::to_value(&permissions).expect("permissions serialize");
1231
1232 assert_eq!(
1233 value,
1234 json!([
1235 {"resource": "cards", "action": "write", "scope": "all"},
1236 {"resource": "cards", "action": "read", "scope": "all"},
1237 {"resource": {"any_of": ["operators", "evals"]}, "action": "invoke", "scope": "all"},
1238 {"resource": "wildcard", "action": "wildcard", "scope": "all"}
1239 ])
1240 );
1241
1242 let round_trip: Vec<Permission> =
1243 serde_json::from_value(value).expect("permissions deserialize");
1244 assert_eq!(round_trip, permissions);
1245 }
1246
1247 #[test]
1255 fn gateway_permissions_are_typed_and_scope_checked() {
1256 use super::GatewayAccess;
1257 use wyrd_spec::ids::{ModelId, ProviderDeploymentName};
1258 let openai = ProviderDeploymentName::new("openai").expect("valid deployment");
1259 let provider = GatewayAccess::Deployment {
1260 deployment: openai.clone(),
1261 };
1262 let model = |name: &str| GatewayAccess::Model {
1263 deployment: openai.clone(),
1264 model: ModelId::new(name).expect("valid model"),
1265 };
1266 let required = Permission::gateway_invoke(model("gpt-4o"));
1267 assert!(PermissionSet::from_iter([Permission::gateway_invoke_any()]).contains(&required));
1268 assert!(
1269 !PermissionSet::from_iter([Permission::gateway_invoke_any()])
1270 .contains_object_scoped(&required),
1271 "the unscoped grant is not a grant of the deployment"
1272 );
1273 assert!(
1274 PermissionSet::from_iter([Permission::gateway_invoke(provider.clone())])
1275 .contains_object_scoped(&required)
1276 );
1277
1278 assert_eq!(
1279 Permission::gateway_payload_read().to_string(),
1280 "gateway_payload:read"
1281 );
1282 assert_eq!(
1283 "gateway:write".parse::<Permission>().expect("parses"),
1284 Permission::gateway_write()
1285 );
1286 assert!(
1287 Permission::gateway_invoke(provider.clone())
1288 .covers(&Permission::gateway_invoke(model("gpt-4o")))
1289 );
1290 assert!(
1291 !Permission::gateway_invoke(model("gpt-4o"))
1292 .covers(&Permission::gateway_invoke(model("gpt-4.1")))
1293 );
1294 assert!(
1295 !Permission::gateway_invoke(provider.clone()).covers(&Permission::gateway_read()),
1296 "invoke never grants admin metadata reads"
1297 );
1298 assert_eq!(
1299 serde_json::to_value(Permission::gateway_invoke(model("gpt-4o"))).expect("serializes"),
1300 json!({
1301 "resource": "gateway",
1302 "action": "invoke",
1303 "scope": {"gateway": {"model": {"deployment": "openai", "model": "gpt-4o"}}}
1304 })
1305 );
1306 for (resource, action) in [("gateway", "read"), ("cards", "invoke")] {
1307 assert!(
1308 serde_json::from_value::<Permission>(json!({
1309 "resource": resource,
1310 "action": action,
1311 "scope": {"gateway": {"deployment": {"deployment": "openai"}}}
1312 }))
1313 .is_err(),
1314 "{resource}:{action}"
1315 );
1316 }
1317 }
1318
1319 #[test]
1323 fn verifier_run_is_uid_scoped_and_distinct_from_eval_run() {
1324 use wyrd_spec::ids::CardUid;
1325 let uid = |value: &str| CardUid::new(value).expect("valid uuid7");
1326 let bound = uid("01927b4c-0000-7000-8000-000000000001");
1327 let other = uid("01927b4c-0000-7000-8000-000000000002");
1328
1329 assert_eq!(
1330 "verifier:run".parse::<Permission>().expect("parses"),
1331 Permission::verifier_run_any()
1332 );
1333 assert!(Permission::verifier_run_any().covers(&Permission::verifier_run(bound.clone())));
1334 assert!(
1335 Permission::verifier_run(bound.clone())
1336 .covers(&Permission::verifier_run(bound.clone()))
1337 );
1338 assert!(
1339 !Permission::verifier_run(other).covers(&Permission::verifier_run(bound.clone())),
1340 "an unrelated UID grant does not cover"
1341 );
1342 assert!(
1343 !Permission::eval_run().covers(&Permission::verifier_run(bound.clone())),
1344 "evals:run alone does not cover"
1345 );
1346 assert_eq!(
1347 serde_json::to_value(Permission::verifier_run(bound.clone())).expect("serializes"),
1348 json!({"resource": "verifier", "action": "run", "scope": {"verifier": bound.as_str()}})
1349 );
1350 for (resource, action) in [("verifier", "read"), ("evals", "run")] {
1351 assert!(
1352 serde_json::from_value::<Permission>(json!({
1353 "resource": resource,
1354 "action": action,
1355 "scope": {"verifier": bound.as_str()}
1356 }))
1357 .is_err(),
1358 "{resource}:{action}"
1359 );
1360 }
1361 }
1362
1363 #[test]
1376 fn gateway_grants_survive_merge_and_intersect_exactly() {
1377 use super::GatewayAccess;
1378 use wyrd_spec::ids::{ModelId, ProviderDeploymentName};
1379 let restricted = ProviderDeploymentName::new("restricted").expect("valid deployment");
1380 let deployment = Permission::gateway_invoke(GatewayAccess::Deployment {
1381 deployment: restricted.clone(),
1382 });
1383 let model = Permission::gateway_invoke(GatewayAccess::Model {
1384 deployment: restricted,
1385 model: ModelId::new("gpt-4o").expect("valid model"),
1386 });
1387 let any_resource_invoke = Permission {
1388 resource: Resource::AnyOf(vec![Resource::Gateway, Resource::Cards]),
1389 action: Action::Wildcard,
1390 scope: PermissionScope::All,
1391 };
1392 for broad in [
1393 Permission::gateway_invoke_any(),
1394 Permission::wildcard(),
1395 any_resource_invoke,
1396 ] {
1397 for set in [
1398 PermissionSet::from_iter([broad.clone(), deployment.clone()]),
1399 PermissionSet::from_iter([deployment.clone(), broad.clone()]),
1400 ] {
1401 assert!(set.contains_object_scoped(&model), "{broad} kept explicit");
1402 assert!(set.contains(&Permission::gateway_invoke_any()));
1403 }
1404 }
1405
1406 let all = PermissionSet::from_iter([Permission::gateway_invoke_any()]);
1407 let explicit = PermissionSet::from_iter([deployment.clone()]);
1408 let narrow = PermissionSet::from_iter([model.clone()]);
1409 assert_eq!(all.intersection(&all).expect("representable"), all);
1410 assert_eq!(
1411 explicit.intersection(&narrow).expect("representable"),
1412 narrow
1413 );
1414 assert!(all.intersection(&explicit).is_err());
1415 assert!(explicit.intersection(&all).is_err());
1416 let wildcard = PermissionSet::from_iter([Permission::wildcard()]);
1417 assert!(wildcard.intersection(&narrow).is_err());
1418
1419 let mixed = PermissionSet::from_iter([Permission::gateway_invoke_any(), deployment]);
1420 assert_eq!(mixed.intersection(&narrow).expect("representable"), narrow);
1421 assert_eq!(narrow.intersection(&mixed).expect("representable"), narrow);
1422 assert_eq!(
1423 mixed.intersection(&all).expect("representable"),
1424 all,
1425 "both hold all; only one holds the explicit grant"
1426 );
1427 assert!(
1428 PermissionSet::from_iter([Permission::card_read()])
1429 .intersection(&explicit)
1430 .expect("no overlap is the representable empty result")
1431 .is_empty()
1432 );
1433 }
1434
1435 fn logs_schema_grant() -> Permission {
1437 Permission {
1438 resource: Resource::BifrostQuery,
1439 action: Action::Read,
1440 scope: PermissionScope::Bifrost(BifrostPermissionScope::Schema(BifrostSchemaScope {
1441 catalog: "vala".to_owned(),
1442 schema: "logs".to_owned(),
1443 })),
1444 }
1445 }
1446
1447 fn table_requirement(schema: &str, uid: uuid::Uuid) -> Permission {
1449 Permission {
1450 resource: Resource::BifrostQuery,
1451 action: Action::Read,
1452 scope: PermissionScope::Bifrost(BifrostPermissionScope::Table(BifrostTableScope {
1453 catalog: "vala".to_owned(),
1454 schema: schema.to_owned(),
1455 table_uid: uid,
1456 })),
1457 }
1458 }
1459
1460 #[test]
1467 fn intersection_keeps_only_the_narrower_shared_authority() {
1468 let uid = uuid::Uuid::from_u128(0x77);
1469 let set = |permissions: Vec<Permission>| permissions.into_iter().collect::<PermissionSet>();
1470
1471 let wildcard = set(vec![Permission::wildcard()]);
1472 let target = set(vec![Permission::card_read(), logs_schema_grant()]);
1473 assert_eq!(
1474 wildcard.intersection(&target).expect("representable"),
1475 target
1476 );
1477 assert_eq!(
1478 target.intersection(&wildcard).expect("representable"),
1479 target
1480 );
1481
1482 let schema = set(vec![logs_schema_grant()]);
1483 let table = set(vec![table_requirement("logs", uid)]);
1484 assert_eq!(schema.intersection(&table).expect("representable"), table);
1485 assert_eq!(table.intersection(&schema).expect("representable"), table);
1486
1487 let other_table = set(vec![table_requirement("logs", uuid::Uuid::from_u128(0x78))]);
1488 assert!(
1489 table
1490 .intersection(&other_table)
1491 .expect("representable")
1492 .is_empty()
1493 );
1494 assert!(
1495 set(vec![table_requirement("traces", uid)])
1496 .intersection(&schema)
1497 .expect("representable")
1498 .is_empty()
1499 );
1500
1501 let all_reads = set(vec![Permission {
1502 resource: Resource::Wildcard,
1503 action: Action::Read,
1504 scope: PermissionScope::All,
1505 }]);
1506 let cards_any = set(vec![Permission {
1507 resource: Resource::AnyOf(vec![Resource::Cards, Resource::Audit]),
1508 action: Action::Wildcard,
1509 scope: PermissionScope::All,
1510 }]);
1511 assert_eq!(
1512 all_reads.intersection(&cards_any).expect("representable"),
1513 set(vec![Permission::card_read(), Permission::audit_read()])
1514 );
1515
1516 assert!(
1517 set(vec![Permission::card_write()])
1518 .intersection(&set(vec![
1519 Permission::card_read(),
1520 Permission::bifrost_record_write()
1521 ]))
1522 .expect("representable")
1523 .is_empty()
1524 );
1525 }
1526
1527 #[test]
1530 fn scoped_permission_json_matches_the_approved_projection() {
1531 let uid = uuid::Uuid::from_u128(0x99);
1532 let cases = [
1533 (
1534 Permission::bifrost_query_read(),
1535 json!({"resource": "bifrost_query", "action": "read", "scope": "all"}),
1536 ),
1537 (
1538 logs_schema_grant(),
1539 json!({
1540 "resource": "bifrost_query",
1541 "action": "read",
1542 "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": "logs"}}}
1543 }),
1544 ),
1545 (
1546 table_requirement("traces", uid),
1547 json!({
1548 "resource": "bifrost_query",
1549 "action": "read",
1550 "scope": {"bifrost": {"table": {
1551 "catalog": "vala",
1552 "schema": "traces",
1553 "table_uid": uid.to_string(),
1554 }}}
1555 }),
1556 ),
1557 ];
1558
1559 for (permission, wire) in cases {
1560 assert_eq!(
1561 serde_json::to_value(&permission).expect("permission serializes"),
1562 wire
1563 );
1564 assert_eq!(
1565 serde_json::from_value::<Permission>(wire).expect("permission deserializes"),
1566 permission
1567 );
1568 }
1569 }
1570
1571 #[test]
1575 fn bifrost_scope_applies_to_record_write_and_query_read_only() {
1576 let calls = uuid::Uuid::from_u128(0xca11);
1577 let spans = uuid::Uuid::from_u128(0x5a45);
1578 let table_write = |uid| Permission {
1579 resource: Resource::BifrostRecord,
1580 action: Action::Write,
1581 scope: PermissionScope::Bifrost(BifrostPermissionScope::Table(BifrostTableScope {
1582 catalog: "vala".to_owned(),
1583 schema: "gateway".to_owned(),
1584 table_uid: uid,
1585 })),
1586 };
1587 table_write(calls)
1588 .validate()
1589 .expect("scoped record write is valid");
1590 assert!(table_write(calls).covers(&table_write(calls)));
1591 assert!(!table_write(calls).covers(&table_write(spans)));
1592 assert!(
1593 !table_write(calls).covers(&Permission::bifrost_record_write()),
1594 "a table-scoped write never covers the unscoped requirement"
1595 );
1596 assert!(Permission::bifrost_record_write().covers(&table_write(spans)));
1597 let wire = serde_json::to_value(table_write(calls)).expect("serializes");
1598 assert_eq!(
1599 serde_json::from_value::<Permission>(wire).expect("round-trips"),
1600 table_write(calls)
1601 );
1602 for (resource, action) in [
1603 (Resource::BifrostRecord, Action::Read),
1604 (Resource::BifrostQuery, Action::Write),
1605 (Resource::BifrostTable, Action::Write),
1606 ] {
1607 let permission = Permission {
1608 resource,
1609 action,
1610 ..table_write(calls)
1611 };
1612 assert!(permission.validate().is_err(), "{permission}");
1613 }
1614 }
1615
1616 #[test]
1619 fn scope_is_required_and_has_no_compatibility_decoder() {
1620 let error = serde_json::from_value::<Permission>(json!({
1621 "resource": "bifrost_query",
1622 "action": "read"
1623 }))
1624 .expect_err("the unscoped two-field form is not accepted");
1625
1626 assert!(error.to_string().contains("scope"), "{error}");
1627 }
1628
1629 #[test]
1632 fn bifrost_scope_is_rejected_on_an_unrelated_resource() {
1633 for resource in ["cards", "wildcard"] {
1634 let error = serde_json::from_value::<Permission>(json!({
1635 "resource": resource,
1636 "action": "read",
1637 "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": "logs"}}}
1638 }))
1639 .expect_err("a Bifrost object scope needs a Bifrost object resource");
1640
1641 assert!(error.to_string().contains("resource"), "{error}");
1642 }
1643 }
1644
1645 #[test]
1651 fn bifrost_scope_is_rejected_on_a_non_read_action() {
1652 for action in [
1653 json!("write"),
1654 json!("wildcard"),
1655 json!({"any_of": ["read", "write"]}),
1656 ] {
1657 let error = serde_json::from_value::<Permission>(json!({
1658 "resource": "bifrost_query",
1659 "action": action,
1660 "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": "logs"}}}
1661 }))
1662 .expect_err("a Bifrost object scope only applies to an exact read");
1663
1664 assert!(error.to_string().contains("action"), "{error}");
1665 }
1666
1667 serde_json::from_value::<Permission>(json!({
1668 "resource": "bifrost_query",
1669 "action": "read",
1670 "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": "logs"}}}
1671 }))
1672 .expect("exact query read stays valid for Bifrost scope");
1673 }
1674
1675 #[test]
1678 fn malformed_scope_identity_is_rejected_at_decode() {
1679 assert!(
1680 serde_json::from_value::<Permission>(json!({
1681 "resource": "bifrost_query",
1682 "action": "read",
1683 "scope": {"bifrost": {"schema": {"catalog": "vala", "schema": ""}}}
1684 }))
1685 .is_err(),
1686 "an empty schema segment is not an object identity"
1687 );
1688 }
1689
1690 #[test]
1693 fn coverage_is_three_axis() {
1694 let logs = uuid::Uuid::from_u128(1);
1695 let traces = uuid::Uuid::from_u128(2);
1696
1697 assert!(logs_schema_grant().covers(&table_requirement("logs", logs)));
1699 assert!(logs_schema_grant().covers(&table_requirement("logs", traces)));
1700 assert!(!logs_schema_grant().covers(&table_requirement("traces", traces)));
1701
1702 let exact = table_requirement("traces", traces);
1704 assert!(exact.covers(&table_requirement("traces", traces)));
1705 assert!(!exact.covers(&table_requirement("traces", logs)));
1706
1707 assert!(Permission::bifrost_query_read().covers(&table_requirement("logs", logs)));
1709 assert!(!Permission::bifrost_table_read().covers(&table_requirement("logs", logs)));
1710
1711 assert!(Permission::wildcard().covers(&table_requirement("logs", logs)));
1713
1714 assert!(!logs_schema_grant().covers(&Permission::bifrost_query_read()));
1716 }
1717
1718 #[test]
1721 fn covers_operation_admits_a_scoped_grant_without_authorizing_an_object() {
1722 let set = PermissionSet::from_iter([logs_schema_grant()]);
1723
1724 assert!(set.covers_operation(&Resource::BifrostQuery, &Action::Read));
1725 assert!(!set.covers_operation(&Resource::BifrostTable, &Action::Read));
1726 assert!(!set.contains(&Permission::bifrost_query_read()));
1727 assert!(set.contains(&table_requirement("logs", uuid::Uuid::from_u128(7))));
1728 assert!(!set.contains(&table_requirement("traces", uuid::Uuid::from_u128(7))));
1729 }
1730
1731 #[test]
1734 fn permission_set_keeps_scopes_that_do_not_subsume_each_other() {
1735 let mut set = PermissionSet::from_iter([logs_schema_grant()]);
1736 set.insert(table_requirement("traces", uuid::Uuid::from_u128(3)));
1737
1738 assert_eq!(set.len(), 2);
1739
1740 set.insert(Permission::bifrost_query_read());
1741
1742 assert_eq!(set.len(), 1);
1743 assert_eq!(set.iter().next(), Some(&Permission::bifrost_query_read()));
1744 }
1745
1746 #[test]
1747 fn permission_wire_token_roundtrips() {
1748 let parsed = "cards:write"
1749 .parse::<Permission>()
1750 .expect("permission parses");
1751
1752 assert_eq!(parsed, Permission::card_write());
1753 assert_eq!(parsed.to_string(), "cards:write");
1754 }
1755}