1use crate::codes::*;
2use crate::error::InvalidError;
3use crate::limits::MAX_ROLE_NAME_BYTES;
4use crate::{checkpoint::CheckpointRequestId, destination::DestinationId};
5use serde::{Deserialize, Serialize};
6
7#[derive(
9 Clone,
10 Copy,
11 Debug,
12 Default,
13 PartialEq,
14 Eq,
15 Serialize,
16 Deserialize,
17 strum::Display,
18 strum::EnumString,
19 strum::VariantArray,
20)]
21#[strum(serialize_all = "snake_case")]
22#[serde(rename_all = "snake_case")]
23#[non_exhaustive]
24pub enum Effect {
25 #[default]
26 Allow,
27 Deny,
28}
29
30#[derive(
33 Clone,
34 Copy,
35 Debug,
36 PartialEq,
37 Eq,
38 Hash,
39 Serialize,
40 Deserialize,
41 strum::Display,
42 strum::EnumString,
43 strum::VariantArray,
44)]
45#[strum(serialize_all = "snake_case")]
46#[serde(rename_all = "snake_case")]
47#[non_exhaustive]
48pub enum Feature {
49 Kv,
50 Memory,
51 Projection,
52 Fork,
53 Graph,
54 Query,
55 Agent,
56 Workflow,
57 Destination,
58 Checkpoint,
59 Authz,
62 KvLease,
69 KvFence,
73 Filter,
77 #[serde(other)]
85 Unrecognized,
86}
87
88pub const SUPERVISOR_ASSERTION_VERSION: u32 = 1;
89pub const SUPERVISOR_ASSERTION_DOMAIN: &[u8] = b"agdx.supervisor-actor.v1";
90pub const MAX_SUPERVISOR_ASSERTION_TTL_MICROS: u64 = 5 * 60 * 1_000_000;
91pub const SUPERVISOR_ASSERTION_KEY_ID_BYTES: usize = 8;
92pub const SUPERVISOR_ASSERTION_SIGNATURE_BYTES: usize = 64;
93
94#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
96pub struct SupervisorActorClaims {
97 pub v: u32,
98 pub request_id: CheckpointRequestId,
99 pub deployment_id: u32,
100 pub cloud_user_id: u32,
101 pub action: SupervisorAssertionAction,
102 pub destination_id: DestinationId,
103 pub destination_generation: u64,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub expected_revision: Option<u64>,
106 pub issued_at_micros: u64,
107 pub expires_at_micros: u64,
108}
109
110#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
112pub struct SupervisorActorAssertion {
113 pub claims: SupervisorActorClaims,
114 #[serde(with = "crate::encoding::bin_bytes")]
115 pub key_id: Vec<u8>,
116 #[serde(with = "crate::encoding::bin_bytes")]
117 pub signature: Vec<u8>,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122#[non_exhaustive]
123pub enum SupervisorAssertionAction {
124 AcceptRetentionGap,
125 SupersedeGeneration,
126 RecordRepair,
127}
128
129impl SupervisorActorAssertion {
130 pub fn validate(&self) -> Result<(), InvalidError> {
131 let claims = &self.claims;
132 if claims.v != SUPERVISOR_ASSERTION_VERSION {
133 return Err(InvalidError::new(format!(
134 "supervisor assertion version must be {SUPERVISOR_ASSERTION_VERSION}"
135 )));
136 }
137 if claims.request_id.as_u128() == 0
138 || claims.deployment_id == 0
139 || claims.cloud_user_id == 0
140 || claims.destination_id.as_u128() == 0
141 || claims.destination_generation == 0
142 {
143 return Err(InvalidError::new(
144 "supervisor assertion identities must be nonzero",
145 ));
146 }
147 if claims.issued_at_micros == 0
148 || claims.expires_at_micros <= claims.issued_at_micros
149 || claims.expires_at_micros - claims.issued_at_micros
150 > MAX_SUPERVISOR_ASSERTION_TTL_MICROS
151 {
152 return Err(InvalidError::new(
153 "supervisor assertion validity window is invalid",
154 ));
155 }
156 if self.key_id.len() != SUPERVISOR_ASSERTION_KEY_ID_BYTES
157 || self.signature.len() != SUPERVISOR_ASSERTION_SIGNATURE_BYTES
158 {
159 return Err(InvalidError::new(
160 "supervisor assertion key id or signature length is invalid",
161 ));
162 }
163 Ok(())
164 }
165}
166
167#[derive(
169 Clone,
170 Copy,
171 Debug,
172 PartialEq,
173 Eq,
174 Hash,
175 Serialize,
176 Deserialize,
177 strum::Display,
178 strum::EnumString,
179 strum::VariantArray,
180)]
181#[strum(serialize_all = "snake_case")]
182#[serde(rename_all = "snake_case")]
183#[non_exhaustive]
184pub enum Action {
185 Read,
186 Write,
187 Delete,
188 Admin,
189 #[serde(other)]
194 Unrecognized,
195}
196
197#[derive(
199 Clone,
200 Copy,
201 Debug,
202 Default,
203 PartialEq,
204 Eq,
205 Serialize,
206 Deserialize,
207 strum::Display,
208 strum::EnumString,
209 strum::VariantArray,
210)]
211#[strum(serialize_all = "snake_case")]
212#[serde(rename_all = "snake_case")]
213#[non_exhaustive]
214pub enum ResourceKind {
215 #[default]
217 All,
218 Literal,
220 Prefix,
222}
223
224#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
226pub struct ResourcePattern {
227 #[serde(default)]
228 pub kind: ResourceKind,
229 #[serde(default, skip_serializing_if = "String::is_empty")]
230 pub value: String,
231}
232
233impl ResourcePattern {
234 pub fn all() -> Self {
236 Self::default()
237 }
238
239 pub fn literal(value: impl Into<String>) -> Self {
241 Self {
242 kind: ResourceKind::Literal,
243 value: value.into(),
244 }
245 }
246
247 pub fn prefix(value: impl Into<String>) -> Self {
249 Self {
250 kind: ResourceKind::Prefix,
251 value: value.into(),
252 }
253 }
254
255 pub fn matches(&self, resource: Option<&str>) -> bool {
258 match (self.kind, resource) {
259 (ResourceKind::All, _) => true,
260 (ResourceKind::Literal, Some(r)) => r == self.value,
261 (ResourceKind::Prefix, Some(r)) => r.starts_with(&self.value),
262 (_, None) => false,
263 }
264 }
265}
266
267#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
270pub struct Grant {
271 pub effect: Effect,
272 pub feature: Feature,
273 pub action: Action,
274 #[serde(default)]
275 pub resource: ResourcePattern,
276}
277
278#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
281pub struct Role {
282 pub name: String,
283 pub grants: Vec<Grant>,
284}
285
286#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub struct RoleBinding {
289 pub user_id: u32,
290 pub roles: Vec<String>,
291}
292
293pub fn validate_role_name(name: &str) -> Result<(), InvalidError> {
300 crate::validate::validate_safelisted_name("role name", name, MAX_ROLE_NAME_BYTES)
301}
302
303pub fn feature_action(code: u32) -> Option<(Feature, Action)> {
307 let pair = match code {
308 AGDX_QUERY_CODE | AGDX_QUERY_PAGE_CODE | AGDX_QUERY_STATUS_CODE => {
309 (Feature::Query, Action::Read)
310 }
311 AGDX_QUERY_CANCEL_CODE => (Feature::Query, Action::Delete),
312 AGDX_DESTINATION_GET_CODE | AGDX_DESTINATION_LIST_CODE | AGDX_QUERY_ROUTE_LIST_CODE => {
313 (Feature::Destination, Action::Read)
314 }
315 AGDX_GET_PROJECTION_CODE
316 | AGDX_LIST_PROJECTIONS_CODE
317 | AGDX_GET_SCHEMA_CODE
318 | AGDX_LIST_SCHEMAS_CODE
319 | AGDX_DECODE_RECORD_CODE => (Feature::Projection, Action::Read),
320 AGDX_REGISTER_SCHEMA_CODE => (Feature::Projection, Action::Admin),
321 AGDX_KV_GET_CODE | AGDX_KV_SCAN_CODE | AGDX_KV_NAMESPACES_CODE | AGDX_KV_EXISTS_CODE => {
322 (Feature::Kv, Action::Read)
323 }
324 AGDX_KV_SET_CODE
329 | AGDX_KV_CAS_CODE
330 | AGDX_KV_CAS_FENCED_CODE
331 | AGDX_KV_PATCH_CODE
332 | AGDX_KV_EXPIRE_CODE
333 | AGDX_KV_COPY_CODE
334 | AGDX_KV_MOVE_CODE => (Feature::Kv, Action::Write),
335 AGDX_KV_LEASE_CODE | AGDX_KV_LEASE_RENEW_CODE | AGDX_KV_RELEASE_CODE => {
341 (Feature::KvLease, Action::Admin)
342 }
343 AGDX_KV_DELETE_CODE | AGDX_KV_DELETE_MANY_CODE => (Feature::Kv, Action::Delete),
344 AGDX_FORK_LIST_CODE => (Feature::Fork, Action::Read),
345 AGDX_FORK_CREATE_CODE | AGDX_FORK_PUT_CODE => (Feature::Fork, Action::Write),
346 AGDX_FORK_PROMOTE_CODE => (Feature::Fork, Action::Admin),
347 AGDX_FORK_DELETE_CODE => (Feature::Fork, Action::Delete),
348 AGDX_GRAPH_QUERY_CODE | AGDX_GRAPH_NEIGHBORS_CODE => (Feature::Graph, Action::Read),
349 AGDX_GRAPH_UPSERT_CODE => (Feature::Graph, Action::Write),
350 AGDX_AGENT_STATUS_CODE | AGDX_AGENT_LIST_CODE => (Feature::Agent, Action::Read),
351 AGDX_AGENT_SUBMIT_CODE => (Feature::Agent, Action::Write),
352 AGDX_AGENT_CANCEL_CODE => (Feature::Agent, Action::Delete),
353 AGDX_GET_FILTER_CODE
354 | AGDX_LIST_FILTERS_CODE
355 | AGDX_LIST_FILTER_REVISIONS_CODE
356 | AGDX_GET_FILTER_BINDING_CODE
357 | AGDX_LIST_FILTER_BINDINGS_CODE
358 | AGDX_FILTER_OPERATION_CODE => (Feature::Filter, Action::Read),
359 _ => return None,
360 };
361 Some(pair)
362}
363
364pub const ACTION_COUNT: usize = 5;
371
372pub fn action_index(feature: Feature, action: Action) -> usize {
377 feature as usize * ACTION_COUNT + action as usize
378}
379
380const _: () = {
388 assert!(
389 <Action as strum::VariantArray>::VARIANTS.len() == ACTION_COUNT,
390 "ACTION_COUNT must equal the number of Action variants"
391 );
392 assert!(
393 <Feature as strum::VariantArray>::VARIANTS.len() * ACTION_COUNT <= 128,
394 "authz coarse-capability bitmask overflow: Feature count * ACTION_COUNT exceeds 128 bits"
395 );
396};
397
398pub fn grants_allow(
402 grants: &[Grant],
403 feature: Feature,
404 action: Action,
405 resource: Option<&str>,
406) -> bool {
407 let mut allowed = false;
408 for grant in grants {
409 if grant.feature == feature && grant.action == action && grant.resource.matches(resource) {
410 match grant.effect {
411 Effect::Deny => return false,
412 Effect::Allow => allowed = true,
413 }
414 }
415 }
416 allowed
417}
418
419pub fn delegated_allow(
423 agent: &[Grant],
424 user: &[Grant],
425 feature: Feature,
426 action: Action,
427 resource: Option<&str>,
428) -> bool {
429 grants_allow(agent, feature, action, resource) && grants_allow(user, feature, action, resource)
430}
431
432#[derive(Clone, Debug, Serialize, Deserialize)]
434pub struct WhoamiReq {
435 pub v: u32,
436}
437
438#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
440pub struct WhoamiReply {
441 pub v: u32,
442 pub roles: Vec<String>,
443 pub grants: Vec<Grant>,
444}
445
446#[derive(Clone, Debug, Serialize, Deserialize)]
449pub struct ListRolesReq {
450 pub v: u32,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub name_prefix: Option<String>,
453 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub search: Option<String>,
455}
456
457#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
459pub struct ListRolesReply {
460 pub v: u32,
461 pub roles: Vec<Role>,
462}
463
464#[derive(Clone, Debug, Serialize, Deserialize)]
466pub struct GetRoleReq {
467 pub v: u32,
468 pub name: String,
469}
470
471#[derive(Clone, Debug, Serialize, Deserialize)]
473pub struct GetBindingsReq {
474 pub v: u32,
475 pub user_id: u32,
476}
477
478#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
480pub struct BindingsReply {
481 pub v: u32,
482 pub roles: Vec<String>,
483}
484
485#[derive(Clone, Debug, Serialize, Deserialize)]
487pub struct DefineRoleReq {
488 pub v: u32,
489 pub role: Role,
490 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub mutation_id: Option<String>,
492}
493
494#[derive(Clone, Debug, Serialize, Deserialize)]
496pub struct DeleteRoleReq {
497 pub v: u32,
498 pub name: String,
499 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub mutation_id: Option<String>,
501}
502
503#[derive(Clone, Debug, Serialize, Deserialize)]
505pub struct BindRolesReq {
506 pub v: u32,
507 pub user_id: u32,
508 pub roles: Vec<String>,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub expect_revision: Option<u64>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub mutation_id: Option<String>,
518}
519
520#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
522#[serde(rename_all = "snake_case")]
523pub enum AuthzSubject {
524 Role(String),
526 Binding { user_id: u32 },
528 All,
530}
531
532#[derive(Clone, Debug, Serialize, Deserialize)]
535pub struct AuthzHistoryReq {
536 pub v: u32,
537 pub subject: AuthzSubject,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub after_revision: Option<u64>,
540 pub limit: u32,
541}
542
543#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
545#[serde(rename_all = "snake_case")]
546pub enum AuthzEventKind {
547 RoleDefined(String),
549 RoleDeleted(String),
551 RolesBound { user_id: u32, roles: Vec<String> },
553}
554
555#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
557pub struct AuthzEvent {
558 pub revision: u64,
559 pub actor: String,
560 pub at_micros: u64,
561 pub op: AuthzEventKind,
562}
563
564#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
566pub struct AuthzHistoryReply {
567 pub v: u32,
568 pub events: Vec<AuthzEvent>,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub next_after_revision: Option<u64>,
571}
572
573#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
575#[non_exhaustive]
576pub enum AuthzReply {
577 Ok,
579 Whoami(WhoamiReply),
581 Roles(ListRolesReply),
583 Role(Option<Role>),
585 Bindings(BindingsReply),
587 History(AuthzHistoryReply),
589 Err(AuthzError),
590}
591
592#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
594#[non_exhaustive]
595pub enum AuthzError {
596 #[error("authz not supported: {0}")]
597 Unsupported(String),
598 #[error("unauthorized")]
599 Unauthorized,
600 #[error("unknown role: {0}")]
601 UnknownRole(String),
602 #[error("invalid role name: {0}")]
604 InvalidName(String),
605 #[error("revision conflict: current is {current_revision}")]
609 Conflict { current_revision: u64 },
610 #[error("unsupported authz op version (expected {expected}, got {got})")]
611 Version { expected: u32, got: u32 },
612}
613
614#[cfg(all(test, feature = "cbor"))]
615mod tests {
616 use super::*;
617 use crate::framing::{decode_named, encode_named};
618
619 #[test]
620 fn given_a_role_when_round_tripped_then_should_preserve_grants() {
621 let role = Role {
622 name: "kv-reader".to_string(),
623 grants: vec![
624 Grant {
625 effect: Effect::Allow,
626 feature: Feature::Kv,
627 action: Action::Read,
628 resource: ResourcePattern::prefix("agent-abc/"),
629 },
630 Grant {
631 effect: Effect::Deny,
632 feature: Feature::Kv,
633 action: Action::Read,
634 resource: ResourcePattern::literal("agent-abc/secret"),
635 },
636 ],
637 };
638 let bytes = encode_named(&role).expect("role serializes");
639 let back: Role = decode_named(&bytes).expect("role deserializes");
640 assert_eq!(back, role);
641 }
642
643 #[test]
644 fn given_role_names_when_validated_then_should_enforce_charset_and_length() {
645 assert!(validate_role_name("kv-reader").is_ok());
646 assert!(validate_role_name("ops.admin_2").is_ok());
647 assert!(validate_role_name(&"r".repeat(MAX_ROLE_NAME_BYTES)).is_ok());
648 assert!(validate_role_name("").is_err(), "empty");
649 assert!(validate_role_name("bad name").is_err(), "space");
650 assert!(validate_role_name("rĂ´le").is_err(), "non-ascii");
651 assert!(validate_role_name(&"r".repeat(MAX_ROLE_NAME_BYTES + 1)).is_err());
652 }
653
654 #[test]
655 fn given_a_resource_pattern_when_matched_then_should_honor_its_kind() {
656 assert!(ResourcePattern::all().matches(Some("anything")));
657 assert!(ResourcePattern::all().matches(None));
658 assert!(ResourcePattern::literal("ns").matches(Some("ns")));
659 assert!(!ResourcePattern::literal("ns").matches(Some("ns2")));
660 assert!(ResourcePattern::prefix("agent-").matches(Some("agent-abc")));
661 assert!(!ResourcePattern::prefix("agent-").matches(Some("other")));
662 assert!(!ResourcePattern::literal("ns").matches(None));
665 assert!(!ResourcePattern::prefix("agent-").matches(None));
666 }
667
668 #[test]
669 fn given_delegation_when_checked_then_agent_is_intersected_with_the_user() {
670 let allow = |feature, action, resource| Grant {
671 effect: Effect::Allow,
672 feature,
673 action,
674 resource,
675 };
676 let agent = vec![
679 allow(Feature::Kv, Action::Read, ResourcePattern::all()),
680 allow(Feature::Kv, Action::Write, ResourcePattern::all()),
681 ];
682 let user = vec![allow(
683 Feature::Kv,
684 Action::Read,
685 ResourcePattern::prefix("shared/"),
686 )];
687 assert!(delegated_allow(
688 &agent,
689 &user,
690 Feature::Kv,
691 Action::Read,
692 Some("shared/x")
693 ));
694 assert!(!delegated_allow(
696 &agent,
697 &user,
698 Feature::Kv,
699 Action::Read,
700 Some("private/x")
701 ));
702 assert!(!delegated_allow(
704 &agent,
705 &user,
706 Feature::Kv,
707 Action::Write,
708 Some("shared/x")
709 ));
710 assert!(!grants_allow(&[], Feature::Kv, Action::Read, None));
712 }
713
714 #[test]
715 fn given_a_command_code_when_classified_then_should_map_to_feature_and_action() {
716 assert_eq!(
717 feature_action(AGDX_KV_GET_CODE),
718 Some((Feature::Kv, Action::Read))
719 );
720 assert_eq!(
721 feature_action(AGDX_KV_SET_CODE),
722 Some((Feature::Kv, Action::Write))
723 );
724 assert_eq!(
725 feature_action(AGDX_KV_DELETE_CODE),
726 Some((Feature::Kv, Action::Delete))
727 );
728 assert_eq!(
731 feature_action(AGDX_KV_LEASE_CODE),
732 Some((Feature::KvLease, Action::Admin))
733 );
734 assert_eq!(
735 feature_action(AGDX_KV_LEASE_RENEW_CODE),
736 Some((Feature::KvLease, Action::Admin))
737 );
738 assert_eq!(
739 feature_action(AGDX_KV_RELEASE_CODE),
740 Some((Feature::KvLease, Action::Admin))
741 );
742 assert_eq!(
743 feature_action(AGDX_KV_CAS_FENCED_CODE),
744 Some((Feature::Kv, Action::Write))
745 );
746 assert_eq!(
747 feature_action(AGDX_REGISTER_SCHEMA_CODE),
748 Some((Feature::Projection, Action::Admin))
749 );
750 assert_eq!(
751 feature_action(AGDX_QUERY_CODE),
752 Some((Feature::Query, Action::Read))
753 );
754 assert_eq!(
755 feature_action(AGDX_GRAPH_UPSERT_CODE),
756 Some((Feature::Graph, Action::Write))
757 );
758 assert_eq!(
759 feature_action(AGDX_DESTINATION_GET_CODE),
760 Some((Feature::Destination, Action::Read))
761 );
762 assert_eq!(feature_action(AGDX_CHECKPOINT_CODE), None);
763 assert_eq!(feature_action(AGDX_DESTINATION_HTTP_CODE), None);
764 assert_eq!(feature_action(AGDX_HELLO_CODE), None);
766 assert_eq!(feature_action(AGDX_BATCH_CODE), None);
767 assert_eq!(feature_action(AGDX_AUTHZ_WHOAMI_CODE), None);
768 assert_eq!(feature_action(AGDX_CONTROL_PUBLISH_CODE), None);
769 }
770
771 #[test]
772 fn given_feature_action_pairs_when_indexed_then_should_fit_a_u128_mask() {
773 use strum::VariantArray;
774 let mut seen = std::collections::HashSet::new();
775 for &feature in Feature::VARIANTS {
776 for &action in Action::VARIANTS {
777 let index = action_index(feature, action);
778 assert!(index < 128, "index {index} must fit a u128 mask");
779 assert!(seen.insert(index), "index {index} collided");
780 }
781 }
782 assert_eq!(action_index(Feature::Destination, Action::Read), 40);
783 assert_eq!(action_index(Feature::Checkpoint, Action::Admin), 48);
784 }
785
786 #[test]
787 fn given_an_unknown_feature_or_action_when_decoded_then_should_be_unrecognized_and_deny() {
788 let json = r#"{"effect":"allow","feature":"quantum","action":"teleport","resource":{"kind":"all"}}"#;
791 let grant: Grant =
792 serde_json::from_str(json).expect("an unknown feature/action still decodes");
793 assert_eq!(grant.feature, Feature::Unrecognized);
794 assert_eq!(grant.action, Action::Unrecognized);
795 assert!(!grants_allow(&[grant], Feature::Kv, Action::Read, None));
799 assert_eq!(Feature::Unrecognized.to_string(), "unrecognized");
803 assert_eq!(Action::Unrecognized.to_string(), "unrecognized");
804 assert_eq!("unrecognized".parse(), Ok(Feature::Unrecognized));
805 assert_eq!("unrecognized".parse(), Ok(Action::Unrecognized));
806 }
807
808 #[test]
809 fn given_an_authz_reply_when_round_tripped_then_should_preserve_the_variant() {
810 let reply = AuthzReply::Whoami(WhoamiReply {
811 v: AUTHZ_OP_VERSION,
812 roles: vec!["admin".to_string()],
813 grants: vec![Grant {
814 effect: Effect::Allow,
815 feature: Feature::Kv,
816 action: Action::Write,
817 resource: ResourcePattern::all(),
818 }],
819 });
820 let bytes = encode_named(&reply).expect("reply serializes");
821 let back: AuthzReply = decode_named(&bytes).expect("reply deserializes");
822 assert_eq!(back, reply);
823 }
824}