Skip to main content

harn_vm/
actor_chain.rs

1//! RFC 8693 actor/principal chain support.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::value::{DictMap, VmValue};
8
9const SUB: &str = "sub";
10const ACT: &str = "act";
11const MAY_ACT: &str = "may_act";
12
13/// A first-class RFC 8693 §4.1 actor chain.
14///
15/// `origin` serializes as the top-level `sub` claim: the human principal the
16/// request is ultimately on behalf of. `actors` serializes as nested `act`
17/// claims in RFC order, with the current actor outermost and prior actors
18/// nested beneath it. For authorization, only the top-level token claims and
19/// [`ActorChain::current`] are authoritative. The audit-not-authz rule is that
20/// nested actors are audit history only and must not be used to grant access.
21#[derive(Clone, Debug, PartialEq, Eq, Hash)]
22pub struct ActorChain {
23    origin: String,
24    actors: Vec<String>,
25    may_act: Option<String>,
26}
27
28/// Alias for APIs that name the full acting chain as a principal.
29pub type Principal = ActorChain;
30
31impl ActorChain {
32    /// Create a chain with only an originating subject and no acting agents.
33    pub fn new(origin: impl Into<String>) -> Self {
34        Self {
35            origin: origin.into(),
36            actors: Vec::new(),
37            may_act: None,
38        }
39    }
40
41    /// Build a chain from actors already ordered current-first, matching the
42    /// RFC 8693 nested `act` traversal order.
43    pub fn from_parts<I, S>(origin: impl Into<String>, actors: I) -> Self
44    where
45        I: IntoIterator<Item = S>,
46        S: Into<String>,
47    {
48        Self {
49            origin: origin.into(),
50            actors: actors.into_iter().map(Into::into).collect(),
51            may_act: None,
52        }
53    }
54
55    /// Insert `actor` as the new current actor.
56    pub fn push(&mut self, actor: impl Into<String>) {
57        self.actors.insert(0, actor.into());
58    }
59
60    /// Chainable form of [`ActorChain::push`].
61    pub fn pushed(mut self, actor: impl Into<String>) -> Self {
62        self.push(actor);
63        self
64    }
65
66    /// The actor that authorization checks should consider authoritative.
67    ///
68    /// When no delegation has occurred, the originating subject is also the
69    /// current actor.
70    pub fn current(&self) -> &str {
71        self.actors
72            .first()
73            .map(String::as_str)
74            .unwrap_or(self.origin.as_str())
75    }
76
77    /// The originating human principal carried in the top-level `sub` claim.
78    pub fn origin(&self) -> &str {
79        &self.origin
80    }
81
82    /// Acting agents in RFC nested-`act` order: current actor first.
83    pub fn actors(&self) -> impl Iterator<Item = &str> {
84        self.actors.iter().map(String::as_str)
85    }
86
87    /// All principals from current actor through the originating subject.
88    pub fn iter(&self) -> ActorChainIter<'_> {
89        ActorChainIter {
90            actors: self.actors.iter(),
91            origin: Some(self.origin.as_str()),
92        }
93    }
94
95    pub fn is_delegated(&self) -> bool {
96        !self.actors.is_empty()
97    }
98
99    /// Authorized actor hint from the optional RFC 8693 §4.4 `may_act` claim.
100    pub fn may_act(&self) -> Option<&str> {
101        self.may_act.as_deref()
102    }
103
104    pub fn set_may_act(&mut self, may_act: impl Into<String>) {
105        self.may_act = Some(may_act.into());
106    }
107
108    pub fn with_may_act(mut self, may_act: impl Into<String>) -> Self {
109        self.set_may_act(may_act);
110        self
111    }
112
113    pub fn clear_may_act(&mut self) {
114        self.may_act = None;
115    }
116
117    pub fn to_json_value(&self) -> serde_json::Value {
118        let mut root = serde_json::Map::new();
119        root.insert(
120            SUB.to_string(),
121            serde_json::Value::String(self.origin.clone()),
122        );
123        if let Some(act) = actor_nodes_to_json(&self.actors) {
124            root.insert(ACT.to_string(), act);
125        }
126        if let Some(may_act) = &self.may_act {
127            root.insert(MAY_ACT.to_string(), principal_claim_json(may_act));
128        }
129        serde_json::Value::Object(root)
130    }
131
132    pub fn from_json_value(value: &serde_json::Value) -> Result<Self, ActorChainError> {
133        let root = expect_json_object(value, "$")?;
134        let origin = json_sub(root, "$")?;
135        let actors = json_actor_subjects(root.get(ACT), "$.act")?;
136        let may_act = root
137            .get(MAY_ACT)
138            .map(|value| {
139                let may_act = expect_json_object(value, "$.may_act")?;
140                json_sub(may_act, "$.may_act")
141            })
142            .transpose()?;
143        Ok(Self {
144            origin,
145            actors,
146            may_act,
147        })
148    }
149
150    pub fn to_vm_value(&self) -> VmValue {
151        crate::schema::json_to_vm_value(&self.to_json_value())
152    }
153
154    pub fn from_vm_value(value: &VmValue) -> Result<Self, ActorChainError> {
155        let root = expect_vm_dict(value, "$")?;
156        let origin = vm_sub(root, "$")?;
157        let actors = vm_actor_subjects(root.get(ACT), "$.act")?;
158        let may_act = root
159            .get(MAY_ACT)
160            .map(|value| {
161                let may_act = expect_vm_dict(value, "$.may_act")?;
162                vm_sub(may_act, "$.may_act")
163            })
164            .transpose()?;
165        Ok(Self {
166            origin,
167            actors,
168            may_act,
169        })
170    }
171}
172
173impl Serialize for ActorChain {
174    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
175    where
176        S: serde::Serializer,
177    {
178        self.to_json_value().serialize(serializer)
179    }
180}
181
182impl<'de> Deserialize<'de> for ActorChain {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: serde::Deserializer<'de>,
186    {
187        let value = serde_json::Value::deserialize(deserializer)?;
188        Self::from_json_value(&value).map_err(serde::de::Error::custom)
189    }
190}
191
192impl TryFrom<&serde_json::Value> for ActorChain {
193    type Error = ActorChainError;
194
195    fn try_from(value: &serde_json::Value) -> Result<Self, Self::Error> {
196        Self::from_json_value(value)
197    }
198}
199
200impl From<&ActorChain> for VmValue {
201    fn from(chain: &ActorChain) -> Self {
202        chain.to_vm_value()
203    }
204}
205
206impl From<ActorChain> for VmValue {
207    fn from(chain: ActorChain) -> Self {
208        chain.to_vm_value()
209    }
210}
211
212impl TryFrom<&VmValue> for ActorChain {
213    type Error = ActorChainError;
214
215    fn try_from(value: &VmValue) -> Result<Self, Self::Error> {
216        Self::from_vm_value(value)
217    }
218}
219
220pub struct ActorChainIter<'a> {
221    actors: std::slice::Iter<'a, String>,
222    origin: Option<&'a str>,
223}
224
225impl<'a> Iterator for ActorChainIter<'a> {
226    type Item = &'a str;
227
228    fn next(&mut self) -> Option<Self::Item> {
229        self.actors
230            .next()
231            .map(String::as_str)
232            .or_else(|| self.origin.take())
233    }
234}
235
236impl<'a> IntoIterator for &'a ActorChain {
237    type Item = &'a str;
238    type IntoIter = ActorChainIter<'a>;
239
240    fn into_iter(self) -> Self::IntoIter {
241        self.iter()
242    }
243}
244
245#[derive(Clone, Debug, PartialEq, Eq)]
246pub struct ActorChainError {
247    message: String,
248}
249
250impl ActorChainError {
251    fn new(message: impl Into<String>) -> Self {
252        Self {
253            message: message.into(),
254        }
255    }
256
257    pub fn message(&self) -> &str {
258        &self.message
259    }
260}
261
262impl fmt::Display for ActorChainError {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        self.message.fmt(f)
265    }
266}
267
268impl std::error::Error for ActorChainError {}
269
270fn actor_nodes_to_json(actors: &[String]) -> Option<serde_json::Value> {
271    let mut next = None;
272    for actor in actors.iter().rev() {
273        let mut node = serde_json::Map::new();
274        node.insert(SUB.to_string(), serde_json::Value::String(actor.clone()));
275        if let Some(act) = next {
276            node.insert(ACT.to_string(), act);
277        }
278        next = Some(serde_json::Value::Object(node));
279    }
280    next
281}
282
283fn principal_claim_json(subject: &str) -> serde_json::Value {
284    let mut claim = serde_json::Map::new();
285    claim.insert(
286        SUB.to_string(),
287        serde_json::Value::String(subject.to_string()),
288    );
289    serde_json::Value::Object(claim)
290}
291
292fn expect_json_object<'a>(
293    value: &'a serde_json::Value,
294    path: &str,
295) -> Result<&'a serde_json::Map<String, serde_json::Value>, ActorChainError> {
296    value
297        .as_object()
298        .ok_or_else(|| ActorChainError::new(format!("{path} must be an object")))
299}
300
301fn json_sub(
302    object: &serde_json::Map<String, serde_json::Value>,
303    path: &str,
304) -> Result<String, ActorChainError> {
305    object
306        .get(SUB)
307        .and_then(serde_json::Value::as_str)
308        .map(ToString::to_string)
309        .ok_or_else(|| ActorChainError::new(format!("{path}.sub must be a string")))
310}
311
312fn json_actor_subjects(
313    first: Option<&serde_json::Value>,
314    first_path: &str,
315) -> Result<Vec<String>, ActorChainError> {
316    let mut actors = Vec::new();
317    let mut current = first;
318    let mut path = first_path.to_string();
319    while let Some(value) = current {
320        let object = expect_json_object(value, &path)?;
321        actors.push(json_sub(object, &path)?);
322        current = object.get(ACT);
323        path.push_str(".act");
324    }
325    Ok(actors)
326}
327
328fn expect_vm_dict<'a>(value: &'a VmValue, path: &str) -> Result<&'a DictMap, ActorChainError> {
329    value
330        .as_dict()
331        .ok_or_else(|| ActorChainError::new(format!("{path} must be a dict")))
332}
333
334fn vm_sub(object: &DictMap, path: &str) -> Result<String, ActorChainError> {
335    match object.get(SUB) {
336        Some(VmValue::String(subject)) => Ok(subject.to_string()),
337        _ => Err(ActorChainError::new(format!("{path}.sub must be a string"))),
338    }
339}
340
341fn vm_actor_subjects(
342    first: Option<&VmValue>,
343    first_path: &str,
344) -> Result<Vec<String>, ActorChainError> {
345    let mut actors = Vec::new();
346    let mut current = first;
347    let mut path = first_path.to_string();
348    while let Some(value) = current {
349        let object = expect_vm_dict(value, &path)?;
350        actors.push(vm_sub(object, &path)?);
351        current = object.get(ACT);
352        path.push_str(".act");
353    }
354    Ok(actors)
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use proptest::prelude::*;
361    use serde_json::json;
362
363    fn principal() -> impl Strategy<Value = String> {
364        proptest::string::string_regex("[a-z][a-z0-9_]{0,24}").unwrap()
365    }
366
367    fn actor_subjects_from_json(value: &serde_json::Value) -> Vec<String> {
368        let mut subjects = Vec::new();
369        let mut current = value.get(ACT);
370        while let Some(node) = current {
371            subjects.push(node[SUB].as_str().expect("act.sub").to_string());
372            current = node.get(ACT);
373        }
374        subjects
375    }
376
377    #[test]
378    fn push_makes_new_actor_current_and_serializes_rfc_shape() {
379        let chain = ActorChain::new("user")
380            .pushed("service77")
381            .pushed("service16");
382
383        assert_eq!(chain.origin(), "user");
384        assert_eq!(chain.current(), "service16");
385        assert_eq!(
386            chain.iter().collect::<Vec<_>>(),
387            vec!["service16", "service77", "user"]
388        );
389        assert_eq!(
390            serde_json::to_value(&chain).unwrap(),
391            json!({
392                "sub": "user",
393                "act": {
394                    "sub": "service16",
395                    "act": {
396                        "sub": "service77"
397                    }
398                }
399            })
400        );
401    }
402
403    #[test]
404    fn may_act_serializes_as_authorized_actor_claim() {
405        let chain = ActorChain::new("user").with_may_act("admin");
406
407        assert_eq!(
408            serde_json::to_value(&chain).unwrap(),
409            json!({
410                "sub": "user",
411                "may_act": {
412                    "sub": "admin"
413                }
414            })
415        );
416    }
417
418    #[test]
419    fn vm_value_conversion_round_trips_plain_harn_dicts() {
420        let value = crate::schema::json_to_vm_value(&json!({
421            "sub": "user",
422            "act": {
423                "sub": "service16",
424                "act": {
425                    "sub": "service77"
426                }
427            },
428            "may_act": {
429                "sub": "admin"
430            }
431        }));
432
433        let chain = ActorChain::try_from(&value).unwrap();
434        assert_eq!(chain.origin(), "user");
435        assert_eq!(chain.current(), "service16");
436        assert_eq!(
437            chain.actors().collect::<Vec<_>>(),
438            vec!["service16", "service77"]
439        );
440        assert_eq!(chain.may_act(), Some("admin"));
441
442        let encoded = chain.to_vm_value();
443        assert_eq!(ActorChain::try_from(&encoded).unwrap(), chain);
444    }
445
446    proptest! {
447        #[test]
448        fn serde_round_trip_preserves_nesting_order(
449            origin in principal(),
450            actors in proptest::collection::vec(principal(), 0..10),
451            may_act in proptest::option::of(principal()),
452        ) {
453            let mut chain = ActorChain::from_parts(origin.clone(), actors.clone());
454            if let Some(may_act) = may_act.as_deref() {
455                chain.set_may_act(may_act);
456            }
457
458            let encoded = serde_json::to_value(&chain).unwrap();
459            prop_assert_eq!(encoded[SUB].as_str(), Some(origin.as_str()));
460            let encoded_actors = actor_subjects_from_json(&encoded);
461            prop_assert_eq!(encoded_actors.as_slice(), actors.as_slice());
462            prop_assert_eq!(
463                encoded.get(MAY_ACT).and_then(|claim| claim[SUB].as_str()),
464                may_act.as_deref()
465            );
466
467            let decoded: ActorChain = serde_json::from_value(encoded.clone()).unwrap();
468            prop_assert_eq!(&decoded, &chain);
469            prop_assert_eq!(serde_json::to_value(&decoded).unwrap(), encoded);
470            prop_assert_eq!(decoded.origin(), origin.as_str());
471            prop_assert_eq!(
472                decoded.current(),
473                actors.first().map(String::as_str).unwrap_or(origin.as_str())
474            );
475
476            let expected_iter = actors
477                .iter()
478                .map(String::as_str)
479                .chain(std::iter::once(origin.as_str()))
480                .collect::<Vec<_>>();
481            prop_assert_eq!(decoded.iter().collect::<Vec<_>>(), expected_iter);
482        }
483    }
484}