Skip to main content

tf_types/
bridge_spiffe.rs

1//! SPIFFE bridge. Mirrors `tools/tf-types-ts/src/core/bridge-spiffe.ts`.
2
3use crate::bridges::{Bridge, BridgeError, BridgeKind};
4
5/// DNS-like label check, equivalent to the anchored pattern
6/// `[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?`: alphanumeric first and
7/// last byte, alphanumeric / `.` / `-` in between.
8fn is_dns_like(s: &str) -> bool {
9    let bytes = s.as_bytes();
10    let Some((&first, rest)) = bytes.split_first() else {
11        return false;
12    };
13    if !first.is_ascii_alphanumeric() {
14        return false;
15    }
16    let Some((&last, middle)) = rest.split_last() else {
17        return true; // single character
18    };
19    last.is_ascii_alphanumeric()
20        && middle
21            .iter()
22            .all(|&b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-')
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct ParsedSpiffeId {
27    pub trust_domain: String,
28    pub path: String,
29    pub raw: String,
30}
31
32pub fn parse_spiffe_id(id: &str) -> Result<ParsedSpiffeId, BridgeError> {
33    if id.is_empty() {
34        return Err(BridgeError::InvalidInput("empty SPIFFE ID".into()));
35    }
36    let rest = id.strip_prefix("spiffe://").ok_or_else(|| {
37        BridgeError::InvalidInput(format!("SPIFFE ID must start with spiffe://, got {:?}", id))
38    })?;
39    let (trust_domain, path) = match rest.find('/') {
40        Some(i) => (&rest[..i], &rest[i + 1..]),
41        None => (rest, ""),
42    };
43    if trust_domain.is_empty() {
44        return Err(BridgeError::InvalidInput(
45            "SPIFFE ID has no trust domain".into(),
46        ));
47    }
48    if path.is_empty() {
49        return Err(BridgeError::InvalidInput("SPIFFE ID has no path".into()));
50    }
51    if !is_dns_like(trust_domain) {
52        return Err(BridgeError::InvalidInput(format!(
53            "SPIFFE trust domain is not DNS-like: {}",
54            trust_domain
55        )));
56    }
57    Ok(ParsedSpiffeId {
58        trust_domain: trust_domain.to_owned(),
59        path: path.to_owned(),
60        raw: id.to_owned(),
61    })
62}
63
64pub fn spiffe_to_actor_id(id: &str) -> Result<String, BridgeError> {
65    let parsed = parse_spiffe_id(id)?;
66    Ok(format!(
67        "tf:actor:service:{}/{}",
68        parsed.trust_domain, parsed.path
69    ))
70}
71
72pub fn actor_id_to_spiffe(actor_id: &str) -> Result<String, BridgeError> {
73    // Shape: `tf:actor:<type>:<path>` where <type> has no `:` and <path>
74    // is non-empty (may itself contain `:`).
75    let malformed =
76        || BridgeError::InvalidInput(format!("malformed actor URI: {}", actor_id));
77    let rest = actor_id.strip_prefix("tf:actor:").ok_or_else(malformed)?;
78    let colon = rest.find(':').ok_or_else(malformed)?;
79    let (type_segment, path_segment) = (&rest[..colon], &rest[colon + 1..]);
80    // `path` may contain further `:` but not a newline (parity with the
81    // former `(.+)$` pattern, where `.` excluded `\n`).
82    if type_segment.is_empty() || path_segment.is_empty() || path_segment.contains('\n') {
83        return Err(malformed());
84    }
85    if type_segment != "service" {
86        return Err(BridgeError::Unsupported(format!(
87            "SPIFFE bridge only projects service actors, got {}",
88            type_segment
89        )));
90    }
91    let slash = path_segment.find('/').ok_or_else(|| {
92        BridgeError::InvalidInput(format!(
93            "service actor path must be <trust-domain>/<path>, got {}",
94            path_segment
95        ))
96    })?;
97    let trust_domain = &path_segment[..slash];
98    let tail = &path_segment[slash + 1..];
99    Ok(format!("spiffe://{}/{}", trust_domain, tail))
100}
101
102pub struct SpiffeBridge {
103    pub bridge_id: String,
104    pub trust_domain: String,
105}
106
107impl SpiffeBridge {
108    pub fn new(bridge_id: impl Into<String>, trust_domain: impl Into<String>) -> Self {
109        SpiffeBridge {
110            bridge_id: bridge_id.into(),
111            trust_domain: trust_domain.into(),
112        }
113    }
114
115    pub fn to_actor_id(&self, id: &str) -> Result<String, BridgeError> {
116        spiffe_to_actor_id(id)
117    }
118
119    pub fn to_spiffe(&self, actor_id: &str) -> Result<String, BridgeError> {
120        actor_id_to_spiffe(actor_id)
121    }
122}
123
124impl Bridge for SpiffeBridge {
125    fn bridge_id(&self) -> &str {
126        &self.bridge_id
127    }
128    fn kind(&self) -> BridgeKind {
129        BridgeKind::Spiffe
130    }
131    fn trust_domain(&self) -> &str {
132        &self.trust_domain
133    }
134}