1use crate::{split_stamped_label, NamespaceToken, RuntimeError, RuntimeResult};
2
3#[derive(Debug)]
5pub struct EventActorReadScope {
6 pub actors: Vec<String>,
8 pub default_caller: Option<String>,
10}
11
12pub fn resolve_event_actor_read_scope(
17 token: &NamespaceToken,
18 fleet_readers: &[String],
19 actor: Option<&str>,
20 all_actors: bool,
21) -> RuntimeResult<EventActorReadScope> {
22 if all_actors && actor.is_some() {
23 return Err(RuntimeError::InvalidInput(
24 "all_actors=true cannot be combined with actor".into(),
25 ));
26 }
27 if all_actors && !fleet_readers.contains(&token.actor().id) {
28 return Err(RuntimeError::InvalidInput(format!(
29 "actor {:?} is not a configured fleet reader",
30 token.actor().id
31 )));
32 }
33 let caller = token.actor().label();
34 if let Some(actor) = actor {
35 let (identity, is_self) = match split_stamped_label(actor) {
36 Some((kind, id)) => (
37 if kind == "actor" { id } else { actor },
38 token.actor().kind == kind && token.actor().id == id,
39 ),
40 None => (actor, actor == caller),
41 };
42 if !is_self && !token.visible_namespace_strs().contains(&identity) {
43 return Err(RuntimeError::InvalidInput(format!(
44 "actor {identity:?} is not visible to this caller"
45 )));
46 }
47 }
48 let default_scope = !all_actors && actor.is_none();
49
50 let actors = match actor {
53 Some(a) if split_stamped_label(a).is_some() => vec![a.to_string()],
54 Some(a) => vec![a.to_string(), format!("actor:{a}")],
55 None if all_actors => Vec::new(),
56 None if token.actor().kind == "actor" && split_stamped_label(&caller).is_some() => {
57 vec![format!("actor:{caller}")]
58 }
59 None if token.actor().kind == "actor" => {
60 vec![caller.clone(), format!("actor:{caller}")]
61 }
62 None => vec![caller.clone()],
63 };
64 Ok(EventActorReadScope {
65 actors,
66 default_caller: default_scope.then_some(caller),
67 })
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::{ActorRef, Namespace, RUNTIME_STAMPED_ACTOR_KINDS};
74
75 fn token(kind: &str, id: &str, visible: &[&str]) -> NamespaceToken {
76 NamespaceToken::mint_with_visibility(
77 Namespace::local(),
78 visible
79 .iter()
80 .map(|name| Namespace::parse(name).unwrap())
81 .collect(),
82 ActorRef::new(kind, id),
83 )
84 }
85
86 fn invalid(result: RuntimeResult<EventActorReadScope>, expected: &str) {
87 match result {
88 Err(RuntimeError::InvalidInput(message)) => assert_eq!(message, expected),
89 other => panic!("expected InvalidInput {expected:?}, got {other:?}"),
90 }
91 }
92
93 #[test]
94 fn default_scope_keeps_caller_labels_and_only_permitted_aliases() {
95 for (kind, id, labels, caller) in [
96 ("actor", "worker", vec!["worker", "actor:worker"], "worker"),
97 (
98 "actor",
99 "svc:build",
100 vec!["svc:build", "actor:svc:build"],
101 "svc:build",
102 ),
103 ("agent", "worker", vec!["agent:worker"], "agent:worker"),
104 (
105 "anonymous",
106 "local",
107 vec!["anonymous:local"],
108 "anonymous:local",
109 ),
110 (
111 "service",
112 "worker",
113 vec!["service:worker"],
114 "service:worker",
115 ),
116 ] {
117 let scope =
118 resolve_event_actor_read_scope(&token(kind, id, &[]), &[], None, false).unwrap();
119 assert_eq!(scope.actors, labels, "{kind}:{id}");
120 assert_eq!(scope.default_caller.as_deref(), Some(caller));
121 }
122 for kind in RUNTIME_STAMPED_ACTOR_KINDS {
123 let id = format!("{kind}:worker");
124 let scope = resolve_event_actor_read_scope(&token("actor", &id, &[]), &[], None, false)
125 .unwrap();
126 assert_eq!(scope.actors, vec![format!("actor:{id}")]);
127 assert_eq!(scope.default_caller, Some(id));
128 }
129 }
130
131 #[test]
132 fn explicit_self_filters_keep_exact_stamps_and_do_not_coalesce() {
133 for (kind, id, requested, labels) in [
134 ("actor", "worker", "worker", vec!["worker", "actor:worker"]),
135 ("actor", "worker", "actor:worker", vec!["actor:worker"]),
136 ("agent", "worker", "agent:worker", vec!["agent:worker"]),
137 (
138 "anonymous",
139 "local",
140 "anonymous:local",
141 vec!["anonymous:local"],
142 ),
143 (
144 "service",
145 "worker",
146 "service:worker",
147 vec!["service:worker", "actor:service:worker"],
148 ),
149 ] {
150 let scope =
151 resolve_event_actor_read_scope(&token(kind, id, &[]), &[], Some(requested), false)
152 .unwrap();
153 assert_eq!(scope.actors, labels);
154 assert!(scope.default_caller.is_none());
155 }
156 }
157
158 #[test]
159 fn foreign_visibility_uses_the_documented_exact_identity() {
160 for (requested, identity, labels) in [
161 ("peer", "peer", vec!["peer", "actor:peer"]),
162 ("actor:peer", "peer", vec!["actor:peer"]),
163 ("agent:peer", "agent:peer", vec!["agent:peer"]),
164 (
165 "anonymous:local",
166 "anonymous:local",
167 vec!["anonymous:local"],
168 ),
169 (
170 "service:peer",
171 "service:peer",
172 vec!["service:peer", "actor:service:peer"],
173 ),
174 ] {
175 let scope = resolve_event_actor_read_scope(
176 &token("actor", "worker", &[identity]),
177 &[],
178 Some(requested),
179 false,
180 )
181 .unwrap();
182 assert_eq!(scope.actors, labels);
183 assert!(scope.default_caller.is_none());
184 let descendant = format!("{identity}:child");
185 invalid(
186 resolve_event_actor_read_scope(
187 &token("actor", "worker", &[&descendant]),
188 &[],
189 Some(requested),
190 false,
191 ),
192 &format!("actor {identity:?} is not visible to this caller"),
193 );
194 }
195 }
196
197 #[test]
198 fn reserved_raw_ids_do_not_gain_collapsed_self_access() {
199 for kind in RUNTIME_STAMPED_ACTOR_KINDS {
200 let id = format!("{kind}:worker");
201 let caller = token("actor", &id, &[]);
202 let canonical = format!("actor:{id}");
203 let own =
204 resolve_event_actor_read_scope(&caller, &[], Some(&canonical), false).unwrap();
205 assert_eq!(own.actors, vec![canonical]);
206 let identity = if *kind == "actor" { "worker" } else { &id };
207 invalid(
208 resolve_event_actor_read_scope(&caller, &[], Some(&id), false),
209 &format!("actor {identity:?} is not visible to this caller"),
210 );
211 }
212 }
213
214 #[test]
215 fn aggregate_requires_exact_fleet_id_and_conflict_wins_first() {
216 let caller = token("actor", "worker", &["peer"]);
217 for readers in [vec![], vec!["actor:worker".into()]] {
218 invalid(
219 resolve_event_actor_read_scope(&caller, &readers, None, true),
220 "actor \"worker\" is not a configured fleet reader",
221 );
222 }
223 let readers = vec!["worker".into()];
224 let all = resolve_event_actor_read_scope(&caller, &readers, None, true).unwrap();
225 assert!(all.actors.is_empty());
226 assert!(all.default_caller.is_none());
227 let default = resolve_event_actor_read_scope(&caller, &readers, None, false).unwrap();
228 assert_eq!(default.actors, vec!["worker", "actor:worker"]);
229 assert_eq!(default.default_caller.as_deref(), Some("worker"));
230 for readers in [vec![], readers] {
231 invalid(
232 resolve_event_actor_read_scope(&caller, &readers, Some(""), true),
233 "all_actors=true cannot be combined with actor",
234 );
235 }
236 }
237
238 #[test]
239 fn shared_scope_does_not_add_telemetry_label_validation_to_brain() {
240 let caller = token("actor", " ", &[]);
241 let scope = resolve_event_actor_read_scope(&caller, &[], Some(" "), false).unwrap();
242 assert_eq!(scope.actors, vec![" ", "actor: "]);
243 assert!(scope.default_caller.is_none());
244 }
245}