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}