use nodedb_sql::ddl_ast::AlterRoleOp;
use pgwire::api::results::{Response, Tag};
use pgwire::error::PgWireResult;
use crate::control::security::audit::AuditEvent;
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::state::SharedState;
use super::super::types::{require_tenant_admin, sqlstate_error};
use super::parse_utils::{strip_if_exists, strip_if_not_exists};
pub fn create_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> PgWireResult<Vec<Response>> {
require_tenant_admin(identity, "create roles")?;
let (if_not_exists, parts) = strip_if_not_exists(parts, 2);
if parts.len() < 3 {
return Err(sqlstate_error(
"42601",
"syntax: CREATE ROLE [IF NOT EXISTS] <name> [INHERIT <parent>]",
));
}
let name = parts[2];
if if_not_exists && state.roles.get_role(name).is_some() {
return Ok(vec![Response::Execution(Tag::new("CREATE ROLE"))]);
}
let parent = if parts.len() >= 5 && parts[3].eq_ignore_ascii_case("INHERIT") {
Some(parts[4])
} else {
None
};
let stored = state
.roles
.prepare_role(name, identity.tenant_id, parent)
.map_err(|e| sqlstate_error("42710", &e.to_string()))?;
let entry = crate::control::catalog_entry::CatalogEntry::PutRole(Box::new(stored.clone()));
let log_index = crate::control::metadata_proposer::propose_catalog_entry(state, &entry)
.map_err(|e| sqlstate_error("XX000", &format!("metadata propose: {e}")))?;
if log_index == 0
&& let Some(catalog) = state.credentials.catalog()
{
catalog
.put_role(&stored)
.map_err(|e| sqlstate_error("XX000", &format!("catalog write: {e}")))?;
state.roles.install_replicated_role(&stored);
}
state.audit_record(
AuditEvent::PrivilegeChange,
Some(identity.tenant_id),
&identity.username,
&format!(
"created role '{name}'{}",
parent.map_or(String::new(), |p| format!(" inheriting from '{p}'"))
),
);
Ok(vec![Response::Execution(Tag::new("CREATE ROLE"))])
}
pub fn alter_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> PgWireResult<Vec<Response>> {
require_tenant_admin(identity, "alter roles")?;
if parts.len() < 6 {
return Err(sqlstate_error(
"42601",
"syntax: ALTER ROLE <name> SET INHERIT <parent>",
));
}
let name = parts[2];
if !parts[3].eq_ignore_ascii_case("SET") || !parts[4].eq_ignore_ascii_case("INHERIT") {
return Err(sqlstate_error(
"42601",
"expected SET INHERIT after role name",
));
}
let parent = parts[5];
let old_role = state
.roles
.get_role(name)
.ok_or_else(|| sqlstate_error("42704", &format!("role '{name}' not found")))?;
let parent_is_builtin = matches!(
parent,
"superuser" | "tenant_admin" | "readwrite" | "readonly" | "monitor"
);
if !parent_is_builtin && state.roles.get_role(parent).is_none() {
return Err(sqlstate_error(
"42704",
&format!("parent role '{parent}' does not exist"),
));
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let stored = crate::control::security::catalog::StoredRole {
name: name.to_string(),
tenant_id: old_role.tenant_id.as_u64(),
parent: parent.to_string(),
created_at: now,
};
let entry = crate::control::catalog_entry::CatalogEntry::PutRole(Box::new(stored.clone()));
let log_index = crate::control::metadata_proposer::propose_catalog_entry(state, &entry)
.map_err(|e| sqlstate_error("XX000", &format!("metadata propose: {e}")))?;
if log_index == 0
&& let Some(catalog) = state.credentials.catalog()
{
catalog
.put_role(&stored)
.map_err(|e| sqlstate_error("XX000", &format!("catalog write: {e}")))?;
state.roles.install_replicated_role(&stored);
}
state.audit_record(
AuditEvent::PrivilegeChange,
Some(identity.tenant_id),
&identity.username,
&format!("altered role '{name}': set inherit '{parent}'"),
);
Ok(vec![Response::Execution(Tag::new("ALTER ROLE"))])
}
pub fn drop_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> PgWireResult<Vec<Response>> {
require_tenant_admin(identity, "drop roles")?;
let (if_exists, parts) = strip_if_exists(parts, 2);
if parts.len() < 3 {
return Err(sqlstate_error(
"42601",
"syntax: DROP ROLE [IF EXISTS] <name>",
));
}
let name = parts[2];
let exists_before = state.roles.get_role(name).is_some();
if !exists_before {
if if_exists {
return Ok(vec![Response::Execution(Tag::new("DROP ROLE"))]);
}
return Err(sqlstate_error(
"42704",
&format!("role '{name}' does not exist"),
));
}
let entry = crate::control::catalog_entry::CatalogEntry::DeleteRole {
name: name.to_string(),
};
let log_index = crate::control::metadata_proposer::propose_catalog_entry(state, &entry)
.map_err(|e| sqlstate_error("XX000", &format!("metadata propose: {e}")))?;
let dropped = if log_index == 0 {
let catalog = state.credentials.catalog();
state
.roles
.drop_role(name, catalog.as_ref())
.map_err(|e| sqlstate_error("42704", &e.to_string()))?
} else {
true
};
if dropped {
state.audit_record(
AuditEvent::PrivilegeChange,
Some(identity.tenant_id),
&identity.username,
&format!("dropped role '{name}'"),
);
Ok(vec![Response::Execution(Tag::new("DROP ROLE"))])
} else {
Err(sqlstate_error(
"42704",
&format!("role '{name}' does not exist"),
))
}
}
pub fn alter_role_typed(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_name: &str,
sub_op: &AlterRoleOp,
) -> PgWireResult<Vec<Response>> {
require_tenant_admin(identity, "alter roles")?;
state
.roles
.get_role(role_name)
.ok_or_else(|| sqlstate_error("42704", &format!("role '{role_name}' not found")))?;
match sub_op {
AlterRoleOp::Grant {
permission,
target_type,
target_name,
} => {
super::grant::permission::grant_permission(
state,
identity,
std::slice::from_ref(permission),
target_type,
target_name,
role_name,
)
}
AlterRoleOp::Revoke {
permission,
target_type,
target_name,
} => {
super::grant::permission::revoke_permission(
state,
identity,
std::slice::from_ref(permission),
target_type,
target_name,
role_name,
)
}
AlterRoleOp::SetInherit { parent } => {
set_role_parent(state, role_name, Some(parent))?;
state.audit_record(
AuditEvent::PrivilegeChange,
Some(identity.tenant_id),
&identity.username,
&format!("altered role '{role_name}': set inherit '{parent}'"),
);
Ok(vec![Response::Execution(Tag::new("ALTER ROLE"))])
}
}
}
pub fn set_role_parent(
state: &SharedState,
role_name: &str,
parent: Option<&str>,
) -> PgWireResult<()> {
let old_role = state
.roles
.get_role(role_name)
.ok_or_else(|| sqlstate_error("42704", &format!("role '{role_name}' not found")))?;
if let Some(parent) = parent {
let parent_is_builtin = matches!(
parent,
"superuser" | "tenant_admin" | "readwrite" | "readonly" | "monitor"
);
if !parent_is_builtin && state.roles.get_role(parent).is_none() {
return Err(sqlstate_error(
"42704",
&format!("parent role '{parent}' does not exist"),
));
}
state
.roles
.check_inheritance_cycle(role_name, parent)
.map_err(|e| sqlstate_error("42P16", &e.to_string()))?;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let stored = crate::control::security::catalog::StoredRole {
name: role_name.to_string(),
tenant_id: old_role.tenant_id.as_u64(),
parent: parent.unwrap_or("").to_string(),
created_at: now,
};
let entry = crate::control::catalog_entry::CatalogEntry::PutRole(Box::new(stored.clone()));
let log_index = crate::control::metadata_proposer::propose_catalog_entry(state, &entry)
.map_err(|e| sqlstate_error("XX000", &format!("metadata propose: {e}")))?;
if log_index == 0
&& let Some(catalog) = state.credentials.catalog()
{
catalog
.put_role(&stored)
.map_err(|e| sqlstate_error("XX000", &format!("catalog write: {e}")))?;
state.roles.install_replicated_role(&stored);
}
Ok(())
}