Skip to main content

tf_types/
bridge_spiffe.rs

1//! SPIFFE bridge. Mirrors `tools/tf-types-ts/src/core/bridge-spiffe.ts`.
2
3use regex::Regex;
4
5use crate::bridges::{Bridge, BridgeError, BridgeKind};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct ParsedSpiffeId {
9    pub trust_domain: String,
10    pub path: String,
11    pub raw: String,
12}
13
14pub fn parse_spiffe_id(id: &str) -> Result<ParsedSpiffeId, BridgeError> {
15    if id.is_empty() {
16        return Err(BridgeError::InvalidInput("empty SPIFFE ID".into()));
17    }
18    let rest = id.strip_prefix("spiffe://").ok_or_else(|| {
19        BridgeError::InvalidInput(format!("SPIFFE ID must start with spiffe://, got {:?}", id))
20    })?;
21    let (trust_domain, path) = match rest.find('/') {
22        Some(i) => (&rest[..i], &rest[i + 1..]),
23        None => (rest, ""),
24    };
25    if trust_domain.is_empty() {
26        return Err(BridgeError::InvalidInput(
27            "SPIFFE ID has no trust domain".into(),
28        ));
29    }
30    if path.is_empty() {
31        return Err(BridgeError::InvalidInput("SPIFFE ID has no path".into()));
32    }
33    let re = Regex::new(r"^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$").unwrap();
34    if !re.is_match(trust_domain) {
35        return Err(BridgeError::InvalidInput(format!(
36            "SPIFFE trust domain is not DNS-like: {}",
37            trust_domain
38        )));
39    }
40    Ok(ParsedSpiffeId {
41        trust_domain: trust_domain.to_owned(),
42        path: path.to_owned(),
43        raw: id.to_owned(),
44    })
45}
46
47pub fn spiffe_to_actor_id(id: &str) -> Result<String, BridgeError> {
48    let parsed = parse_spiffe_id(id)?;
49    Ok(format!(
50        "tf:actor:service:{}/{}",
51        parsed.trust_domain, parsed.path
52    ))
53}
54
55pub fn actor_id_to_spiffe(actor_id: &str) -> Result<String, BridgeError> {
56    let re = Regex::new(r"^tf:actor:([^:]+):(.+)$").unwrap();
57    let caps = re
58        .captures(actor_id)
59        .ok_or_else(|| BridgeError::InvalidInput(format!("malformed actor URI: {}", actor_id)))?;
60    let type_segment = caps.get(1).unwrap().as_str();
61    let path_segment = caps.get(2).unwrap().as_str();
62    if type_segment != "service" {
63        return Err(BridgeError::Unsupported(format!(
64            "SPIFFE bridge only projects service actors, got {}",
65            type_segment
66        )));
67    }
68    let slash = path_segment.find('/').ok_or_else(|| {
69        BridgeError::InvalidInput(format!(
70            "service actor path must be <trust-domain>/<path>, got {}",
71            path_segment
72        ))
73    })?;
74    let trust_domain = &path_segment[..slash];
75    let tail = &path_segment[slash + 1..];
76    Ok(format!("spiffe://{}/{}", trust_domain, tail))
77}
78
79pub struct SpiffeBridge {
80    pub bridge_id: String,
81    pub trust_domain: String,
82}
83
84impl SpiffeBridge {
85    pub fn new(bridge_id: impl Into<String>, trust_domain: impl Into<String>) -> Self {
86        SpiffeBridge {
87            bridge_id: bridge_id.into(),
88            trust_domain: trust_domain.into(),
89        }
90    }
91
92    pub fn to_actor_id(&self, id: &str) -> Result<String, BridgeError> {
93        spiffe_to_actor_id(id)
94    }
95
96    pub fn to_spiffe(&self, actor_id: &str) -> Result<String, BridgeError> {
97        actor_id_to_spiffe(actor_id)
98    }
99}
100
101impl Bridge for SpiffeBridge {
102    fn bridge_id(&self) -> &str {
103        &self.bridge_id
104    }
105    fn kind(&self) -> BridgeKind {
106        BridgeKind::Spiffe
107    }
108    fn trust_domain(&self) -> &str {
109        &self.trust_domain
110    }
111}