use crate::ast::ObjectAclEntry;
use crate::catalog::roles::{
identity::RoleSubject, role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey,
RoleReference,
};
use crate::SQLError;
use std::collections::{BTreeMap, BTreeSet};
use uqa_core::{catalog_acl::AclGrantee, catalog_role::RoleIdentity};
pub fn privilege_allowed(
owner: &RoleIdentity,
acl: Option<&[ObjectAclEntry]>,
grant_option: bool,
superuser: bool,
has_role: impl Fn(&RoleIdentity) -> bool,
) -> bool {
if superuser || (grant_option && has_role(owner)) {
return true;
}
acl.map_or(!grant_option, |acl| {
acl.iter().any(|entry| {
(!grant_option || entry.grant_option)
&& ((entry.role.is_none() && !grant_option)
|| entry.role.as_ref().is_some_and(&has_role))
})
})
}
pub fn grant_option_roles(
owner: RoleIdentity,
acl: Option<&[ObjectAclEntry]>,
) -> BTreeSet<RoleIdentity> {
let mut reachable = BTreeSet::from([owner]);
let Some(acl) = acl else {
return reachable;
};
loop {
let mut changed = false;
for entry in acl {
if let Some(role) = entry.role {
if entry.grant_option && reachable.contains(&entry.grantor) {
changed |= reachable.insert(role);
}
}
}
if !changed {
return reachable;
}
}
}
pub fn select_grantor(
owner: RoleIdentity,
acl: Option<&[ObjectAclEntry]>,
current_user: &(impl RoleSubject + ?Sized),
roles: &BTreeMap<String, RoleDefinition>,
memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
) -> Option<RoleIdentity> {
if role_inherits(roles, memberships, current_user, &owner) {
return Some(owner);
}
let grant_options = grant_option_roles(owner, acl);
if let Some(identity) = current_user
.role_definition(roles)
.map(RoleDefinition::identity)
.filter(|identity| grant_options.contains(identity))
{
return Some(identity);
}
acl.and_then(|acl| {
acl.iter()
.filter_map(|entry| entry.role)
.filter(|role| grant_options.contains(role))
.find(|role| role_inherits(roles, memberships, current_user, role))
})
}
fn materialize(
owner: RoleIdentity,
acl: &mut Option<Vec<ObjectAclEntry>>,
) -> &mut Vec<ObjectAclEntry> {
acl.get_or_insert_with(|| {
vec![
ObjectAclEntry {
role: None,
grantor: owner,
grant_option: false,
},
ObjectAclEntry {
role: Some(owner),
grantor: owner,
grant_option: false,
},
]
})
}
#[must_use]
pub fn explicit_acl(
owner: RoleIdentity,
mut acl: Option<Vec<ObjectAclEntry>>,
) -> Option<Vec<ObjectAclEntry>> {
materialize(owner, &mut acl);
acl
}
pub fn grant(
owner: RoleIdentity,
acl: &mut Option<Vec<ObjectAclEntry>>,
grantee: Option<RoleIdentity>,
grantor: RoleIdentity,
grant_option: bool,
) {
let entries = materialize(owner, acl);
if let Some(entry) = entries
.iter_mut()
.find(|entry| entry.role == grantee && entry.grantor == grantor)
{
entry.grant_option |= grant_option;
} else {
entries.push(ObjectAclEntry {
role: grantee,
grantor,
grant_option,
});
}
}
pub fn revoke(
owner: RoleIdentity,
acl: &mut Option<Vec<ObjectAclEntry>>,
grantee: Option<RoleIdentity>,
grantor: RoleIdentity,
grant_option_only: bool,
cascade: bool,
) -> Result<(), SQLError> {
let before = grant_option_roles(owner, acl.as_deref());
let entries = materialize(owner, acl);
let Some(position) = entries
.iter()
.position(|entry| entry.role == grantee && entry.grantor == grantor)
else {
return Ok(());
};
if grant_option_only {
if !entries[position].grant_option {
return Ok(());
}
entries[position].grant_option = false;
} else {
entries.remove(position);
}
revoke_dependents(owner, acl, &before, cascade)
}
fn revoke_dependents(
owner: RoleIdentity,
acl: &mut Option<Vec<ObjectAclEntry>>,
before: &BTreeSet<RoleIdentity>,
cascade: bool,
) -> Result<(), SQLError> {
loop {
let current = grant_option_roles(owner, acl.as_deref());
let lost = before
.difference(¤t)
.copied()
.collect::<BTreeSet<_>>();
let Some(entries) = acl.as_mut() else {
return Ok(());
};
if lost.is_empty() || !entries.iter().any(|entry| lost.contains(&entry.grantor)) {
return Ok(());
}
if !cascade {
return Err(SQLError::Diagnostic {
sqlstate: "2BP01".into(),
message: "dependent privileges exist".into(),
detail: None,
hint: Some("Use CASCADE to revoke them too.".into()),
});
}
entries.retain(|entry| !lost.contains(&entry.grantor));
}
}
pub fn rewrite_owner(
acl: &mut Option<Vec<ObjectAclEntry>>,
old_owner: RoleIdentity,
new_owner: RoleIdentity,
) {
let Some(entries) = acl.as_mut() else {
return;
};
for entry in entries.iter_mut() {
if entry.role == Some(old_owner) {
entry.role = Some(new_owner);
}
if entry.grantor == old_owner {
entry.grantor = new_owner;
}
}
let mut merged: Vec<ObjectAclEntry> = Vec::with_capacity(entries.len());
for entry in std::mem::take(entries) {
if let Some(existing) = merged
.iter_mut()
.find(|existing| existing.role == entry.role && existing.grantor == entry.grantor)
{
existing.grant_option |= entry.grant_option;
} else {
merged.push(entry);
}
}
*entries = merged;
}
pub fn validate_acl(
owner: RoleIdentity,
acl: Option<&[ObjectAclEntry]>,
object_kind: &str,
) -> Result<(), SQLError> {
let invalid =
|message: &str| SQLError::Internal(format!("invalid {object_kind} authority: {message}"));
if !owner.is_valid() {
return Err(invalid("missing owner incarnation"));
}
let reachable = grant_option_roles(owner, acl);
let mut paths = BTreeSet::new();
for entry in acl.into_iter().flatten() {
if !entry.grantor.is_valid() || entry.role.is_some_and(|role| !role.is_valid()) {
return Err(invalid("missing ACL endpoint incarnation"));
}
if entry.role.is_none() && entry.grant_option {
return Err(invalid("PUBLIC cannot retain a grant option"));
}
if !paths.insert((entry.role, entry.grantor)) {
return Err(invalid("duplicate ACL grant path"));
}
if !reachable.contains(&entry.grantor) {
return Err(invalid("ACL grantor has no owner-rooted grant option"));
}
}
Ok(())
}
pub fn acl_roles(owner: RoleIdentity, acl: Option<&[ObjectAclEntry]>) -> BTreeSet<RoleIdentity> {
let mut identities = BTreeSet::from([owner]);
for entry in acl.into_iter().flatten() {
identities.extend(entry.role);
identities.insert(entry.grantor);
}
identities
}
pub fn added_acl_roles(
before: (RoleIdentity, Option<&[ObjectAclEntry]>),
after: (RoleIdentity, Option<&[ObjectAclEntry]>),
roles: &BTreeMap<String, RoleDefinition>,
object_kind: &str,
added: &mut BTreeSet<String>,
) -> Result<(), SQLError> {
let mut old = acl_roles(before.0, before.1);
old.remove(&before.0);
let mut new = acl_roles(after.0, after.1);
new.remove(&after.0);
validate_acl(after.0, after.1, object_kind)?;
for identity in new.difference(&old) {
added.insert(
identity
.role_name(roles)
.ok_or_else(|| {
SQLError::Internal(format!(
"invalid {object_kind} authority: missing ACL incarnation"
))
})?
.to_owned(),
);
}
Ok(())
}
pub fn bind_grantees(
grantees: &[AclGrantee],
roles: &BTreeMap<String, RoleDefinition>,
) -> Result<Vec<Option<RoleIdentity>>, SQLError> {
grantees
.iter()
.map(|grantee| {
grantee
.role_name()
.map(|name| {
RoleReference::from(name)
.bind(roles)
.map(|role| role.identity())
})
.transpose()
})
.collect()
}
pub fn acl_warning(is_grant: bool, local_name: &str) -> crate::SQLNotice {
super::acl_warning::acl_warning(is_grant, false, local_name)
}
#[cfg(test)]
mod tests {
use super::*;
fn role(value: u8) -> RoleIdentity {
RoleIdentity {
oid: i64::from(value),
object_id: [value; 16],
}
}
fn grant_entry(grantee: u8, grantor: u8) -> ObjectAclEntry {
ObjectAclEntry {
role: Some(role(grantee)),
grantor: role(grantor),
grant_option: true,
}
}
#[test]
fn grant_option_reachability_requires_an_owner_root() {
let disconnected_cycle = [grant_entry(2, 3), grant_entry(3, 2)];
assert_eq!(
grant_option_roles(role(1), Some(&disconnected_cycle)),
BTreeSet::from([role(1)])
);
let rooted_cycle = [grant_entry(2, 1), grant_entry(3, 2), grant_entry(2, 3)];
assert_eq!(
grant_option_roles(role(1), Some(&rooted_cycle)),
BTreeSet::from([role(1), role(2), role(3)])
);
}
#[test]
fn grant_option_reachability_accepts_an_independent_owner_path() {
let acl = [
grant_entry(2, 1),
grant_entry(3, 2),
grant_entry(3, 1),
grant_entry(4, 3),
];
assert_eq!(
grant_option_roles(role(1), Some(&acl[2..])),
BTreeSet::from([role(1), role(3), role(4)])
);
}
#[test]
fn revoking_a_grant_option_with_dependent_grants_requires_cascade() {
let owner = role(1);
let mut acl = None;
grant(owner, &mut acl, Some(role(2)), owner, true);
grant(owner, &mut acl, Some(role(3)), role(2), false);
let error = revoke(owner, &mut acl.clone(), Some(role(2)), owner, true, false).unwrap_err();
assert_eq!(error.sqlstate(), Some("2BP01"));
assert_eq!(error.hint(), Some("Use CASCADE to revoke them too."));
revoke(owner, &mut acl, Some(role(2)), owner, true, true).unwrap();
assert_eq!(
acl.unwrap(),
[
ObjectAclEntry {
role: None,
grantor: owner,
grant_option: false
},
ObjectAclEntry {
role: Some(owner),
grantor: owner,
grant_option: false
},
ObjectAclEntry {
role: Some(role(2)),
grantor: owner,
grant_option: false
},
]
);
}
}