use tonic::Status;
use crate::ir::{
ComparisonOp, LogicalFilter, LogicalPagination, LogicalProjection, LogicalRead, LogicalRecord,
LogicalValue,
};
use crate::proto::udb::core::tenant::services::v1 as tenant_pb;
use crate::runtime::service::method_security::VerifiedClaimContext;
use super::config::{TENANT_CONFIG_MSG, TENANT_MSG};
pub(crate) fn logical_string(value: impl Into<String>) -> LogicalValue {
LogicalValue::String(value.into())
}
#[derive(Debug)]
pub(crate) struct ListTenantsScope {
pub(crate) admit_tenant: String,
pub(crate) subtree_of: Option<String>,
}
pub(crate) fn list_tenants_scope(
claim_present: bool,
claim: &VerifiedClaimContext,
) -> Result<ListTenantsScope, Status> {
if !claim_present {
return Ok(ListTenantsScope {
admit_tenant: String::new(),
subtree_of: None,
});
}
let claim_tenant = claim.tenant_id.trim().to_string();
if claim.is_cross_tenant_admin() {
return Ok(ListTenantsScope {
admit_tenant: claim_tenant,
subtree_of: None,
});
}
if claim_tenant.is_empty() {
return Err(crate::runtime::executor_utils::policy_status_with_code(
tonic::Code::PermissionDenied,
"native_request_scope",
"tenant_list_scope_required",
"ListTenants requires a tenant-scoped bearer token or a cross-tenant admin role",
));
}
Ok(ListTenantsScope {
admit_tenant: claim_tenant.clone(),
subtree_of: Some(claim_tenant),
})
}
pub(crate) fn list_tenants_subtree_predicate(
tenant_col: &str,
parent_col: &str,
bind: &str,
) -> String {
format!("({tenant_col}::text = {bind} OR {parent_col}::text = {bind})")
}
pub(crate) fn active_tenant_filter(tenant_id: &str) -> LogicalFilter {
LogicalFilter::And(vec![
LogicalFilter::Comparison {
field: "tenant_id".to_string(),
op: ComparisonOp::Eq,
value: logical_string(tenant_id),
},
LogicalFilter::IsNull("deleted_at".to_string()),
])
}
pub(crate) fn tenant_projection() -> LogicalProjection {
LogicalProjection::fields([
"tenant_id".to_string(),
"code".to_string(),
"name".to_string(),
"type".to_string(),
"status".to_string(),
"parent_tenant_id".to_string(),
"config".to_string(),
"branding".to_string(),
"deleted_by".to_string(),
])
}
pub(crate) fn tenant_read_by_id(tenant_id: &str) -> LogicalRead {
LogicalRead {
message_type: TENANT_MSG.to_string(),
filter: Some(active_tenant_filter(tenant_id)),
projection: Some(tenant_projection()),
sort: Vec::new(),
include: Vec::new(),
pagination: Some(LogicalPagination::limit(1)),
}
}
pub(crate) fn tenant_config_filter(tenant_id: &str, config_key: Option<&str>) -> LogicalFilter {
let mut filters = vec![LogicalFilter::Comparison {
field: "tenant_id".to_string(),
op: ComparisonOp::Eq,
value: logical_string(tenant_id),
}];
if let Some(config_key) = config_key.filter(|value| !value.trim().is_empty()) {
filters.push(LogicalFilter::Comparison {
field: "config_key".to_string(),
op: ComparisonOp::Eq,
value: logical_string(config_key.to_string()),
});
}
LogicalFilter::And(filters)
}
pub(crate) fn tenant_config_projection() -> LogicalProjection {
LogicalProjection::fields([
"id".to_string(),
"tenant_id".to_string(),
"config_key".to_string(),
"config_value".to_string(),
"type".to_string(),
"description".to_string(),
])
}
pub(crate) fn tenant_config_read(
tenant_id: &str,
config_key: Option<&str>,
limit: u32,
) -> LogicalRead {
LogicalRead {
message_type: TENANT_CONFIG_MSG.to_string(),
filter: Some(tenant_config_filter(tenant_id, config_key)),
projection: Some(tenant_config_projection()),
sort: Vec::new(),
include: Vec::new(),
pagination: Some(LogicalPagination::limit(limit)),
}
}
pub(crate) fn tenant_config_record(
id: String,
tenant_id: &str,
req: &tenant_pb::UpdateTenantConfigRequest,
kind: String,
) -> LogicalRecord {
let mut record = LogicalRecord::new();
record.insert("id".to_string(), logical_string(id));
record.insert(
"tenant_id".to_string(),
logical_string(tenant_id.to_string()),
);
record.insert(
"config_key".to_string(),
logical_string(req.config_key.trim().to_string()),
);
record.insert(
"config_value".to_string(),
logical_string(req.config_value.clone()),
);
record.insert("type".to_string(), logical_string(kind));
record.insert("description".to_string(), logical_string(String::new()));
record
}