oauth2_broker/auth/
id.rs

1//! Strongly typed identifiers enforced across the broker domain.
2
3// std
4use std::{borrow::Borrow, ops::Deref};
5// self
6use crate::_prelude::*;
7
8macro_rules! def_id {
9	($name:ident, $doc:literal, $kind:literal) => {
10		#[doc = $doc]
11		#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12		#[serde(try_from = "String", into = "String")]
13		pub struct $name(String);
14		impl $name {
15			/// Creates a new identifier after validation.
16			pub fn new(value: impl AsRef<str>) -> Result<Self, IdentifierError> {
17				let view = value.as_ref();
18
19				validate_view($kind, view)?;
20
21				Ok(Self(view.to_owned()))
22			}
23		}
24		impl Deref for $name {
25			type Target = str;
26
27			fn deref(&self) -> &Self::Target {
28				&self.0
29			}
30		}
31		impl AsRef<str> for $name {
32			fn as_ref(&self) -> &str {
33				&self.0
34			}
35		}
36		impl From<$name> for String {
37			fn from(value: $name) -> Self {
38				value.0
39			}
40		}
41		impl TryFrom<String> for $name {
42			type Error = IdentifierError;
43
44			fn try_from(value: String) -> Result<Self, Self::Error> {
45				validate_view($kind, &value)?;
46
47				Ok(Self(value))
48			}
49		}
50		impl Borrow<str> for $name {
51			fn borrow(&self) -> &str {
52				&self.0
53			}
54		}
55		impl Debug for $name {
56			fn fmt(&self, f: &mut Formatter) -> FmtResult {
57				write!(f, concat!($kind, "({})"), self.0)
58			}
59		}
60		impl Display for $name {
61			fn fmt(&self, f: &mut Formatter) -> FmtResult {
62				f.write_str(&self.0)
63			}
64		}
65		impl FromStr for $name {
66			type Err = IdentifierError;
67
68			fn from_str(s: &str) -> Result<Self, Self::Err> {
69				Self::new(s)
70			}
71		}
72	};
73}
74
75const IDENTIFIER_MAX_LEN: usize = 128;
76
77/// Error returned when identifier validation fails.
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, ThisError)]
79pub enum IdentifierError {
80	/// The identifier was empty or whitespace.
81	#[error("{kind} identifier cannot be empty.")]
82	Empty {
83		/// Kind of identifier (tenant, principal, provider).
84		kind: &'static str,
85	},
86	/// The identifier contains whitespace characters.
87	#[error("{kind} identifier contains whitespace.")]
88	ContainsWhitespace {
89		/// Kind of identifier (tenant, principal, provider).
90		kind: &'static str,
91	},
92	/// The identifier exceeded the allowed character count.
93	#[error("{kind} identifier exceeds {max} characters.")]
94	TooLong {
95		/// Kind of identifier (tenant, principal, provider).
96		kind: &'static str,
97		/// Maximum permitted character count.
98		max: usize,
99	},
100}
101
102def_id! { TenantId, "Unique identifier for a broker tenant.", "Tenant" }
103def_id! { PrincipalId, "Unique identifier for a broker principal.", "Principal" }
104def_id! { ProviderId, "Identifier for an OAuth provider descriptor.", "Provider" }
105
106fn validate_view(kind: &'static str, view: &str) -> Result<(), IdentifierError> {
107	if view.is_empty() {
108		return Err(IdentifierError::Empty { kind });
109	}
110	if view.chars().any(char::is_whitespace) {
111		return Err(IdentifierError::ContainsWhitespace { kind });
112	}
113	if view.len() > IDENTIFIER_MAX_LEN {
114		return Err(IdentifierError::TooLong { kind, max: IDENTIFIER_MAX_LEN });
115	}
116
117	Ok(())
118}
119
120#[cfg(test)]
121mod tests {
122	// self
123	use super::*;
124
125	#[test]
126	fn identifiers_trim_and_validate() {
127		assert!(TenantId::new(" tenant-123").is_err(), "Leading whitespace must be rejected.");
128		assert!(TenantId::new("tenant-123 ").is_err(), "Trailing whitespace must be rejected.");
129
130		let tenant =
131			TenantId::new("tenant-123").expect("Tenant fixture should be considered valid.");
132
133		assert_eq!(tenant.as_ref(), "tenant-123");
134		assert!(PrincipalId::new("").is_err());
135		assert!(ProviderId::new("with space").is_err());
136	}
137
138	#[test]
139	fn serde_round_trip_enforces_validation() {
140		let payload = "\"tenant-42\"";
141		let tenant: TenantId =
142			serde_json::from_str(payload).expect("Tenant should deserialize successfully.");
143
144		assert_eq!(tenant.as_ref(), "tenant-42");
145		assert!(serde_json::from_str::<TenantId>("\"with space\"").is_err());
146		assert!(serde_json::from_str::<TenantId>("\" tenant-42\"").is_err());
147	}
148
149	#[test]
150	fn unicode_whitespace_and_length_limits() {
151		let nbsp = format!("tenant{}id", '\u{00A0}');
152
153		assert!(TenantId::new(&nbsp).is_err());
154
155		let exact = "a".repeat(IDENTIFIER_MAX_LEN);
156
157		TenantId::new(&exact).expect("Exact length should succeed.");
158
159		let too_long = "a".repeat(IDENTIFIER_MAX_LEN + 1);
160
161		assert!(TenantId::new(&too_long).is_err());
162	}
163
164	#[test]
165	fn borrow_supports_fast_lookup() {
166		let map: HashMap<TenantId, u8> = HashMap::from_iter([(
167			TenantId::new("tenant-123").expect("Tenant used for lookup should be valid."),
168			7_u8,
169		)]);
170
171		assert_eq!(map.get("tenant-123"), Some(&7));
172	}
173}