use nodedb_sql::parser::preprocess::lex::find_ascii_case_insensitive;
use nodedb_types::DatabaseId;
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::security::permission_tree::types::PermissionTreeDef;
use crate::control::state::SharedState;
use super::super::result::{DdlError, DdlResult};
fn err(sqlstate: &str, message: impl Into<String>) -> DdlError {
DdlError {
sqlstate: sqlstate.to_string(),
message: message.into(),
}
}
fn status(command: impl Into<String>) -> Vec<DdlResult> {
vec![DdlResult::Status {
command: command.into(),
rows_affected: None,
}]
}
pub async fn set_permission_tree(
state: &SharedState,
identity: &AuthenticatedIdentity,
sql: &str,
) -> Result<Vec<DdlResult>, DdlError> {
let start = "ALTER COLLECTION ".len();
let end = find_ascii_case_insensitive(sql, " SET PERMISSION_TREE")
.ok_or_else(|| err("42601", "expected SET PERMISSION_TREE"))?;
let collection = sql[start..end].trim().to_lowercase();
let eq_pos = sql[end..]
.find('=')
.ok_or_else(|| err("42601", "expected '=' after SET PERMISSION_TREE"))?;
let json_part = sql[end + eq_pos + 1..].trim();
let json_str = if json_part.starts_with('\'') && json_part.ends_with('\'') {
&json_part[1..json_part.len() - 1]
} else {
json_part
};
let def: PermissionTreeDef = sonic_rs::from_str(json_str)
.map_err(|e| err("42601", format!("invalid PERMISSION_TREE JSON: {e}")))?;
def.validate()
.map_err(|e| err("42601", format!("invalid PERMISSION_TREE: {e}")))?;
let tenant_id = identity.tenant_id;
let catalog = state.credentials.catalog();
let mut coll = catalog
.get_collection(DatabaseId::DEFAULT, tenant_id.as_u64(), &collection)
.map_err(|e| err("XX000", e.to_string()))?
.ok_or_else(|| err("42P01", format!("collection '{collection}' does not exist")))?;
if !coll.is_active {
return Err(err(
"42P01",
format!("collection '{collection}' is not active"),
));
}
let def_json = sonic_rs::to_string(&def)
.map_err(|e| err("XX000", format!("serialize PERMISSION_TREE: {e}")))?;
coll.permission_tree_def = Some(def_json);
catalog
.put_collection(DatabaseId::DEFAULT, &coll)
.map_err(|e| err("XX000", e.to_string()))?;
state
.permission_cache
.write()
.await
.register_tree_def(tenant_id.as_u64(), &collection, def);
state
.audit
.lock()
.unwrap_or_else(|p| p.into_inner())
.record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(tenant_id),
&identity.username,
&format!("SET PERMISSION_TREE on '{collection}'"),
);
Ok(status("ALTER COLLECTION"))
}
pub async fn drop_permission_tree(
state: &SharedState,
identity: &AuthenticatedIdentity,
sql: &str,
) -> Result<Vec<DdlResult>, DdlError> {
let start = "ALTER COLLECTION ".len();
let end = find_ascii_case_insensitive(sql, " DROP PERMISSION_TREE")
.ok_or_else(|| err("42601", "expected DROP PERMISSION_TREE"))?;
let collection = sql[start..end].trim().to_lowercase();
let tenant_id = identity.tenant_id;
let catalog = state.credentials.catalog();
let mut coll = catalog
.get_collection(DatabaseId::DEFAULT, tenant_id.as_u64(), &collection)
.map_err(|e| err("XX000", e.to_string()))?
.ok_or_else(|| err("42P01", format!("collection '{collection}' does not exist")))?;
coll.permission_tree_def = None;
catalog
.put_collection(DatabaseId::DEFAULT, &coll)
.map_err(|e| err("XX000", e.to_string()))?;
state
.permission_cache
.write()
.await
.unregister_tree_def(tenant_id.as_u64(), &collection);
state
.audit
.lock()
.unwrap_or_else(|p| p.into_inner())
.record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(tenant_id),
&identity.username,
&format!("DROP PERMISSION_TREE on '{collection}'"),
);
Ok(status("ALTER COLLECTION"))
}