use serde_json::{Map, Value as JsonValue};
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::security::scope::grant::ScopeGrantParams;
use crate::control::server::response_shape::types::ShapedRows;
use crate::control::state::SharedState;
use super::super::super::result::{DdlError, DdlResult};
use super::support::{err, status};
pub fn grant_scope(
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: GRANT SCOPE '<scope>' TO <ORG|USER|ROLE> '<id>'",
));
}
let scope_name = parts[2].trim_matches('\'');
let grantee_type = parts[4].to_lowercase();
let grantee_id = parts[5].trim_matches('\'');
if !matches!(grantee_type.as_str(), "org" | "user" | "role" | "team") {
return Err(err(
"42601",
"grantee type must be ORG, USER, ROLE, or TEAM",
));
}
let expires_at = parse_expires(parts);
let grace_period_secs = parse_grace_period(parts);
let on_expire_action = parse_on_expire(parts);
state
.scope_grants
.grant(ScopeGrantParams {
scope_name,
grantee_type: &grantee_type,
grantee_id,
granted_by: &identity.username,
expires_at,
grace_period_secs,
on_expire_action: &on_expire_action,
})
.map_err(|e| err("XX000", e.to_string()))?;
state.audit_record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
&identity.username,
&format!("granted scope '{scope_name}' to {grantee_type} '{grantee_id}'"),
);
Ok(status("GRANT SCOPE"))
}
pub fn revoke_scope(
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: REVOKE SCOPE '<scope>' FROM <ORG|USER|ROLE> '<id>'",
));
}
let scope_name = parts[2].trim_matches('\'');
let grantee_type = parts[4].to_lowercase();
let grantee_id = parts[5].trim_matches('\'');
state
.scope_grants
.revoke(scope_name, &grantee_type, grantee_id)
.map_err(|e| err("XX000", e.to_string()))?;
state.audit_record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
&identity.username,
&format!("revoked scope '{scope_name}' from {grantee_type} '{grantee_id}'"),
);
Ok(status("REVOKE SCOPE"))
}
pub fn renew_scope(
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() < 8 {
return Err(err(
"42601",
"syntax: RENEW SCOPE '<scope>' FOR <ORG|USER> '<id>' EXTEND BY <duration>",
));
}
let scope_name = parts[2].trim_matches('\'');
let grantee_type = parts[4].to_lowercase();
let grantee_id = parts[5].trim_matches('\'');
let duration_str = parts[7];
let extend_secs =
crate::control::server::shared::ddl::neutral::auth_user::parse_duration_public(
duration_str,
)
.ok_or_else(|| err("42601", format!("invalid duration: '{duration_str}'")))?;
let found = state
.scope_grants
.renew(scope_name, &grantee_type, grantee_id, extend_secs)
.map_err(|e| err("XX000", e.to_string()))?;
if !found {
return Err(err("42704", "scope grant not found"));
}
state.audit_record(
crate::control::security::audit::AuditEvent::AdminAction,
Some(identity.tenant_id),
&identity.username,
&format!(
"renewed scope '{scope_name}' for {grantee_type} '{grantee_id}' by {duration_str}"
),
);
Ok(status("RENEW SCOPE"))
}
pub fn show_scope_grants(
state: &SharedState,
_identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
let grants = if let Some(within_idx) = parts.iter().position(|p| p.to_uppercase() == "WITHIN") {
let dur_str = parts.get(within_idx + 1).unwrap_or(&"7d");
let secs =
crate::control::server::shared::ddl::neutral::auth_user::parse_duration_public(dur_str)
.unwrap_or(7 * 86_400);
state.scope_grants.expiring_within(secs)
} else {
state.scope_grants.list(None)
};
let columns = vec![
"scope".to_string(),
"grantee_type".to_string(),
"grantee_id".to_string(),
"status".to_string(),
"expires_at".to_string(),
"granted_by".to_string(),
];
let column_types = ShapedRows::text_types(columns.len());
let rows: Vec<_> = grants
.iter()
.map(|g| {
let mut row = Map::new();
row.insert("scope".to_string(), JsonValue::String(g.scope_name.clone()));
row.insert(
"grantee_type".to_string(),
JsonValue::String(g.grantee_type.clone()),
);
row.insert(
"grantee_id".to_string(),
JsonValue::String(g.grantee_id.clone()),
);
row.insert(
"status".to_string(),
JsonValue::String(g.status().to_string()),
);
row.insert(
"expires_at".to_string(),
JsonValue::String(if g.expires_at == 0 {
"permanent".to_string()
} else {
g.expires_at.to_string()
}),
);
row.insert(
"granted_by".to_string(),
JsonValue::String(g.granted_by.clone()),
);
row
})
.collect();
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows,
notice: None,
})])
}
fn parse_expires(parts: &[&str]) -> u64 {
parts
.iter()
.position(|p| p.to_uppercase() == "EXPIRES")
.and_then(|i| parts.get(i + 1))
.and_then(|s| s.trim_matches('\'').parse::<u64>().ok())
.unwrap_or(0)
}
fn parse_grace_period(parts: &[&str]) -> u64 {
parts
.iter()
.position(|p| p.to_uppercase() == "GRACE")
.and_then(|i| {
if parts.get(i + 1).map(|s| s.to_uppercase()) == Some("PERIOD".into()) {
parts.get(i + 2)
} else {
None
}
})
.and_then(|s| {
crate::control::server::shared::ddl::neutral::auth_user::parse_duration_public(s)
})
.unwrap_or(0)
}
fn parse_on_expire(parts: &[&str]) -> String {
let idx = parts.iter().position(|p| p.to_uppercase() == "EXPIRE");
let Some(i) = idx else {
return String::new();
};
if i == 0 || parts[i - 1].to_uppercase() != "ON" {
return String::new();
}
let action = parts
.get(i + 1)
.map(|s| s.to_uppercase())
.unwrap_or_default();
match action.as_str() {
"GRANT" => {
let scope = parts.get(i + 3).unwrap_or(&"");
format!("grant:{}", scope.trim_matches('\''))
}
"REVOKE" => "revoke_all".into(),
_ => String::new(),
}
}