1use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
11pub struct Caller {
12 pub user_id: u32,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub agent: Option<crate::agent::AgentId>,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub on_behalf_of: Option<String>,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27pub struct ForwardedScope {
28 pub stream_id: u32,
29 pub stream: String,
30 pub stream_created_at_micros: u64,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct ForwardedQuery {
37 pub user_id: u32,
40 pub client_id: u128,
43 pub correlation: Option<String>,
45 #[serde(with = "crate::encoding::bin_bytes")]
47 pub query_envelope: Vec<u8>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub grants: Vec<crate::authz::Grant>,
52 #[serde(default, skip_serializing_if = "crate::authz::Tenancy::is_deployment")]
55 pub tenancy: crate::authz::Tenancy,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub scope: Option<ForwardedScope>,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68pub struct ForwardedCommand {
69 pub user_id: u32,
72 pub client_id: u128,
75 pub correlation: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub operation_id: Option<u128>,
81 #[serde(default)]
86 pub read_all: bool,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub scope: Option<ForwardedScope>,
90 pub command_code: u32,
93 #[serde(with = "crate::encoding::bin_bytes")]
95 pub payload: Vec<u8>,
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub grants: Vec<crate::authz::Grant>,
100 #[serde(default, skip_serializing_if = "is_false")]
104 pub read_barrier: bool,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub min_position: Option<crate::mutation::MutationPosition>,
110 #[serde(default, skip_serializing_if = "is_false")]
115 pub positioned_reply: bool,
116 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub min_sessions: Vec<crate::mutation::SessionPosition>,
120 #[serde(default, skip_serializing_if = "crate::authz::Tenancy::is_deployment")]
123 pub tenancy: crate::authz::Tenancy,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub agent: Option<crate::agent::AgentId>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub on_behalf_of: Option<String>,
135}
136
137#[expect(clippy::trivially_copy_pass_by_ref, reason = "serde skip predicate")]
138fn is_false(value: &bool) -> bool {
139 !*value
140}
141
142#[cfg(all(test, feature = "cbor"))]
143mod tests {
144 use super::*;
145 use crate::framing::{decode_named, encode_named};
146
147 fn contains_byte_string(frame: &[u8], len: u8, fill: u8) -> bool {
152 let head = 0x40 | len; let mut want = vec![head];
154 want.extend(std::iter::repeat_n(fill, len as usize));
155 frame.windows(want.len()).any(|w| w == want.as_slice())
156 }
157
158 #[test]
159 fn given_forwarded_query_when_encoded_then_envelope_rides_as_a_byte_string() {
160 let frame = encode_named(&ForwardedQuery {
161 user_id: 7,
162 client_id: 42,
163 correlation: Some("conv-1".to_owned()),
164 query_envelope: vec![0x90; 5],
165 grants: Vec::new(),
166 tenancy: Default::default(),
167 scope: None,
168 })
169 .expect("encodes");
170 assert!(
171 contains_byte_string(&frame, 5, 0x90),
172 "query_envelope must encode as a CBOR byte string, got {frame:02x?}"
173 );
174 }
175
176 #[test]
177 fn given_forwarded_command_when_encoded_then_payload_rides_as_a_byte_string() {
178 let frame = encode_named(&ForwardedCommand {
179 read_barrier: false,
180 min_position: None,
181 positioned_reply: false,
182 min_sessions: Vec::new(),
183 tenancy: Default::default(),
184 user_id: 7,
185 client_id: 42,
186 correlation: None,
187 operation_id: Some(17),
188 read_all: true,
189 scope: None,
190 command_code: 1_000_000,
191 payload: vec![0x90; 5],
192 grants: Vec::new(),
193 agent: None,
194 on_behalf_of: None,
195 })
196 .expect("encodes");
197 assert!(
198 contains_byte_string(&frame, 5, 0x90),
199 "payload must encode as a CBOR byte string, got {frame:02x?}"
200 );
201 }
202
203 #[test]
204 fn given_forwarded_frames_when_round_tripped_then_should_preserve_fields() {
205 let query = ForwardedQuery {
206 user_id: 1,
207 client_id: u128::MAX,
208 correlation: Some("c".to_owned()),
209 query_envelope: vec![0xff, 0x00, 0x18, 0x7f],
210 grants: Vec::new(),
211 tenancy: Default::default(),
212 scope: Some(ForwardedScope {
213 stream_id: 3,
214 stream: "acme".to_owned(),
215 stream_created_at_micros: 9,
216 }),
217 };
218 let back: ForwardedQuery =
219 decode_named(&encode_named(&query).expect("encodes")).expect("decodes");
220 assert_eq!(back, query);
221
222 let command = ForwardedCommand {
223 read_barrier: false,
224 min_position: None,
225 positioned_reply: false,
226 min_sessions: Vec::new(),
227 tenancy: Default::default(),
228 user_id: 2,
229 client_id: 9,
230 correlation: None,
231 operation_id: Some(9),
232 read_all: false,
233 scope: None,
234 command_code: 42,
235 payload: vec![0xde, 0xad, 0xbe, 0xef],
236 grants: Vec::new(),
237 agent: None,
238 on_behalf_of: None,
239 };
240 let back: ForwardedCommand =
241 decode_named(&encode_named(&command).expect("encodes")).expect("decodes");
242 assert_eq!(back, command);
243 }
244
245 #[test]
246 fn given_a_root_caller_with_an_agent_when_round_tripped_then_should_keep_both() {
247 let caller = Caller {
248 user_id: 0,
249 agent: Some("orbit-planner".parse().expect("agent id")),
250 on_behalf_of: None,
251 };
252
253 let back: Caller = decode_named(&encode_named(&caller).expect("encodes")).expect("decodes");
254
255 assert_eq!(back, caller);
256 }
257
258 #[test]
259 fn given_a_caller_written_for_an_end_user_when_round_tripped_then_should_keep_who_it_was_for() {
260 let caller = Caller {
261 user_id: 3,
262 agent: None,
263 on_behalf_of: Some("1793690355287073".to_owned()),
264 };
265
266 let back: Caller = decode_named(&encode_named(&caller).expect("encodes")).expect("decodes");
267
268 assert_eq!(back, caller);
269 }
270
271 #[test]
272 fn given_a_caller_with_an_empty_agent_when_decoded_then_should_be_refused() {
273 #[derive(serde::Serialize)]
274 struct Unchecked<'a> {
275 user_id: u32,
276 agent: &'a str,
277 }
278 let frame = encode_named(&Unchecked {
279 user_id: 3,
280 agent: "",
281 })
282 .expect("encodes");
283
284 assert!(decode_named::<Caller>(&frame).is_err());
285 }
286
287 #[test]
288 fn given_a_forwarded_command_with_an_agent_when_read_without_the_field_then_should_ignore_it() {
289 #[derive(Debug, PartialEq, serde::Deserialize)]
290 struct BeforeAgent {
291 user_id: u32,
292 client_id: u128,
293 command_code: u32,
294 }
295 let command = ForwardedCommand {
296 read_barrier: false,
297 min_position: None,
298 positioned_reply: false,
299 min_sessions: Vec::new(),
300 tenancy: Default::default(),
301 user_id: 0,
302 client_id: 9,
303 correlation: None,
304 operation_id: Some(9),
305 read_all: false,
306 scope: None,
307 command_code: 42,
308 payload: vec![1],
309 grants: Vec::new(),
310 agent: Some("orbit-planner".parse().expect("agent id")),
311 on_behalf_of: None,
312 };
313
314 let before: BeforeAgent =
315 decode_named(&encode_named(&command).expect("encodes")).expect("decodes");
316
317 assert_eq!(
318 before,
319 BeforeAgent {
320 user_id: 0,
321 client_id: 9,
322 command_code: 42,
323 }
324 );
325 }
326}