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 err(sqlstate: &str, message: impl Into<String>) -> DdlError {
DdlError {
sqlstate: sqlstate.to_string(),
message: message.into(),
}
}
fn status(command: &str) -> Vec<DdlResult> {
vec![DdlResult::Status {
command: command.to_string(),
rows_affected: None,
}]
}
pub fn show_sessions(
state: &SharedState,
identity: &AuthenticatedIdentity,
_parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if !identity.is_superuser {
return Err(err("42501", "permission denied: requires superuser"));
}
let sessions = state.session_registry.list_all();
let columns = vec![
"session_id".to_string(),
"user_id".to_string(),
"db_user".to_string(),
"auth_method".to_string(),
"connected_at".to_string(),
"last_active".to_string(),
"idle_seconds".to_string(),
"client_ip".to_string(),
"protocol".to_string(),
"current_database".to_string(),
"bytes_in".to_string(),
"bytes_out".to_string(),
"current_statement".to_string(),
"token_expires_in_secs".to_string(),
];
let rows: Vec<Map<String, JsonValue>> = sessions
.iter()
.map(|s| {
let mut row = Map::new();
row.insert(
"session_id".to_string(),
JsonValue::String(s.session_id.clone()),
);
row.insert(
"user_id".to_string(),
JsonValue::String(s.user_id.to_string()),
);
row.insert("db_user".to_string(), JsonValue::String(s.db_user.clone()));
row.insert(
"auth_method".to_string(),
JsonValue::String(s.auth_method.clone()),
);
row.insert(
"connected_at".to_string(),
JsonValue::String(s.connected_at.to_string()),
);
row.insert(
"last_active".to_string(),
JsonValue::String(s.last_active.to_string()),
);
row.insert(
"idle_seconds".to_string(),
JsonValue::String(s.idle_seconds.to_string()),
);
row.insert(
"client_ip".to_string(),
JsonValue::String(s.client_ip.clone()),
);
row.insert(
"protocol".to_string(),
JsonValue::String(s.protocol.clone()),
);
row.insert(
"current_database".to_string(),
JsonValue::String(s.current_database.as_u64().to_string()),
);
row.insert(
"bytes_in".to_string(),
JsonValue::String(s.bytes_in.to_string()),
);
row.insert(
"bytes_out".to_string(),
JsonValue::String(s.bytes_out.to_string()),
);
let current_stmt = s
.current_statement_digest
.as_deref()
.unwrap_or("")
.to_string();
row.insert(
"current_statement".to_string(),
JsonValue::String(current_stmt),
);
let token_exp = s
.token_expires_in_seconds
.map(|v| v.to_string())
.unwrap_or_default();
row.insert(
"token_expires_in_secs".to_string(),
JsonValue::String(token_exp),
);
row
})
.collect();
let column_types = ShapedRows::text_types(columns.len());
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows,
notice: None,
})])
}
pub fn kill_session(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if parts.len() < 3 {
return Err(err("42601", "syntax: KILL SESSION '<session_id>'"));
}
let session_id = parts[2].trim_matches('\'');
let target_db = match state.session_registry.lookup_session_database(session_id) {
Some(db) => db,
None => {
return Err(err("42704", format!("session '{session_id}' not found")));
}
};
let authorized = identity.is_superuser
|| identity.has_cluster_admin()
|| identity.is_database_owner(target_db);
if !authorized {
state.audit_record_with_db(
crate::control::security::audit::AuditEvent::PermissionDenied,
Some(identity.tenant_id),
Some(target_db),
&identity.username,
&format!("KILL SESSION '{session_id}'"),
);
return Err(err(
"42501",
"permission denied: KILL SESSION requires superuser, cluster_admin, or database_owner of the session's database",
));
}
match state.session_registry.kill_session_by_id(
session_id,
crate::control::security::sessions::KillReason::AdminKill,
) {
Some(_db) => {
state.audit_record_with_db(
crate::control::security::audit::AuditEvent::SessionRevoked,
Some(identity.tenant_id),
Some(target_db),
&identity.username,
&format!("killed session '{session_id}' by {}", identity.username),
);
Ok(status("KILL SESSION"))
}
None => {
state.audit_record_with_db(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
Some(target_db),
&identity.username,
&format!(
"KILL SESSION '{session_id}' raced — session disconnected before kill applied"
),
);
Err(err(
"42704",
format!("session '{session_id}' disconnected before KILL applied"),
))
}
}
}
pub fn kill_user_sessions(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if !identity.is_superuser {
return Err(err("42501", "permission denied: requires superuser"));
}
if parts.len() < 4 {
return Err(err("42601", "syntax: KILL USER SESSIONS '<auth_user_id>'"));
}
let user_id_str = parts[3].trim_matches('\'');
let user_id: u64 = user_id_str.parse().map_err(|_| {
err(
"22003",
format!("invalid user_id '{user_id_str}': must be numeric"),
)
})?;
let killed = state.session_registry.kill_sessions_for_user(
user_id,
crate::control::security::sessions::KillReason::AdminKill,
);
state.audit_record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
&identity.username,
&format!("killed {killed} sessions for user_id={user_id}"),
);
Ok(status(&format!("KILL {killed}")))
}
pub fn verify_audit_chain(
state: &SharedState,
identity: &AuthenticatedIdentity,
_parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if !identity.is_superuser {
return Err(err("42501", "permission denied: requires superuser"));
}
let audit = state.audit.lock().unwrap_or_else(|p| p.into_inner());
match audit.verify_chain() {
Ok(()) => {
let columns = vec!["status".to_string(), "entries".to_string()];
let mut row = Map::new();
row.insert("status".to_string(), JsonValue::String("VALID".to_string()));
row.insert(
"entries".to_string(),
JsonValue::String(audit.len().to_string()),
);
let column_types = ShapedRows::text_types(columns.len());
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows: vec![row],
notice: None,
})])
}
Err(broken_seq) => Err(err(
"XX001",
format!("audit chain broken at sequence {broken_seq}"),
)),
}
}