spiffe_rustls/authorizer.rs
1//! Authorization abstractions for SPIFFE ID-based access control.
2
3use crate::error::{AuthorizerConfigError, Result};
4use spiffe::{SpiffeId, TrustDomain};
5use std::collections::BTreeSet;
6use std::sync::Arc;
7
8/// Authorization policy for peer SPIFFE IDs.
9///
10/// Authorization runs **after** cryptographic verification succeeds.
11/// This trait allows you to implement custom authorization logic while
12/// maintaining type safety and auditability.
13pub trait Authorizer: Send + Sync + 'static {
14 /// Returns `true` if the peer SPIFFE ID is authorized.
15 fn authorize(&self, peer: &SpiffeId) -> bool;
16}
17
18impl<F> Authorizer for F
19where
20 F: Fn(&SpiffeId) -> bool + Send + Sync + 'static,
21{
22 fn authorize(&self, peer: &SpiffeId) -> bool {
23 self(peer)
24 }
25}
26
27/// Authorizes any SPIFFE ID (authentication only, no authorization).
28///
29/// This is useful when authorization is performed at another layer
30/// (e.g., application-level RBAC).
31///
32/// This is a zero-sized type that can be used directly without allocation.
33#[derive(Debug, Clone, Copy, Default)]
34pub struct Any;
35
36impl Authorizer for Any {
37 fn authorize(&self, _peer: &SpiffeId) -> bool {
38 true
39 }
40}
41
42/// Authorizes only the exact SPIFFE IDs in the allow list.
43#[derive(Debug)]
44pub struct Exact {
45 allowed: Arc<BTreeSet<SpiffeId>>,
46}
47
48impl Exact {
49 /// Creates a new `Exact` authorizer, failing if any SPIFFE ID is invalid.
50 ///
51 /// All SPIFFE IDs must be valid; invalid IDs are not silently ignored.
52 ///
53 /// If the iterator is empty, the resulting authorizer will authorize no SPIFFE IDs
54 /// (all authorization checks will return `false`).
55 ///
56 /// # Errors
57 ///
58 /// Returns `Error::AuthorizerConfig` if any ID cannot be parsed.
59 pub fn new<I>(ids: I) -> Result<Self>
60 where
61 I: IntoIterator,
62 I::Item: TryInto<SpiffeId>,
63 <I::Item as TryInto<SpiffeId>>::Error: std::fmt::Display,
64 {
65 let mut allowed = BTreeSet::new();
66 for id in ids {
67 let spiffe_id = id
68 .try_into()
69 .map_err(|e| AuthorizerConfigError::InvalidSpiffeId(e.to_string()))?;
70 allowed.insert(spiffe_id);
71 }
72 Ok(Self {
73 allowed: Arc::new(allowed),
74 })
75 }
76}
77
78impl Authorizer for Exact {
79 fn authorize(&self, peer: &SpiffeId) -> bool {
80 self.allowed.contains(peer)
81 }
82}
83
84/// Authorizes any SPIFFE ID from the given trust domains.
85#[derive(Debug)]
86pub struct TrustDomains {
87 allowed: Arc<BTreeSet<TrustDomain>>,
88}
89
90impl TrustDomains {
91 /// Creates a new `TrustDomains` authorizer, failing if any trust domain is invalid.
92 ///
93 /// All trust domains must be valid; invalid domains are not silently ignored.
94 ///
95 /// If the iterator is empty, the resulting authorizer will authorize no trust domains
96 /// (all authorization checks will return `false`).
97 ///
98 /// # Errors
99 ///
100 /// Returns `Error::AuthorizerConfig` if any domain cannot be parsed.
101 pub fn new<I>(domains: I) -> Result<Self>
102 where
103 I: IntoIterator,
104 I::Item: TryInto<TrustDomain>,
105 <I::Item as TryInto<TrustDomain>>::Error: std::fmt::Display,
106 {
107 let mut allowed = BTreeSet::new();
108 for domain in domains {
109 let td = domain
110 .try_into()
111 .map_err(|e| AuthorizerConfigError::InvalidTrustDomain(e.to_string()))?;
112 allowed.insert(td);
113 }
114 Ok(Self {
115 allowed: Arc::new(allowed),
116 })
117 }
118}
119
120impl Authorizer for TrustDomains {
121 fn authorize(&self, peer: &SpiffeId) -> bool {
122 self.allowed.contains(peer.trust_domain())
123 }
124}
125
126/// Returns an authorizer that accepts any SPIFFE ID.
127///
128/// This is useful when authorization is performed at another layer
129/// (e.g., application-level RBAC). Authentication (certificate verification)
130/// still applies.
131///
132/// Returns a zero-sized `Any` value that can be used directly.
133///
134/// # Examples
135///
136/// ```rust
137/// use spiffe_rustls::authorizer;
138///
139/// let auth = authorizer::any();
140/// ```
141pub fn any() -> Any {
142 Any
143}
144
145/// Returns an authorizer that only accepts the exact SPIFFE IDs.
146///
147/// # Arguments
148///
149/// * `ids` - An iterator of SPIFFE IDs (or types that can be converted to `SpiffeId`)
150///
151/// If the iterator is empty, the resulting authorizer will authorize no SPIFFE IDs
152/// (all authorization checks will return `false`).
153///
154/// # Errors
155///
156/// Returns `Error::AuthorizerConfig` if any SPIFFE ID is invalid.
157///
158/// # Examples
159///
160/// ```rust
161/// use spiffe_rustls::authorizer;
162///
163/// // Pass string literals directly - exact() will convert them
164/// let auth = authorizer::exact([
165/// "spiffe://example.org/payment",
166/// "spiffe://example.org/checkout",
167/// ])?;
168/// # Ok::<(), spiffe_rustls::Error>(())
169/// ```
170pub fn exact<I>(ids: I) -> Result<Exact>
171where
172 I: IntoIterator,
173 I::Item: TryInto<SpiffeId>,
174 <I::Item as TryInto<SpiffeId>>::Error: std::fmt::Display,
175{
176 Exact::new(ids)
177}
178
179/// Returns an authorizer that accepts any SPIFFE ID from the given trust domains.
180///
181/// # Arguments
182///
183/// * `domains` - An iterator of trust domains (or types that can be converted to `TrustDomain`)
184///
185/// If the iterator is empty, the resulting authorizer will authorize no trust domains
186/// (all authorization checks will return `false`).
187///
188/// # Errors
189///
190/// Returns `Error::AuthorizerConfig` if any trust domain is invalid.
191///
192/// # Examples
193///
194/// ```rust
195/// use spiffe_rustls::authorizer;
196///
197/// // Pass string literals directly - trust_domains() will convert them
198/// let auth = authorizer::trust_domains([
199/// "broker.example",
200/// "stockmarket.example",
201/// ])?;
202/// # Ok::<(), spiffe_rustls::Error>(())
203/// ```
204pub fn trust_domains<I>(domains: I) -> Result<TrustDomains>
205where
206 I: IntoIterator,
207 I::Item: TryInto<TrustDomain>,
208 <I::Item as TryInto<TrustDomain>>::Error: std::fmt::Display,
209{
210 TrustDomains::new(domains)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_exact_authorizer() {
219 let id1 = SpiffeId::new("spiffe://example.org/service1").unwrap();
220 let id2 = SpiffeId::new("spiffe://example.org/service2").unwrap();
221 let id3 = SpiffeId::new("spiffe://other.org/service1").unwrap();
222
223 let auth = Exact::new([id1.clone(), id2.clone()]).unwrap();
224 assert!(auth.authorize(&id1));
225 assert!(auth.authorize(&id2));
226 assert!(!auth.authorize(&id3));
227 }
228
229 #[test]
230 fn test_exact_authorizer_rejects_invalid() {
231 let result = Exact::new(["invalid-spiffe-id", "also-invalid"]);
232 assert!(result.is_err());
233 }
234
235 #[test]
236 fn test_trust_domains_authorizer() {
237 let td1 = TrustDomain::new("example.org").unwrap();
238 let td2 = TrustDomain::new("other.org").unwrap();
239
240 let id1 = SpiffeId::new("spiffe://example.org/service1").unwrap();
241 let id2 = SpiffeId::new("spiffe://example.org/service2").unwrap();
242 let id3 = SpiffeId::new("spiffe://other.org/service1").unwrap();
243 let id4 = SpiffeId::new("spiffe://third.org/service1").unwrap();
244
245 let auth = TrustDomains::new([td1, td2]).unwrap();
246 assert!(auth.authorize(&id1));
247 assert!(auth.authorize(&id2));
248 assert!(auth.authorize(&id3));
249 assert!(!auth.authorize(&id4));
250 }
251
252 #[test]
253 fn test_trust_domains_authorizer_rejects_invalid() {
254 // Use a string with invalid characters (uppercase and special chars not allowed)
255 // TrustDomain::new explicitly validates the format, so this should fail
256 let result = TrustDomains::new(["Invalid@Trust#Domain"]);
257 assert!(result.is_err());
258
259 // Verify that valid trust domains are accepted
260 let valid = TrustDomains::new(["example.org", "other.org"]).unwrap();
261 let id1 = SpiffeId::new("spiffe://example.org/service").unwrap();
262 let id2 = SpiffeId::new("spiffe://other.org/service").unwrap();
263 let id3 = SpiffeId::new("spiffe://rejected.org/service").unwrap();
264 assert!(valid.authorize(&id1));
265 assert!(valid.authorize(&id2));
266 assert!(!valid.authorize(&id3));
267 }
268
269 #[test]
270 fn test_any_authorizer_always_authorizes() {
271 // Verify that `Any` authorizer accepts all valid SPIFFE IDs regardless of trust domain
272 let auth = any();
273 let id1 = SpiffeId::new("spiffe://example.org/service").unwrap();
274 let id2 = SpiffeId::new("spiffe://other.org/another").unwrap();
275 let id3 = SpiffeId::new("spiffe://test.domain/path/to/resource").unwrap();
276
277 // Any authorizer should accept all SPIFFE IDs
278 assert!(auth.authorize(&id1));
279 assert!(auth.authorize(&id2));
280 assert!(auth.authorize(&id3));
281 }
282}