use serde_json::{Map, Value as JsonValue};
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::server::response_shape::types::ShapedRows;
use crate::control::state::SharedState;
use super::super::result::{DdlError, DdlResult};
fn ddl_err(sqlstate: &str, message: impl Into<String>) -> DdlError {
DdlError {
sqlstate: sqlstate.to_string(),
message: message.into(),
}
}
pub fn explain_permission(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if parts.len() < 8 {
return Err(ddl_err(
"42601",
"syntax: EXPLAIN PERMISSION <perm> ON <collection> FOR AUTH USER '<id>'",
));
}
let perm = parts[2];
let collection = parts[4];
let user_id_str = parts.get(7).map(|s| s.trim_matches('\'')).unwrap_or("");
let target_identity = if let Some(user) = state.credentials.get_user(user_id_str) {
{
let is_su = user.is_superuser;
crate::control::security::identity::AuthenticatedIdentity {
user_id: user.user_id,
username: user.username.clone(),
tenant_id: user.tenant_id,
auth_method: crate::control::security::identity::AuthMethod::Trust,
roles: user.roles.clone(),
is_superuser: is_su,
default_database: None,
accessible_databases:
crate::control::security::identity::AuthenticatedIdentity::default_database_set(
is_su,
),
}
}
} else {
identity.clone()
};
let auth_ctx = crate::control::server::session_auth::build_auth_context(&target_identity);
let explanation = crate::control::security::explain::explain_permission(
perm,
collection,
&target_identity,
&auth_ctx,
state,
);
let columns = vec![
"check".to_string(),
"result".to_string(),
"source".to_string(),
];
let mut rows: Vec<Map<String, JsonValue>> = explanation
.steps
.iter()
.map(|s| {
let mut row = Map::new();
row.insert("check".to_string(), JsonValue::String(s.check.clone()));
row.insert("result".to_string(), JsonValue::String(s.result.clone()));
row.insert("source".to_string(), JsonValue::String(s.source.clone()));
row
})
.collect();
let mut row = Map::new();
row.insert("check".to_string(), JsonValue::String("FINAL".to_string()));
row.insert(
"result".to_string(),
JsonValue::String((if explanation.allowed { "ALLOW" } else { "DENY" }).to_string()),
);
row.insert(
"source".to_string(),
JsonValue::String(format!("{} on {}", perm, collection)),
);
rows.push(row);
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types: ShapedRows::text_types(3),
rows,
notice: None,
})])
}
pub fn explain_scope(
state: &SharedState,
_identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if parts.len() < 6 {
return Err(ddl_err(
"42601",
"syntax: EXPLAIN SCOPE FOR AUTH USER '<id>'",
));
}
let user_id = parts.get(5).map(|s| s.trim_matches('\'')).unwrap_or("");
let org_ids = state.orgs.orgs_for_user(user_id);
let effective = state.scope_grants.effective_scopes(user_id, &org_ids);
let columns = vec![
"scope".to_string(),
"source".to_string(),
"resolved_grants".to_string(),
];
let rows: Vec<Map<String, JsonValue>> = effective
.iter()
.map(|scope_name| {
let source = if state
.scope_grants
.scopes_for("user", user_id)
.contains(scope_name)
{
"direct (user)"
} else {
"inherited (org)"
};
let resolved = state.scope_defs.resolve(scope_name);
let grants_str: Vec<String> = resolved
.iter()
.map(|(p, c)| format!("{p} ON {c}"))
.collect();
let mut row = Map::new();
row.insert("scope".to_string(), JsonValue::String(scope_name.clone()));
row.insert("source".to_string(), JsonValue::String(source.to_string()));
row.insert(
"resolved_grants".to_string(),
JsonValue::String(grants_str.join(", ")),
);
row
})
.collect();
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types: ShapedRows::text_types(3),
rows,
notice: None,
})])
}
pub fn assert_visible(
state: &SharedState,
_identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if parts.len() < 4 {
return Err(ddl_err(
"42601",
"syntax: SELECT nodedb_assert_visible('<collection>', '<row_id>', '<user_id>')",
));
}
let collection = parts[1].trim_matches('\'').trim_end_matches(',');
let _row_id = parts[2].trim_matches('\'').trim_end_matches(',');
let user_id = parts[3].trim_matches('\'').trim_end_matches(')');
let target_identity = crate::control::security::identity::AuthenticatedIdentity {
user_id: user_id.parse().unwrap_or(0),
username: user_id.to_string(),
tenant_id: crate::types::TenantId::new(1),
auth_method: crate::control::security::identity::AuthMethod::Trust,
roles: vec![crate::control::security::identity::Role::ReadWrite],
is_superuser: false,
default_database: None,
accessible_databases: crate::control::security::identity::DatabaseSet::Some(
smallvec::smallvec![nodedb_types::id::DatabaseId::DEFAULT],
),
};
let auth_ctx = crate::control::server::session_auth::build_auth_context(&target_identity);
let rls_bytes = state.rls.combined_read_predicate_with_auth(
target_identity.tenant_id.as_u64(),
collection,
&auth_ctx,
);
let visible = rls_bytes.is_some_and(|b| b.is_empty());
let mut row = Map::new();
row.insert(
"visible".to_string(),
JsonValue::String(visible.to_string()),
);
Ok(vec![DdlResult::Rows(ShapedRows {
columns: vec!["visible".to_string()],
column_types: ShapedRows::text_types(1),
rows: vec![row],
notice: None,
})])
}