Skip to main content

auth_framework/tenant/
context.rs

1//! Tenant context and identity management
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Unique identifier for a tenant
7#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
8pub struct TenantId(String);
9
10impl TenantId {
11    /// Create a new TenantId
12    pub fn new(id: impl Into<String>) -> Self {
13        Self(id.into())
14    }
15
16    /// Get the tenant ID as a string
17    pub fn as_str(&self) -> &str {
18        &self.0
19    }
20
21    /// Convert into the inner string
22    pub fn into_inner(self) -> String {
23        self.0
24    }
25
26    /// Validate the tenant ID format
27    ///
28    /// Tenant IDs must:
29    /// - Be non-empty
30    /// - Only contain alphanumeric characters, hyphens, and underscores
31    /// - Be between 1 and 64 characters
32    pub fn validate(&self) -> Result<(), String> {
33        if self.0.is_empty() {
34            return Err("Tenant ID cannot be empty".to_string());
35        }
36
37        if self.0.len() > 64 {
38            return Err("Tenant ID cannot exceed 64 characters".to_string());
39        }
40
41        if !self
42            .0
43            .chars()
44            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
45        {
46            return Err(
47                "Tenant ID can only contain alphanumeric characters, hyphens, and underscores"
48                    .to_string(),
49            );
50        }
51
52        Ok(())
53    }
54}
55
56impl fmt::Display for TenantId {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "{}", self.0)
59    }
60}
61
62impl AsRef<str> for TenantId {
63    fn as_ref(&self) -> &str {
64        &self.0
65    }
66}
67
68/// Metadata about a tenant
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TenantMetadata {
71    /// Display name for the tenant
72    pub name: String,
73
74    /// Optional description
75    pub description: Option<String>,
76
77    /// When the tenant was created
78    pub created_at: chrono::DateTime<chrono::Utc>,
79
80    /// Custom attributes for the tenant
81    pub attributes: std::collections::HashMap<String, serde_json::Value>,
82}
83
84impl TenantMetadata {
85    /// Create new tenant metadata
86    pub fn new(name: impl Into<String>) -> Self {
87        Self {
88            name: name.into(),
89            description: None,
90            created_at: chrono::Utc::now(),
91            attributes: std::collections::HashMap::new(),
92        }
93    }
94
95    /// Set the description
96    pub fn with_description(mut self, description: impl Into<String>) -> Self {
97        self.description = Some(description.into());
98        self
99    }
100
101    /// Add a custom attribute
102    pub fn with_attribute(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
103        self.attributes.insert(key.into(), value);
104        self
105    }
106}
107
108/// Context for a specific tenant
109#[derive(Debug, Clone)]
110pub struct TenantContext {
111    /// Unique identifier for the tenant
112    pub id: TenantId,
113
114    /// Metadata about the tenant
115    pub metadata: TenantMetadata,
116
117    /// Whether the tenant is active
118    pub active: bool,
119}
120
121impl TenantContext {
122    /// Create a new tenant context
123    pub fn new(id: TenantId, metadata: TenantMetadata) -> Result<Self, String> {
124        id.validate()?;
125
126        Ok(Self {
127            id,
128            metadata,
129            active: true,
130        })
131    }
132
133    /// Create a new tenant with name only
134    pub fn with_name(id: impl Into<String>, name: impl Into<String>) -> Result<Self, String> {
135        let id = TenantId::new(id);
136        id.validate()?;
137
138        Ok(Self {
139            id,
140            metadata: TenantMetadata::new(name),
141            active: true,
142        })
143    }
144
145    /// Deactivate the tenant
146    pub fn deactivate(&mut self) {
147        self.active = false;
148    }
149
150    /// Activate the tenant
151    pub fn activate(&mut self) {
152        self.active = true;
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_tenant_id_validation() {
162        // Valid IDs
163        assert!(TenantId::new("tenant-123").validate().is_ok());
164        assert!(TenantId::new("tenant_123").validate().is_ok());
165        assert!(TenantId::new("acme-corp").validate().is_ok());
166
167        // Invalid IDs
168        assert!(TenantId::new("").validate().is_err());
169        assert!(TenantId::new("tenant@123").validate().is_err());
170        assert!(TenantId::new("a".repeat(65)).validate().is_err());
171    }
172
173    #[test]
174    fn test_tenant_context_creation() {
175        let context = TenantContext::with_name("acme", "ACME Corp").unwrap();
176        assert_eq!(context.id.as_str(), "acme");
177        assert_eq!(context.metadata.name, "ACME Corp");
178        assert!(context.active);
179    }
180
181    #[test]
182    fn test_tenant_activation() {
183        let mut context = TenantContext::with_name("test", "Test Tenant").unwrap();
184        assert!(context.active);
185
186        context.deactivate();
187        assert!(!context.active);
188
189        context.activate();
190        assert!(context.active);
191    }
192}