tf_types/
bridge_spiffe.rs1use crate::bridges::{Bridge, BridgeError, BridgeKind};
4
5fn 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; };
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 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 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}