use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TenantMode {
Single,
QueryParam,
ClientId,
}
impl Default for TenantMode {
fn default() -> Self {
TenantMode::Single
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TenantConfig {
pub mode: TenantMode,
pub tenant: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct TenantResolution {
pub client_id_tenant: Option<String>,
pub admin_override_tenant: Option<String>,
pub default_tenant: Option<String>,
}
impl TenantResolution {
pub fn resolve(&self) -> Option<String> {
self.client_id_tenant
.as_ref()
.or(self.admin_override_tenant.as_ref())
.or(self.default_tenant.as_ref())
.cloned()
}
pub fn tenant_id(&self) -> String {
self.resolve().unwrap_or_else(|| "default".to_string())
}
}
impl TenantConfig {
pub fn single() -> Self {
Self {
mode: TenantMode::Single,
tenant: None,
}
}
pub fn query_param(tenant: String) -> Self {
Self {
mode: TenantMode::QueryParam,
tenant: Some(tenant),
}
}
pub fn client_id(client_id: String) -> Self {
Self {
mode: TenantMode::ClientId,
tenant: Some(client_id),
}
}
pub fn apply_to_url(&self, url: &str) -> Result<String, String> {
match &self.mode {
TenantMode::Single => Ok(url.to_string()),
TenantMode::QueryParam => {
if let Some(tenant) = &self.tenant {
let separator = if url.contains('?') { "&" } else { "?" };
Ok(format!("{}{}tenant_id={}", url, separator, tenant))
} else {
Err("Tenant identifier required for query param mode".to_string())
}
}
TenantMode::ClientId => {
if let Some(client_id) = &self.tenant {
let separator = if url.contains('?') { "&" } else { "?" };
Ok(format!("{}{}client_id={}", url, separator, client_id))
} else {
Err("Client ID required for client_id mode".to_string())
}
}
}
}
pub fn validate(&self) -> Result<(), String> {
match &self.mode {
TenantMode::Single => Ok(()),
TenantMode::QueryParam => {
if self.tenant.is_none() {
Err("Query param mode requires tenant".to_string())
} else {
Ok(())
}
}
TenantMode::ClientId => {
if self.tenant.is_none() {
Err("Client ID mode requires client_id".to_string())
} else {
Ok(())
}
}
}
}
}
impl fmt::Display for TenantConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.mode {
TenantMode::Single => write!(f, "Single tenant"),
TenantMode::QueryParam => {
if let Some(tenant) = &self.tenant {
write!(f, "Query param: tenant_id={}", tenant)
} else {
write!(f, "Query param (unconfigured)")
}
}
TenantMode::ClientId => write!(f, "Client ID based"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_tenant() {
let config = TenantConfig::single();
assert_eq!(config.mode, TenantMode::Single);
assert!(config.validate().is_ok());
}
#[test]
fn test_query_param_tenant() {
let config = TenantConfig::query_param("xiaojinpro".to_string());
assert_eq!(config.mode, TenantMode::QueryParam);
assert!(config.validate().is_ok());
let url = "https://auth.xiaojinpro.com/test";
let result = config.apply_to_url(url).unwrap();
assert_eq!(result, "https://auth.xiaojinpro.com/test?tenant_id=xiaojinpro");
let url_with_params = "https://auth.xiaojinpro.com/test?foo=bar";
let result = config.apply_to_url(url_with_params).unwrap();
assert_eq!(result, "https://auth.xiaojinpro.com/test?foo=bar&tenant_id=xiaojinpro");
}
#[test]
fn test_client_id_tenant() {
let config = TenantConfig::client_id("xjp-web".to_string());
assert_eq!(config.mode, TenantMode::ClientId);
assert!(config.validate().is_ok());
let url = "https://auth.xiaojinpro.com/.well-known/openid-configuration";
let result = config.apply_to_url(url).unwrap();
assert_eq!(result, "https://auth.xiaojinpro.com/.well-known/openid-configuration?client_id=xjp-web");
let url_with_params = "https://auth.xiaojinpro.com/.well-known/openid-configuration?foo=bar";
let result = config.apply_to_url(url_with_params).unwrap();
assert_eq!(result, "https://auth.xiaojinpro.com/.well-known/openid-configuration?foo=bar&client_id=xjp-web");
}
#[test]
fn test_client_id_tenant_validation() {
let mut config = TenantConfig {
mode: TenantMode::ClientId,
tenant: None,
};
assert!(config.validate().is_err());
config.tenant = Some("xjp-web".to_string());
assert!(config.validate().is_ok());
}
#[test]
fn test_tenant_resolution_priority() {
let resolution = TenantResolution {
client_id_tenant: Some("client-tenant".to_string()),
admin_override_tenant: Some("admin-tenant".to_string()),
default_tenant: Some("default-tenant".to_string()),
};
assert_eq!(resolution.resolve(), Some("client-tenant".to_string()));
let resolution = TenantResolution {
client_id_tenant: None,
admin_override_tenant: Some("admin-tenant".to_string()),
default_tenant: Some("default-tenant".to_string()),
};
assert_eq!(resolution.resolve(), Some("admin-tenant".to_string()));
let resolution = TenantResolution {
client_id_tenant: None,
admin_override_tenant: None,
default_tenant: Some("default-tenant".to_string()),
};
assert_eq!(resolution.resolve(), Some("default-tenant".to_string()));
let resolution = TenantResolution::default();
assert_eq!(resolution.resolve(), None);
assert_eq!(resolution.tenant_id(), "default");
}
}