use pgwire::api::results::{Response, Tag};
use pgwire::error::PgWireResult;
use crate::control::catalog_entry::CatalogEntry;
use crate::control::metadata_proposer::propose_catalog_entry;
use crate::control::security::audit::AuditEvent;
use crate::control::security::identity::{AuthenticatedIdentity, Role};
use crate::control::state::SharedState;
use super::super::super::types::{parse_role, require_tenant_admin, sqlstate_error};
fn current_roles(state: &SharedState, username: &str) -> PgWireResult<Vec<Role>> {
state
.credentials
.get_user(username)
.map(|r| r.roles)
.ok_or_else(|| sqlstate_error("42704", &format!("user '{username}' not found")))
}
fn propose_user_with_roles(
state: &SharedState,
username: &str,
new_roles: Vec<Role>,
invalidation: crate::control::security::buses::SessionInvalidationReason,
) -> PgWireResult<()> {
let stored = state
.credentials
.prepare_user_update(username, None, Some(new_roles))
.map_err(|e| sqlstate_error("42704", &e.to_string()))?;
let entry = CatalogEntry::PutUser(Box::new(stored.clone()));
let log_index = propose_catalog_entry(state, &entry)
.map_err(|e| sqlstate_error("XX000", &format!("metadata propose: {e}")))?;
if log_index == 0 {
if let Some(catalog) = state.credentials.catalog() {
catalog
.put_user(&stored)
.map_err(|e| sqlstate_error("XX000", &format!("catalog write: {e}")))?;
}
state
.credentials
.install_replicated_user(&stored, Some(invalidation));
}
Ok(())
}
pub fn grant_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
roles: &[String],
grantee: &str,
) -> PgWireResult<Vec<Response>> {
require_tenant_admin(identity, "grant roles")?;
if roles.is_empty() {
return Err(sqlstate_error("42601", "GRANT: missing role name"));
}
if state.credentials.get_user(grantee).is_some() {
grant_roles_to_user(state, identity, roles, grantee)
} else if state.roles.get_role(grantee).is_some() {
grant_role_to_role(state, identity, roles, grantee)
} else {
Err(sqlstate_error(
"42704",
&format!("grantee '{grantee}' is not a known user or role"),
))
}
}
fn grant_roles_to_user(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_names: &[String],
username: &str,
) -> PgWireResult<Vec<Response>> {
let mut roles = current_roles(state, username)?;
for name in role_names {
let role = parse_role(name);
if matches!(role, Role::Superuser) && !identity.is_superuser {
return Err(sqlstate_error(
"42501",
"only superuser can grant superuser role",
));
}
if !roles.contains(&role) {
roles.push(role);
}
}
propose_user_with_roles(
state,
username,
roles,
crate::control::security::buses::SessionInvalidationReason::RoleGranted,
)?;
state.audit_record(
AuditEvent::PrivilegeChange,
Some(identity.tenant_id),
&identity.username,
&format!(
"granted role(s) {} to user '{username}'",
role_names.join(", ")
),
);
Ok(vec![Response::Execution(Tag::new("GRANT"))])
}
fn grant_role_to_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_names: &[String],
child: &str,
) -> PgWireResult<Vec<Response>> {
if role_names.len() != 1 {
return Err(sqlstate_error(
"0A000",
"a role can inherit from only one parent role; grant one role at a time",
));
}
let parent = &role_names[0];
super::super::role::set_role_parent(state, child, Some(parent))?;
state.audit_record(
AuditEvent::PrivilegeChange,
Some(identity.tenant_id),
&identity.username,
&format!("granted role '{parent}' to role '{child}'"),
);
Ok(vec![Response::Execution(Tag::new("GRANT"))])
}
pub fn revoke_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
roles: &[String],
grantee: &str,
) -> PgWireResult<Vec<Response>> {
require_tenant_admin(identity, "revoke roles")?;
if roles.is_empty() {
return Err(sqlstate_error("42601", "REVOKE: missing role name"));
}
if grantee == identity.username
&& roles
.iter()
.any(|r| matches!(parse_role(r), Role::Superuser))
{
return Err(sqlstate_error(
"42501",
"cannot revoke your own superuser role",
));
}
if state.credentials.get_user(grantee).is_some() {
revoke_roles_from_user(state, identity, roles, grantee)
} else if state.roles.get_role(grantee).is_some() {
revoke_role_from_role(state, identity, roles, grantee)
} else {
Err(sqlstate_error(
"42704",
&format!("grantee '{grantee}' is not a known user or role"),
))
}
}
fn revoke_roles_from_user(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_names: &[String],
username: &str,
) -> PgWireResult<Vec<Response>> {
let mut roles = current_roles(state, username)?;
let revoked: Vec<Role> = role_names.iter().map(|n| parse_role(n)).collect();
for role in &revoked {
if !roles.contains(role) {
return Err(sqlstate_error(
"42704",
&format!("user '{username}' does not have role '{role}'"),
));
}
}
roles.retain(|r| !revoked.contains(r));
propose_user_with_roles(
state,
username,
roles,
crate::control::security::buses::SessionInvalidationReason::RoleRevoked,
)?;
state.audit_record(
AuditEvent::PrivilegeChange,
Some(identity.tenant_id),
&identity.username,
&format!(
"revoked role(s) {} from user '{username}'",
role_names.join(", ")
),
);
Ok(vec![Response::Execution(Tag::new("REVOKE"))])
}
fn revoke_role_from_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_names: &[String],
child: &str,
) -> PgWireResult<Vec<Response>> {
if role_names.len() != 1 {
return Err(sqlstate_error(
"0A000",
"a role inherits from at most one parent role; revoke one role at a time",
));
}
let parent = &role_names[0];
let current_parent = state.roles.get_role(child).and_then(|r| r.parent);
if current_parent.as_deref() != Some(parent.as_str()) {
return Err(sqlstate_error(
"42704",
&format!("role '{child}' does not inherit from '{parent}'"),
));
}
super::super::role::set_role_parent(state, child, None)?;
state.audit_record(
AuditEvent::PrivilegeChange,
Some(identity.tenant_id),
&identity.username,
&format!("revoked role '{parent}' from role '{child}'"),
);
Ok(vec![Response::Execution(Tag::new("REVOKE"))])
}