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::result::{DdlError, DdlResult};
use super::support::{parse_role, require_tenant_admin, status};
fn current_roles(state: &SharedState, username: &str) -> Result<Vec<Role>, DdlError> {
state
.credentials
.get_user(username)
.map(|r| r.roles)
.ok_or_else(|| DdlError {
sqlstate: "42704".to_string(),
message: format!("user '{username}' not found"),
})
}
fn propose_user_with_roles(
state: &SharedState,
username: &str,
new_roles: Vec<Role>,
invalidation: crate::control::security::buses::SessionInvalidationReason,
) -> Result<(), DdlError> {
let stored = state
.credentials
.prepare_user_update(username, None, Some(new_roles))
.map_err(|e| DdlError {
sqlstate: "42704".to_string(),
message: e.to_string(),
})?;
let entry = CatalogEntry::PutUser(Box::new(stored.clone()));
let log_index = propose_catalog_entry(state, &entry).map_err(|e| DdlError {
sqlstate: "XX000".to_string(),
message: format!("metadata propose: {e}"),
})?;
if log_index == 0 {
{
let catalog = state.credentials.catalog();
catalog.put_user(&stored).map_err(|e| DdlError {
sqlstate: "XX000".to_string(),
message: 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,
) -> Result<Vec<DdlResult>, DdlError> {
require_tenant_admin(identity, "grant roles")?;
if roles.is_empty() {
return Err(DdlError {
sqlstate: "42601".to_string(),
message: "GRANT: missing role name".to_string(),
});
}
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(DdlError {
sqlstate: "42704".to_string(),
message: format!("grantee '{grantee}' is not a known user or role"),
})
}
}
fn grant_roles_to_user(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_names: &[String],
username: &str,
) -> Result<Vec<DdlResult>, DdlError> {
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(DdlError {
sqlstate: "42501".to_string(),
message: "only superuser can grant superuser role".to_string(),
});
}
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(status("GRANT"))
}
fn grant_role_to_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_names: &[String],
child: &str,
) -> Result<Vec<DdlResult>, DdlError> {
if role_names.len() != 1 {
return Err(DdlError {
sqlstate: "0A000".to_string(),
message: "a role can inherit from only one parent role; grant one role at a time"
.to_string(),
});
}
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(status("GRANT"))
}
pub fn revoke_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
roles: &[String],
grantee: &str,
) -> Result<Vec<DdlResult>, DdlError> {
require_tenant_admin(identity, "revoke roles")?;
if roles.is_empty() {
return Err(DdlError {
sqlstate: "42601".to_string(),
message: "REVOKE: missing role name".to_string(),
});
}
if grantee == identity.username
&& roles
.iter()
.any(|r| matches!(parse_role(r), Role::Superuser))
{
return Err(DdlError {
sqlstate: "42501".to_string(),
message: "cannot revoke your own superuser role".to_string(),
});
}
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(DdlError {
sqlstate: "42704".to_string(),
message: format!("grantee '{grantee}' is not a known user or role"),
})
}
}
fn revoke_roles_from_user(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_names: &[String],
username: &str,
) -> Result<Vec<DdlResult>, DdlError> {
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(DdlError {
sqlstate: "42704".to_string(),
message: 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(status("REVOKE"))
}
fn revoke_role_from_role(
state: &SharedState,
identity: &AuthenticatedIdentity,
role_names: &[String],
child: &str,
) -> Result<Vec<DdlResult>, DdlError> {
if role_names.len() != 1 {
return Err(DdlError {
sqlstate: "0A000".to_string(),
message: "a role inherits from at most one parent role; revoke one role at a time"
.to_string(),
});
}
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(DdlError {
sqlstate: "42704".to_string(),
message: 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(status("REVOKE"))
}