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