Skip to main content

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