use serde_json::{Map, Value as JsonValue};
use crate::control::security::auth_context::AuthStatus;
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: impl Into<String>) -> Vec<DdlResult> {
vec![DdlResult::Status {
command: command.into(),
rows_affected: None,
}]
}
pub fn handle_auth_user(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if !identity.is_superuser {
return Err(err("42501", "permission denied: requires superuser"));
}
let upper0 = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
match upper0.as_str() {
"DEACTIVATE" => deactivate_auth_user(state, identity, parts),
"ALTER" => alter_auth_user_status(state, identity, parts),
_ => Err(err(
"42601",
"expected ALTER AUTH USER or DEACTIVATE AUTH USER",
)),
}
}
fn deactivate_auth_user(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if parts.len() < 4 {
return Err(err("42601", "syntax: DEACTIVATE AUTH USER '<user_id>'"));
}
let user_id = parts[3].trim_matches('\'');
let found = state
.auth_users
.deactivate(user_id)
.map_err(|e| err("XX000", e.to_string()))?;
if !found {
return Err(err("42704", format!("auth user '{user_id}' not found")));
}
state.audit_record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
&identity.username,
&format!("deactivated auth user '{user_id}'"),
);
Ok(status("DEACTIVATE"))
}
fn alter_auth_user_status(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if parts.len() < 7 {
return Err(err(
"42601",
"syntax: ALTER AUTH USER '<user_id>' SET STATUS <active|suspended|banned|restricted|read_only>",
));
}
let user_id = parts[3].trim_matches('\'');
let status_str = parts[6].to_lowercase();
let status_val: AuthStatus = status_str.parse().map_err(|e: String| err("42601", e))?;
let found = state
.auth_users
.set_status(user_id, status_val)
.map_err(|e| err("XX000", e.to_string()))?;
if !found {
return Err(err("42704", format!("auth user '{user_id}' not found")));
}
state.audit_record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
&identity.username,
&format!("auth user '{user_id}' status set to {status_val}"),
);
Ok(status("ALTER AUTH USER"))
}
pub fn purge_auth_users(
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() < 6 {
return Err(err(
"42601",
"syntax: PURGE AUTH USERS INACTIVE FOR <duration> (e.g., 90d, 24h)",
));
}
let duration_str = parts[5];
let threshold_secs = parse_duration_secs(duration_str).ok_or_else(|| {
err(
"42601",
format!("invalid duration: '{duration_str}'. Use 90d or 24h"),
)
})?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let cutoff = now.saturating_sub(threshold_secs);
let purged = state
.auth_users
.purge_inactive(cutoff)
.map_err(|e| err("XX000", e.to_string()))?;
state.audit_record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
&identity.username,
&format!("purged {purged} inactive auth users (older than {duration_str})"),
);
Ok(status(format!("PURGE {purged}")))
}
pub fn show_auth_users(
state: &SharedState,
identity: &AuthenticatedIdentity,
_parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if !identity.is_superuser {
return Err(err("42501", "permission denied: requires superuser"));
}
let users = state.auth_users.list(false);
let columns = vec![
"id".to_string(),
"username".to_string(),
"email".to_string(),
"tenant_id".to_string(),
"provider".to_string(),
"status".to_string(),
"is_active".to_string(),
"last_seen".to_string(),
];
let column_types = ShapedRows::text_types(columns.len());
let rows: Vec<_> = users
.iter()
.map(|u| {
let mut row = Map::new();
row.insert("id".to_string(), JsonValue::String(u.id.clone()));
row.insert(
"username".to_string(),
JsonValue::String(u.username.clone()),
);
row.insert("email".to_string(), JsonValue::String(u.email.clone()));
row.insert(
"tenant_id".to_string(),
JsonValue::String(u.tenant_id.to_string()),
);
row.insert(
"provider".to_string(),
JsonValue::String(u.provider.clone()),
);
row.insert(
"status".to_string(),
JsonValue::String(u.status.to_string()),
);
row.insert(
"is_active".to_string(),
JsonValue::String(u.is_active.to_string()),
);
row.insert(
"last_seen".to_string(),
JsonValue::String(u.last_seen.to_string()),
);
row
})
.collect();
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows,
notice: None,
})])
}
pub fn parse_duration_public(s: &str) -> Option<u64> {
parse_duration_secs(s)
}
fn parse_duration_secs(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(n) = s.strip_suffix('d') {
let n: u64 = n.parse().ok()?;
Some(n * 86_400)
} else if let Some(n) = s.strip_suffix('h') {
let n: u64 = n.parse().ok()?;
Some(n * 3_600)
} else if let Some(n) = s.strip_suffix('s') {
n.parse().ok()
} else {
s.parse().ok()
}
}