use crate::control::catalog_entry::CatalogEntry;
use crate::control::metadata_proposer::propose_catalog_entry;
use crate::control::security::catalog::auth_types::object_type;
use crate::control::security::catalog::{StoredOwner, SystemCatalog};
use crate::control::state::SharedState;
use crate::types::TenantId;
use super::super::super::result::DdlError;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum OwnerKind {
Collection,
Function,
Procedure,
Trigger,
MaterializedView,
Sequence,
Schedule,
ChangeStream,
ContinuousAggregate,
Index,
}
impl OwnerKind {
pub(super) fn from_object_type(s: &str) -> Option<Self> {
Some(match s {
object_type::COLLECTION => Self::Collection,
object_type::FUNCTION => Self::Function,
object_type::PROCEDURE => Self::Procedure,
object_type::TRIGGER => Self::Trigger,
object_type::MATERIALIZED_VIEW => Self::MaterializedView,
object_type::SEQUENCE => Self::Sequence,
object_type::SCHEDULE => Self::Schedule,
object_type::CHANGE_STREAM => Self::ChangeStream,
object_type::CONTINUOUS_AGGREGATE => Self::ContinuousAggregate,
object_type::INDEX => Self::Index,
_ => return None,
})
}
fn as_object_type(&self) -> &'static str {
match self {
Self::Collection => object_type::COLLECTION,
Self::Function => object_type::FUNCTION,
Self::Procedure => object_type::PROCEDURE,
Self::Trigger => object_type::TRIGGER,
Self::MaterializedView => object_type::MATERIALIZED_VIEW,
Self::Sequence => object_type::SEQUENCE,
Self::Schedule => object_type::SCHEDULE,
Self::ChangeStream => object_type::CHANGE_STREAM,
Self::ContinuousAggregate => object_type::CONTINUOUS_AGGREGATE,
Self::Index => object_type::INDEX,
}
}
}
pub(super) fn reassign_owned_and_sweep_grants(
state: &SharedState,
username: &str,
user_tenant: TenantId,
) -> Result<Option<String>, DdlError> {
let catalog = state.credentials.catalog();
let owned = catalog
.owners_for_user(username, user_tenant.as_u64())
.map_err(|e| ddl_err(format!("load owner rows: {e}")))?;
if owned.is_empty() {
sweep_grants(state, catalog, username)?;
return Ok(None);
}
let admin_name = catalog
.resolve_ownership_fallback(user_tenant.as_u64(), username)
.map_err(|e| ddl_err(format!("resolve ownership fallback: {e}")))?
.ok_or_else(|| DdlError {
sqlstate: "55000".to_string(),
message: format!(
"cannot drop user '{username}': tenant {} has no active administrative \
principal available for ownership reassignment",
user_tenant.as_u64()
),
})?;
for owner in &owned {
let kind = OwnerKind::from_object_type(&owner.object_type).ok_or_else(|| {
ddl_err(format!(
"cannot reassign object of unknown owner type '{}' ('{}') owned by \
'{username}' — refusing to drop user to avoid a dangling owner reference",
owner.object_type, owner.object_name
))
})?;
reassign_one(
state,
catalog,
kind,
owner.database_id,
user_tenant,
&owner.object_name,
&admin_name,
)?;
}
sweep_grants(state, catalog, username)?;
Ok(Some(admin_name))
}
fn reassign_one(
state: &SharedState,
catalog: &SystemCatalog,
kind: OwnerKind,
database_id: u64,
tenant: TenantId,
name: &str,
admin_name: &str,
) -> Result<(), DdlError> {
let tenant_id = tenant.as_u64();
let object_type = kind.as_object_type();
match kind {
OwnerKind::Collection => {
let database_id = nodedb_types::DatabaseId::new(database_id);
let mut stored = catalog
.get_collection(database_id, tenant_id, name)
.map_err(|e| ddl_err(format!("get collection '{name}': {e}")))?
.ok_or_else(|| missing(object_type, name))?;
stored.owner = admin_name.to_string();
let entry = CatalogEntry::PutCollection(Box::new(stored.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_collection(database_id, &stored)
.map_err(|e| ddl_err(format!("put collection '{name}': {e}")))?;
persist_owner_local_in_database(
state,
catalog,
object_type,
database_id.as_u64(),
tenant_id,
name,
admin_name,
)?;
}
}
OwnerKind::Function => {
let mut s = catalog
.get_function(tenant_id, name)
.map_err(|e| ddl_err(format!("get function '{name}': {e}")))?
.ok_or_else(|| missing(object_type, name))?;
s.owner = admin_name.to_string();
let entry = CatalogEntry::PutFunction(Box::new(s.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_function(&s)
.map_err(|e| ddl_err(format!("put function '{name}': {e}")))?;
persist_owner_local(state, catalog, object_type, tenant_id, name, admin_name)?;
}
}
OwnerKind::Procedure => {
let mut s = catalog
.get_procedure(tenant_id, name)
.map_err(|e| ddl_err(format!("get procedure '{name}': {e}")))?
.ok_or_else(|| missing(object_type, name))?;
s.owner = admin_name.to_string();
let entry = CatalogEntry::PutProcedure(Box::new(s.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_procedure(&s)
.map_err(|e| ddl_err(format!("put procedure '{name}': {e}")))?;
persist_owner_local(state, catalog, object_type, tenant_id, name, admin_name)?;
}
}
OwnerKind::Trigger => {
let mut s = catalog
.get_trigger(tenant_id, name)
.map_err(|e| ddl_err(format!("get trigger '{name}': {e}")))?
.ok_or_else(|| missing(object_type, name))?;
s.owner = admin_name.to_string();
let entry = CatalogEntry::PutTrigger(Box::new(s.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_trigger(&s)
.map_err(|e| ddl_err(format!("put trigger '{name}': {e}")))?;
persist_owner_local(state, catalog, object_type, tenant_id, name, admin_name)?;
}
}
OwnerKind::MaterializedView => {
let mut s = catalog
.get_materialized_view(tenant_id, name)
.map_err(|e| ddl_err(format!("get materialized_view '{name}': {e}")))?
.ok_or_else(|| missing(object_type, name))?;
s.owner = admin_name.to_string();
let entry = CatalogEntry::PutMaterializedView(Box::new(s.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_materialized_view(&s)
.map_err(|e| ddl_err(format!("put materialized_view '{name}': {e}")))?;
persist_owner_local(state, catalog, object_type, tenant_id, name, admin_name)?;
}
}
OwnerKind::Sequence => {
let mut s = catalog
.get_sequence(tenant_id, name)
.map_err(|e| ddl_err(format!("get sequence '{name}': {e}")))?
.ok_or_else(|| missing(object_type, name))?;
s.owner = admin_name.to_string();
let entry = CatalogEntry::PutSequence(Box::new(s.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_sequence(&s)
.map_err(|e| ddl_err(format!("put sequence '{name}': {e}")))?;
persist_owner_local(state, catalog, object_type, tenant_id, name, admin_name)?;
}
}
OwnerKind::Schedule => {
let mut s = catalog
.load_all_schedules()
.map_err(|e| ddl_err(format!("load schedules: {e}")))?
.into_iter()
.find(|d| d.tenant_id == tenant_id && d.name == name)
.ok_or_else(|| missing(object_type, name))?;
s.owner = admin_name.to_string();
let entry = CatalogEntry::PutSchedule(Box::new(s.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_schedule(&s)
.map_err(|e| ddl_err(format!("put schedule '{name}': {e}")))?;
persist_owner_local(state, catalog, object_type, tenant_id, name, admin_name)?;
}
}
OwnerKind::ChangeStream => {
let mut s = catalog
.get_change_stream(tenant_id, name)
.map_err(|e| ddl_err(format!("get change_stream '{name}': {e}")))?
.ok_or_else(|| missing(object_type, name))?;
s.owner = admin_name.to_string();
let entry = CatalogEntry::PutChangeStream(Box::new(s.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_change_stream(&s)
.map_err(|e| ddl_err(format!("put change_stream '{name}': {e}")))?;
persist_owner_local(state, catalog, object_type, tenant_id, name, admin_name)?;
}
}
OwnerKind::ContinuousAggregate => {
let mut stored = catalog
.get_continuous_aggregate(database_id, tenant_id, name)
.map_err(|e| ddl_err(format!("get continuous_aggregate '{name}': {e}")))?
.ok_or_else(|| missing(object_type, name))?;
stored.owner = admin_name.to_string();
let entry = CatalogEntry::PutContinuousAggregate(Box::new(stored.clone()));
if propose(state, &entry)? == 0 {
catalog
.put_continuous_aggregate(&stored)
.map_err(|e| ddl_err(format!("put continuous_aggregate '{name}': {e}")))?;
persist_owner_local_in_database(
state,
catalog,
object_type,
database_id,
tenant_id,
name,
admin_name,
)?;
}
}
OwnerKind::Index => {
let stored = StoredOwner {
database_id,
object_type: object_type.to_string(),
object_name: name.to_string(),
tenant_id,
owner_username: admin_name.to_string(),
};
let entry = CatalogEntry::PutOwner(Box::new(stored.clone()));
if propose(state, &entry)? == 0 {
persist_owner_local_in_database(
state,
catalog,
object_type,
database_id,
tenant_id,
name,
admin_name,
)?;
}
}
}
Ok(())
}
pub(super) fn sweep_grants(
state: &SharedState,
catalog: &SystemCatalog,
username: &str,
) -> Result<(), DdlError> {
let grantee = format!("user:{username}");
let grants = catalog
.load_all_permissions()
.map_err(|e| ddl_err(format!("load permissions: {e}")))?;
for grant in grants.into_iter().filter(|grant| grant.grantee == grantee) {
let entry = CatalogEntry::DeletePermission {
target: grant.target.clone(),
grantee: grantee.clone(),
permission: grant.permission.clone(),
};
if propose(state, &entry)? == 0 {
catalog
.delete_permission(&grant.target, &grantee, &grant.permission)
.map_err(|e| ddl_err(format!("delete permission on '{}': {e}", grant.target)))?;
state
.permissions
.install_replicated_revoke(&grant.target, &grantee, &grant.permission);
}
}
Ok(())
}
fn persist_owner_local(
state: &SharedState,
catalog: &SystemCatalog,
object_type: &str,
tenant_id: u64,
name: &str,
admin_name: &str,
) -> Result<(), DdlError> {
persist_owner_local_in_database(state, catalog, object_type, 0, tenant_id, name, admin_name)
}
fn persist_owner_local_in_database(
state: &SharedState,
catalog: &SystemCatalog,
object_type: &str,
database_id: u64,
tenant_id: u64,
name: &str,
admin_name: &str,
) -> Result<(), DdlError> {
catalog
.rewrite_object_owner(object_type, database_id, tenant_id, name, admin_name)
.map_err(|e| ddl_err(format!("rewrite owner for {object_type} '{name}': {e}")))?;
state.permissions.install_replicated_owner(&StoredOwner {
database_id,
object_type: object_type.to_string(),
object_name: name.to_string(),
tenant_id,
owner_username: admin_name.to_string(),
});
Ok(())
}
pub(super) fn propose(state: &SharedState, entry: &CatalogEntry) -> Result<u64, DdlError> {
propose_catalog_entry(state, entry).map_err(|e| ddl_err(format!("metadata propose: {e}")))
}
fn missing(object_type: &str, name: &str) -> DdlError {
ddl_err(format!(
"owned {object_type} '{name}' has an owner row but no primary record — \
cannot reassign; refusing to drop user"
))
}
pub(super) fn ddl_err(message: String) -> DdlError {
DdlError {
sqlstate: "XX000".to_string(),
message,
}
}