1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub enum TenantMode {
9 Single,
11 QueryParam,
13 ClientId,
15}
16
17impl Default for TenantMode {
18 fn default() -> Self {
19 TenantMode::Single
20 }
21}
22
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct TenantConfig {
26 pub mode: TenantMode,
28 pub tenant: Option<String>,
30}
31
32#[derive(Debug, Clone, Default)]
34pub struct TenantResolution {
35 pub client_id_tenant: Option<String>,
37 pub admin_override_tenant: Option<String>,
39 pub default_tenant: Option<String>,
41}
42
43impl TenantResolution {
44 pub fn resolve(&self) -> Option<String> {
46 self.client_id_tenant
47 .as_ref()
48 .or(self.admin_override_tenant.as_ref())
49 .or(self.default_tenant.as_ref())
50 .cloned()
51 }
52
53 pub fn tenant_id(&self) -> String {
55 self.resolve().unwrap_or_else(|| "default".to_string())
56 }
57}
58
59impl TenantConfig {
60 pub fn single() -> Self {
62 Self {
63 mode: TenantMode::Single,
64 tenant: None,
65 }
66 }
67
68
69 pub fn query_param(tenant: String) -> Self {
71 Self {
72 mode: TenantMode::QueryParam,
73 tenant: Some(tenant),
74 }
75 }
76
77 pub fn client_id(client_id: String) -> Self {
79 Self {
80 mode: TenantMode::ClientId,
81 tenant: Some(client_id),
82 }
83 }
84
85 pub fn apply_to_url(&self, url: &str) -> Result<String, String> {
87 match &self.mode {
88 TenantMode::Single => Ok(url.to_string()),
89 TenantMode::QueryParam => {
90 if let Some(tenant) = &self.tenant {
91 let separator = if url.contains('?') { "&" } else { "?" };
92 Ok(format!("{}{}tenant_id={}", url, separator, tenant))
93 } else {
94 Err("Tenant identifier required for query param mode".to_string())
95 }
96 }
97 TenantMode::ClientId => {
98 if let Some(client_id) = &self.tenant {
99 let separator = if url.contains('?') { "&" } else { "?" };
100 Ok(format!("{}{}client_id={}", url, separator, client_id))
101 } else {
102 Err("Client ID required for client_id mode".to_string())
103 }
104 }
105 }
106 }
107
108
109 pub fn validate(&self) -> Result<(), String> {
111 match &self.mode {
112 TenantMode::Single => Ok(()),
113 TenantMode::QueryParam => {
114 if self.tenant.is_none() {
115 Err("Query param mode requires tenant".to_string())
116 } else {
117 Ok(())
118 }
119 }
120 TenantMode::ClientId => {
121 if self.tenant.is_none() {
122 Err("Client ID mode requires client_id".to_string())
123 } else {
124 Ok(())
125 }
126 }
127 }
128 }
129}
130
131impl fmt::Display for TenantConfig {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match &self.mode {
134 TenantMode::Single => write!(f, "Single tenant"),
135 TenantMode::QueryParam => {
136 if let Some(tenant) = &self.tenant {
137 write!(f, "Query param: tenant_id={}", tenant)
138 } else {
139 write!(f, "Query param (unconfigured)")
140 }
141 }
142 TenantMode::ClientId => write!(f, "Client ID based"),
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_single_tenant() {
153 let config = TenantConfig::single();
154 assert_eq!(config.mode, TenantMode::Single);
155 assert!(config.validate().is_ok());
156 }
157
158
159 #[test]
160 fn test_query_param_tenant() {
161 let config = TenantConfig::query_param("xiaojinpro".to_string());
162 assert_eq!(config.mode, TenantMode::QueryParam);
163 assert!(config.validate().is_ok());
164
165 let url = "https://auth.xiaojinpro.com/test";
166 let result = config.apply_to_url(url).unwrap();
167 assert_eq!(result, "https://auth.xiaojinpro.com/test?tenant_id=xiaojinpro");
168
169 let url_with_params = "https://auth.xiaojinpro.com/test?foo=bar";
170 let result = config.apply_to_url(url_with_params).unwrap();
171 assert_eq!(result, "https://auth.xiaojinpro.com/test?foo=bar&tenant_id=xiaojinpro");
172 }
173
174 #[test]
175 fn test_client_id_tenant() {
176 let config = TenantConfig::client_id("xjp-web".to_string());
177 assert_eq!(config.mode, TenantMode::ClientId);
178 assert!(config.validate().is_ok());
179
180 let url = "https://auth.xiaojinpro.com/.well-known/openid-configuration";
181 let result = config.apply_to_url(url).unwrap();
182 assert_eq!(result, "https://auth.xiaojinpro.com/.well-known/openid-configuration?client_id=xjp-web");
183
184 let url_with_params = "https://auth.xiaojinpro.com/.well-known/openid-configuration?foo=bar";
185 let result = config.apply_to_url(url_with_params).unwrap();
186 assert_eq!(result, "https://auth.xiaojinpro.com/.well-known/openid-configuration?foo=bar&client_id=xjp-web");
187 }
188
189 #[test]
190 fn test_client_id_tenant_validation() {
191 let mut config = TenantConfig {
192 mode: TenantMode::ClientId,
193 tenant: None,
194 };
195
196 assert!(config.validate().is_err());
198
199 config.tenant = Some("xjp-web".to_string());
201 assert!(config.validate().is_ok());
202 }
203
204 #[test]
205 fn test_tenant_resolution_priority() {
206 let resolution = TenantResolution {
207 client_id_tenant: Some("client-tenant".to_string()),
208 admin_override_tenant: Some("admin-tenant".to_string()),
209 default_tenant: Some("default-tenant".to_string()),
210 };
211
212 assert_eq!(resolution.resolve(), Some("client-tenant".to_string()));
214
215 let resolution = TenantResolution {
217 client_id_tenant: None,
218 admin_override_tenant: Some("admin-tenant".to_string()),
219 default_tenant: Some("default-tenant".to_string()),
220 };
221 assert_eq!(resolution.resolve(), Some("admin-tenant".to_string()));
222
223 let resolution = TenantResolution {
225 client_id_tenant: None,
226 admin_override_tenant: None,
227 default_tenant: Some("default-tenant".to_string()),
228 };
229 assert_eq!(resolution.resolve(), Some("default-tenant".to_string()));
230
231 let resolution = TenantResolution::default();
233 assert_eq!(resolution.resolve(), None);
234 assert_eq!(resolution.tenant_id(), "default");
235 }
236}