Skip to main content

uvb_core/
tenant.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// Type-safe tenant identifier wrapper to prevent cross-tenant data leakage
5///
6/// This newtype provides compile-time guarantees that:
7/// 1. Tenant IDs cannot be accidentally mixed with other string types
8/// 2. All tenant-scoped operations explicitly require a TenantId
9/// 3. Cross-tenant operations are caught at compile time
10///
11/// # Security
12///
13/// By making TenantId a distinct type, we enforce that all storage operations,
14/// queries, and business logic explicitly handle tenant isolation. This prevents
15/// entire classes of security vulnerabilities where tenant_id strings might be
16/// accidentally omitted, swapped, or compared incorrectly.
17///
18/// # Examples
19///
20/// ```
21/// use uvb_core::TenantId;
22///
23/// // Create a tenant ID
24/// let tenant_id = TenantId::new("tenant_123");
25///
26/// // Access the inner value when needed
27/// assert_eq!(tenant_id.as_str(), "tenant_123");
28///
29/// // Clone is cheap (wraps a String)
30/// let cloned = tenant_id.clone();
31/// assert_eq!(tenant_id, cloned);
32/// ```
33#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(transparent)]
35pub struct TenantId(String);
36
37impl TenantId {
38    /// Create a new TenantId from a string
39    ///
40    /// # Arguments
41    ///
42    /// * `id` - The tenant identifier string
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use uvb_core::TenantId;
48    ///
49    /// let tenant_id = TenantId::new("tenant_123");
50    /// ```
51    pub fn new(id: impl Into<String>) -> Self {
52        Self(id.into())
53    }
54
55    /// Get the tenant ID as a string slice
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use uvb_core::TenantId;
61    ///
62    /// let tenant_id = TenantId::new("tenant_123");
63    /// assert_eq!(tenant_id.as_str(), "tenant_123");
64    /// ```
65    pub fn as_str(&self) -> &str {
66        &self.0
67    }
68
69    /// Consume the TenantId and return the inner String
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// use uvb_core::TenantId;
75    ///
76    /// let tenant_id = TenantId::new("tenant_123");
77    /// let inner: String = tenant_id.into_inner();
78    /// assert_eq!(inner, "tenant_123");
79    /// ```
80    pub fn into_inner(self) -> String {
81        self.0
82    }
83
84    /// Validate that the tenant ID is well-formed
85    ///
86    /// Returns `true` if the tenant ID:
87    /// - Is not empty
88    /// - Contains only alphanumeric characters, hyphens, and underscores
89    /// - Is between 1 and 255 characters long
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// use uvb_core::TenantId;
95    ///
96    /// assert!(TenantId::new("tenant_123").is_valid());
97    /// assert!(TenantId::new("tenant-abc-123").is_valid());
98    /// assert!(!TenantId::new("").is_valid());
99    /// assert!(!TenantId::new("tenant with spaces").is_valid());
100    /// ```
101    pub fn is_valid(&self) -> bool {
102        !self.0.is_empty()
103            && self.0.len() <= 255
104            && self
105                .0
106                .chars()
107                .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
108    }
109}
110
111impl fmt::Display for TenantId {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "{}", self.0)
114    }
115}
116
117impl AsRef<str> for TenantId {
118    fn as_ref(&self) -> &str {
119        &self.0
120    }
121}
122
123impl From<String> for TenantId {
124    fn from(s: String) -> Self {
125        Self(s)
126    }
127}
128
129impl From<&str> for TenantId {
130    fn from(s: &str) -> Self {
131        Self(s.to_string())
132    }
133}
134
135impl From<TenantId> for String {
136    fn from(tenant_id: TenantId) -> Self {
137        tenant_id.0
138    }
139}
140
141// Allow comparing TenantId with &str and String for convenience
142impl PartialEq<str> for TenantId {
143    fn eq(&self, other: &str) -> bool {
144        self.0 == other
145    }
146}
147
148impl PartialEq<&str> for TenantId {
149    fn eq(&self, other: &&str) -> bool {
150        self.0 == *other
151    }
152}
153
154impl PartialEq<String> for TenantId {
155    fn eq(&self, other: &String) -> bool {
156        &self.0 == other
157    }
158}
159
160impl PartialEq<TenantId> for String {
161    fn eq(&self, other: &TenantId) -> bool {
162        self == &other.0
163    }
164}
165
166impl PartialEq<TenantId> for &str {
167    fn eq(&self, other: &TenantId) -> bool {
168        *self == other.0.as_str()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_tenant_id_creation() {
178        let tenant_id = TenantId::new("tenant_123");
179        assert_eq!(tenant_id.as_str(), "tenant_123");
180        assert_eq!(tenant_id.to_string(), "tenant_123");
181    }
182
183    #[test]
184    fn test_tenant_id_clone() {
185        let tenant_id = TenantId::new("tenant_123");
186        let cloned = tenant_id.clone();
187        assert_eq!(tenant_id, cloned);
188    }
189
190    #[test]
191    fn test_tenant_id_from_string() {
192        let s = String::from("tenant_123");
193        let tenant_id: TenantId = s.into();
194        assert_eq!(tenant_id.as_str(), "tenant_123");
195    }
196
197    #[test]
198    fn test_tenant_id_from_str() {
199        let tenant_id: TenantId = "tenant_123".into();
200        assert_eq!(tenant_id.as_str(), "tenant_123");
201    }
202
203    #[test]
204    fn test_tenant_id_into_string() {
205        let tenant_id = TenantId::new("tenant_123");
206        let s: String = tenant_id.into();
207        assert_eq!(s, "tenant_123");
208    }
209
210    #[test]
211    fn test_tenant_id_validation() {
212        // Valid cases
213        assert!(TenantId::new("tenant_123").is_valid());
214        assert!(TenantId::new("tenant-abc-123").is_valid());
215        assert!(TenantId::new("a").is_valid());
216        assert!(TenantId::new("123").is_valid());
217        assert!(TenantId::new("tenant_with_underscores").is_valid());
218        assert!(TenantId::new("tenant-with-hyphens").is_valid());
219
220        // Invalid cases
221        assert!(!TenantId::new("").is_valid());
222        assert!(!TenantId::new("tenant with spaces").is_valid());
223        assert!(!TenantId::new("tenant@example.com").is_valid());
224        assert!(!TenantId::new("tenant/path").is_valid());
225        assert!(!TenantId::new("x".repeat(256)).is_valid());
226    }
227
228    #[test]
229    fn test_tenant_id_equality() {
230        let tenant_id1 = TenantId::new("tenant_123");
231        let tenant_id2 = TenantId::new("tenant_123");
232        let tenant_id3 = TenantId::new("tenant_456");
233
234        assert_eq!(tenant_id1, tenant_id2);
235        assert_ne!(tenant_id1, tenant_id3);
236    }
237
238    #[test]
239    fn test_tenant_id_serialization() {
240        let tenant_id = TenantId::new("tenant_123");
241        let json = serde_json::to_string(&tenant_id).unwrap();
242        assert_eq!(json, r#""tenant_123""#);
243
244        let deserialized: TenantId = serde_json::from_str(&json).unwrap();
245        assert_eq!(deserialized, tenant_id);
246    }
247
248    #[test]
249    fn test_tenant_id_hash() {
250        use std::collections::HashMap;
251
252        let mut map = HashMap::new();
253        let tenant_id = TenantId::new("tenant_123");
254        map.insert(tenant_id.clone(), "value");
255
256        assert_eq!(map.get(&tenant_id), Some(&"value"));
257    }
258
259    #[test]
260    fn test_tenant_id_string_comparison() {
261        let tenant_id = TenantId::new("tenant_123");
262        let string = String::from("tenant_123");
263        let str_ref = "tenant_123";
264
265        // TenantId == String
266        assert_eq!(tenant_id, string);
267        assert_eq!(string, tenant_id);
268
269        // TenantId == &str
270        assert_eq!(tenant_id, str_ref);
271        assert_eq!(str_ref, tenant_id);
272
273        // TenantId == str (through deref)
274        assert!(tenant_id == *str_ref);
275
276        // Inequality
277        assert_ne!(tenant_id, "different_tenant");
278        assert_ne!(String::from("different_tenant"), tenant_id);
279    }
280}