use crate::store::traits::TenantStore;
use crate::wami::tenant::operations::tenant_operations;
use crate::wami::tenant::{Tenant, TenantId, TenantQuotas, TenantUsage};
use std::sync::Arc;
use tokio::sync::RwLock;
use wami_core::arn::{Service, WamiArn};
use wami_core::context::WamiContext;
use wami_core::error::Result;
#[wami_macros::service(store_trait = "crate::store::traits::TenantStore")]
pub struct TenantService<S> {
store: Arc<RwLock<S>>,
}
impl<S: TenantStore> TenantService<S> {
async fn generate_unique_tenant_id(&self, parent_id: Option<&TenantId>) -> Result<TenantId> {
const MAX_RETRIES: usize = 10;
for _ in 0..MAX_RETRIES {
let tenant_id = if let Some(parent) = parent_id {
parent.child()
} else {
TenantId::root()
};
let exists = self.read_store().await.get_tenant(&tenant_id).await?;
if exists.is_none() {
return Ok(tenant_id);
}
}
Err(crate::error::AmiError::ResourceLimitExceeded {
resource_type: "tenant_id_generation".to_string(),
limit: MAX_RETRIES,
})
}
async fn validate_name_uniqueness(
&self,
name: &str,
parent_id: Option<&TenantId>,
) -> Result<()> {
let children = if let Some(parent) = parent_id {
self.list_child_tenants(parent).await?
} else {
self.list_tenants()
.await?
.into_iter()
.filter(|t| t.parent_id.is_none())
.collect()
};
if children.iter().any(|t| t.name == name) {
return Err(crate::error::AmiError::ResourceExists {
resource: format!(
"Tenant with name '{}' already exists{}",
name,
parent_id
.map(|p| format!(" in parent {}", p.as_str()))
.unwrap_or_else(|| " at root level".to_string())
),
});
}
Ok(())
}
pub async fn find_tenant_by_name(
&self,
name: &str,
parent_id: Option<&TenantId>,
) -> Result<Option<Tenant>> {
let candidates = if let Some(parent) = parent_id {
self.list_child_tenants(parent).await?
} else {
self.list_tenants()
.await?
.into_iter()
.filter(|t| t.parent_id.is_none())
.collect()
};
Ok(candidates.into_iter().find(|t| t.name == name))
}
pub async fn create_tenant(
&self,
context: &WamiContext,
name: String,
organization: Option<String>,
parent_id: Option<TenantId>,
) -> Result<Tenant> {
tenant_operations::validate_name(&name)?;
self.validate_name_uniqueness(&name, parent_id.as_ref())
.await?;
let tenant_id = self.generate_unique_tenant_id(parent_id.as_ref()).await?;
let mut tenant = tenant_operations::build_tenant(tenant_id, name, organization, parent_id);
tenant.arn = WamiArn::builder()
.service(Service::Iam)
.tenant_path(context.tenant_path().clone())
.wami_instance(context.instance_id())
.resource("tenant", tenant.id.as_str())
.build()?
.to_string();
self.write_store().await.create_tenant(tenant).await
}
pub async fn get_tenant(&self, tenant_id: &TenantId) -> Result<Option<Tenant>> {
self.read_store().await.get_tenant(tenant_id).await
}
pub async fn update_tenant(&self, tenant: Tenant) -> Result<Tenant> {
self.write_store().await.update_tenant(tenant).await
}
pub async fn delete_tenant(&self, tenant_id: &TenantId) -> Result<()> {
self.write_store().await.delete_tenant(tenant_id).await
}
pub async fn list_tenants(&self) -> Result<Vec<Tenant>> {
self.read_store().await.list_tenants().await
}
pub async fn list_child_tenants(&self, parent_id: &TenantId) -> Result<Vec<Tenant>> {
self.read_store().await.list_child_tenants(parent_id).await
}
pub async fn get_ancestors(&self, tenant_id: &TenantId) -> Result<Vec<Tenant>> {
self.read_store().await.get_ancestors(tenant_id).await
}
pub async fn get_descendants(&self, tenant_id: &TenantId) -> Result<Vec<TenantId>> {
self.read_store().await.get_descendants(tenant_id).await
}
pub async fn get_effective_quotas(&self, tenant_id: &TenantId) -> Result<TenantQuotas> {
self.read_store()
.await
.get_effective_quotas(tenant_id)
.await
}
pub async fn get_tenant_usage(&self, tenant_id: &TenantId) -> Result<TenantUsage> {
self.read_store().await.get_tenant_usage(tenant_id).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::memory::InMemoryWamiStore;
fn setup_service() -> TenantService<InMemoryWamiStore> {
let store = Arc::new(RwLock::new(InMemoryWamiStore::default()));
TenantService::new(store)
}
fn test_context() -> wami_core::context::WamiContext {
use wami_core::arn::{TenantPath, WamiArn};
let arn: WamiArn = "arn:wami:iam:12345678:wami:123456789012:user/test"
.parse()
.unwrap();
wami_core::context::WamiContext::builder()
.instance_id("123456789012")
.tenant_path(TenantPath::single(12345678))
.caller_arn(arn)
.is_root(false)
.build()
.unwrap()
}
#[tokio::test]
async fn test_create_and_get_tenant() {
let service = setup_service();
let context = test_context();
let tenant = service
.create_tenant(
&context,
"acme-corp".to_string(),
Some("ACME Inc".to_string()),
None,
)
.await
.unwrap();
assert_eq!(tenant.name, "acme-corp");
assert_eq!(tenant.organization, Some("ACME Inc".to_string()));
assert!(!tenant.arn.is_empty());
let retrieved = service.get_tenant(&tenant.id).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().name, "acme-corp");
}
#[tokio::test]
async fn test_list_tenants() {
let service = setup_service();
let context = test_context();
service
.create_tenant(&context, "tenant1".to_string(), None, None)
.await
.unwrap();
service
.create_tenant(&context, "tenant2".to_string(), None, None)
.await
.unwrap();
let tenants = service.list_tenants().await.unwrap();
assert_eq!(tenants.len(), 2);
}
#[tokio::test]
async fn test_update_tenant() {
let service = setup_service();
let context = test_context();
let mut tenant = service
.create_tenant(&context, "test-tenant".to_string(), None, None)
.await
.unwrap();
tenant.organization = Some("Updated Org".to_string());
let updated = service.update_tenant(tenant).await.unwrap();
assert_eq!(updated.organization, Some("Updated Org".to_string()));
}
#[tokio::test]
async fn test_delete_tenant() {
let service = setup_service();
let context = test_context();
let tenant = service
.create_tenant(&context, "delete-me".to_string(), None, None)
.await
.unwrap();
service.delete_tenant(&tenant.id).await.unwrap();
let retrieved = service.get_tenant(&tenant.id).await.unwrap();
assert!(retrieved.is_none());
}
#[tokio::test]
async fn test_hierarchical_tenants() {
let service = setup_service();
let context = test_context();
let parent = service
.create_tenant(&context, "parent".to_string(), None, None)
.await
.unwrap();
let child = service
.create_tenant(&context, "child".to_string(), None, Some(parent.id.clone()))
.await
.unwrap();
let children = service.list_child_tenants(&parent.id).await.unwrap();
assert_eq!(children.len(), 1);
assert_eq!(children[0].name, "child");
let ancestors = service.get_ancestors(&child.id).await.unwrap();
assert_eq!(ancestors.len(), 1);
assert_eq!(ancestors[0].name, "parent");
}
#[tokio::test]
async fn test_get_descendants() {
let service = setup_service();
let context = test_context();
let root = service
.create_tenant(&context, "root".to_string(), None, None)
.await
.unwrap();
let child1 = service
.create_tenant(&context, "child1".to_string(), None, Some(root.id.clone()))
.await
.unwrap();
service
.create_tenant(&context, "child2".to_string(), None, Some(root.id.clone()))
.await
.unwrap();
service
.create_tenant(
&context,
"grandchild".to_string(),
None,
Some(child1.id.clone()),
)
.await
.unwrap();
let descendants = service.get_descendants(&root.id).await.unwrap();
assert_eq!(descendants.len(), 3); }
#[tokio::test]
async fn test_validate_invalid_name() {
let service = setup_service();
let context = test_context();
let result = service
.create_tenant(&context, "".to_string(), None, None)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_with_provider() {
let service = setup_service();
let context = test_context();
let tenant = service
.create_tenant(&context, "gcp-tenant".to_string(), None, None)
.await
.unwrap();
assert!(tenant.arn.contains("wami"));
}
#[tokio::test]
async fn test_find_tenant_by_name_root_level() {
let service = setup_service();
let context = test_context();
let tenant1 = service
.create_tenant(&context, "acme".to_string(), None, None)
.await
.unwrap();
service
.create_tenant(&context, "globex".to_string(), None, None)
.await
.unwrap();
let found = service.find_tenant_by_name("acme", None).await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().id, tenant1.id);
let not_found = service
.find_tenant_by_name("nonexistent", None)
.await
.unwrap();
assert!(not_found.is_none());
}
#[tokio::test]
async fn test_find_tenant_by_name_with_parent() {
let service = setup_service();
let context = test_context();
let parent = service
.create_tenant(&context, "parent".to_string(), None, None)
.await
.unwrap();
let child1 = service
.create_tenant(
&context,
"engineering".to_string(),
None,
Some(parent.id.clone()),
)
.await
.unwrap();
let parent2 = service
.create_tenant(&context, "parent2".to_string(), None, None)
.await
.unwrap();
service
.create_tenant(
&context,
"engineering".to_string(),
None,
Some(parent2.id.clone()),
)
.await
.unwrap();
let found = service
.find_tenant_by_name("engineering", Some(&parent.id))
.await
.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().id, child1.id);
}
#[tokio::test]
async fn test_name_uniqueness_validation() {
let service = setup_service();
let context = test_context();
service
.create_tenant(&context, "test".to_string(), None, None)
.await
.unwrap();
let result = service
.create_tenant(&context, "test".to_string(), None, None)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
}
#[tokio::test]
async fn test_name_uniqueness_within_parent() {
let service = setup_service();
let context = test_context();
let parent = service
.create_tenant(&context, "parent".to_string(), None, None)
.await
.unwrap();
service
.create_tenant(&context, "eng".to_string(), None, Some(parent.id.clone()))
.await
.unwrap();
let result = service
.create_tenant(&context, "eng".to_string(), None, Some(parent.id.clone()))
.await;
assert!(result.is_err());
let parent2 = service
.create_tenant(&context, "parent2".to_string(), None, None)
.await
.unwrap();
let result2 = service
.create_tenant(&context, "eng".to_string(), None, Some(parent2.id.clone()))
.await;
assert!(result2.is_ok());
}
#[tokio::test]
async fn test_numeric_tenant_id_format() {
let service = setup_service();
let context = test_context();
let tenant = service
.create_tenant(&context, "test".to_string(), None, None)
.await
.unwrap();
let id_str = tenant.id.as_str();
let parts: Vec<&str> = id_str.split('/').collect();
for part in parts {
assert!(
part.parse::<u64>().is_ok(),
"Invalid numeric segment: {}",
part
);
}
}
#[tokio::test]
async fn test_tenant_id_global_uniqueness() {
let service = setup_service();
let context = test_context();
let tenant1 = service
.create_tenant(&context, "tenant1".to_string(), None, None)
.await
.unwrap();
let tenant2 = service
.create_tenant(&context, "tenant2".to_string(), None, None)
.await
.unwrap();
assert_ne!(tenant1.id, tenant2.id);
}
#[tokio::test]
async fn test_effective_quotas_and_usage() {
let service = setup_service();
let context = test_context();
let tenant = service
.create_tenant(&context, "acme".to_string(), None, None)
.await
.unwrap();
let quotas = service.get_effective_quotas(&tenant.id).await.unwrap();
assert_eq!(quotas.max_users, tenant.quotas.max_users);
assert_eq!(quotas.max_roles, tenant.quotas.max_roles);
let usage = service.get_tenant_usage(&tenant.id).await.unwrap();
assert_eq!(usage.tenant_id, tenant.id);
assert_eq!(usage.current_users, 0);
}
#[tokio::test]
async fn test_quotas_and_usage_reject_unknown_tenant() {
let service = setup_service();
let missing = TenantId::from_string("99999999").unwrap();
assert!(service.get_effective_quotas(&missing).await.is_err());
assert!(service.get_tenant_usage(&missing).await.is_err());
}
}