use std::collections::{BTreeMap, BTreeSet};
use crate::ast::{DatabasePrivilege, DatabaseRevokeBehavior, GrantDatabaseStmt, RoleAttribute};
use crate::catalog::DATABASE_NAME;
use crate::SQLError;
use crate::catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DatabaseAclPrivilege {
Connect,
Create,
Temporary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DatabasePrivilegeCheck {
pub privilege: DatabaseAclPrivilege,
pub grant_option: bool,
}
impl DatabaseAclPrivilege {
const fn mask(self) -> DatabasePrivileges {
match self {
Self::Connect => DatabasePrivileges {
connect: true,
create: false,
temporary: false,
},
Self::Create => DatabasePrivileges {
connect: false,
create: true,
temporary: false,
},
Self::Temporary => DatabasePrivileges {
connect: false,
create: false,
temporary: true,
},
}
}
}
pub fn requested_acl_privileges(
requested: &[DatabasePrivilege],
) -> Result<Vec<DatabaseAclPrivilege>, SQLError> {
requested
.iter()
.map(|privilege| match privilege {
DatabasePrivilege::Connect => Ok(DatabaseAclPrivilege::Connect),
DatabasePrivilege::Create => Ok(DatabaseAclPrivilege::Create),
DatabasePrivilege::Temporary => Ok(DatabaseAclPrivilege::Temporary),
DatabasePrivilege::Unsupported(name) => Err(SQLError::Routine {
sqlstate: "0LP01".into(),
message: format!("invalid privilege type {name} for database"),
}),
})
.collect()
}
pub fn parse_privilege_checks(value: &str) -> Result<Vec<DatabasePrivilegeCheck>, SQLError> {
value
.split(',')
.map(|item| {
let item = item.trim();
let upper = item.to_ascii_uppercase();
let (name, grant_option) = upper
.strip_suffix(" WITH GRANT OPTION")
.map_or((upper.as_str(), false), |name| (name.trim_end(), true));
let privilege = match name {
"CONNECT" => DatabaseAclPrivilege::Connect,
"CREATE" => DatabaseAclPrivilege::Create,
"TEMP" | "TEMPORARY" => DatabaseAclPrivilege::Temporary,
_ => {
return Err(SQLError::Routine {
sqlstate: "22023".into(),
message: format!("unrecognized privilege type: \"{item}\""),
})
}
};
Ok(DatabasePrivilegeCheck {
privilege,
grant_option,
})
})
.collect()
}
fn acl_grantor<'a>(entry: &'a DatabaseAclEntry, owner: &'a str) -> &'a str {
entry.grantor.as_deref().unwrap_or(owner)
}
fn materialize_acl(security: &mut DatabaseSecurity) {
if security.acl.is_some() {
return;
}
let owner = security.role_owner.clone();
security.acl = Some(vec![
DatabaseAclEntry {
role: owner.clone(),
grantor: Some(owner.clone()),
privileges: DatabasePrivileges::ALL,
grant_options: DatabasePrivileges::default(),
},
DatabaseAclEntry {
role: "PUBLIC".into(),
grantor: Some(owner),
privileges: DatabasePrivileges {
connect: true,
create: false,
temporary: true,
},
grant_options: DatabasePrivileges::default(),
},
]);
}
fn grant_option_roles(
security: &DatabaseSecurity,
privilege: DatabaseAclPrivilege,
) -> BTreeSet<String> {
let mut reachable = BTreeSet::from([security.role_owner.clone()]);
let Some(acl) = security.acl.as_ref() else {
return reachable;
};
loop {
let mut changed = false;
for entry in acl {
if entry.role != "PUBLIC"
&& entry.grant_options.intersects(privilege.mask())
&& reachable.contains(acl_grantor(entry, &security.role_owner))
{
changed |= reachable.insert(entry.role.clone());
}
}
if !changed {
return reachable;
}
}
}
pub fn select_acl_grantor(
security: &DatabaseSecurity,
privilege: DatabaseAclPrivilege,
current_user: &str,
roles: &BTreeMap<String, RoleDefinition>,
memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
) -> Option<String> {
if role_inherits(roles, memberships, current_user, &security.role_owner) {
return Some(security.role_owner.clone());
}
let grant_options = grant_option_roles(security, privilege);
if grant_options.contains(current_user) {
return Some(current_user.to_string());
}
security.acl.as_ref().and_then(|acl| {
acl.iter()
.filter(|entry| entry.role != "PUBLIC" && grant_options.contains(&entry.role))
.find(|entry| role_inherits(roles, memberships, current_user, &entry.role))
.map(|entry| entry.role.clone())
})
}
pub fn role_has_database_privilege_check(
security: &DatabaseSecurity,
subject: &str,
check: DatabasePrivilegeCheck,
roles: &BTreeMap<String, RoleDefinition>,
memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
) -> bool {
if roles
.get(subject)
.is_some_and(|role| role.has(RoleAttribute::Superuser))
{
return true;
}
if check.grant_option {
return grant_option_roles(security, check.privilege)
.iter()
.any(|role| role_inherits(roles, memberships, subject, role));
}
match security.acl.as_ref() {
None => {
role_inherits(roles, memberships, subject, &security.role_owner)
|| matches!(
check.privilege,
DatabaseAclPrivilege::Connect | DatabaseAclPrivilege::Temporary
)
}
Some(acl) => acl.iter().any(|entry| {
entry.privileges.intersects(check.privilege.mask())
&& (entry.role == "PUBLIC"
|| role_inherits(roles, memberships, subject, &entry.role))
}),
}
}
pub fn role_has_database_privilege(
security: &DatabaseSecurity,
subject: &str,
privilege: DatabaseAclPrivilege,
roles: &BTreeMap<String, RoleDefinition>,
memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
) -> bool {
role_has_database_privilege_check(
security,
subject,
DatabasePrivilegeCheck {
privilege,
grant_option: false,
},
roles,
memberships,
)
}
pub fn grant_acl(
security: &mut DatabaseSecurity,
privilege: DatabaseAclPrivilege,
grantees: &[String],
grantor: &str,
grant_option: bool,
) {
materialize_acl(security);
let owner = security.role_owner.clone();
let acl = security
.acl
.as_mut()
.expect("database ACL was materialized");
for grantee in grantees {
let position = acl
.iter()
.position(|entry| entry.role == *grantee && acl_grantor(entry, &owner) == grantor)
.unwrap_or_else(|| {
acl.push(DatabaseAclEntry {
role: grantee.clone(),
grantor: Some(grantor.to_string()),
privileges: DatabasePrivileges::default(),
grant_options: DatabasePrivileges::default(),
});
acl.len() - 1
});
let entry = &mut acl[position];
entry.privileges.insert(privilege.mask());
if grant_option && grantee != "PUBLIC" && grantee != &owner {
entry.grant_options.insert(privilege.mask());
}
}
}
pub fn revoke_acl(
security: &mut DatabaseSecurity,
privilege: DatabaseAclPrivilege,
grantees: &[String],
grantor: &str,
grant_option_only: bool,
cascade: bool,
) -> Result<(), SQLError> {
let before = grant_option_roles(security, privilege);
materialize_acl(security);
let owner = security.role_owner.clone();
let acl = security
.acl
.as_mut()
.expect("database ACL was materialized");
for entry in acl
.iter_mut()
.filter(|entry| grantees.contains(&entry.role) && acl_grantor(entry, &owner) == grantor)
{
entry.grant_options.remove(privilege.mask());
if !grant_option_only {
entry.privileges.remove(privilege.mask());
}
}
remove_empty_entries(acl);
revoke_dependent_acl(security, privilege, &before, cascade)
}
fn revoke_dependent_acl(
security: &mut DatabaseSecurity,
privilege: DatabaseAclPrivilege,
before: &BTreeSet<String>,
cascade: bool,
) -> Result<(), SQLError> {
loop {
let current = grant_option_roles(security, privilege);
let lost = before
.difference(¤t)
.cloned()
.collect::<BTreeSet<_>>();
if lost.is_empty() {
return Ok(());
}
let owner = security.role_owner.clone();
let dependent = security.acl.as_ref().is_some_and(|acl| {
acl.iter().any(|entry| {
lost.contains(acl_grantor(entry, &owner))
&& (entry.privileges.intersects(privilege.mask())
|| entry.grant_options.intersects(privilege.mask()))
})
});
if !dependent {
return Ok(());
}
if !cascade {
return Err(SQLError::Routine {
sqlstate: "2BP01".into(),
message: "dependent privileges exist".into(),
});
}
let acl = security
.acl
.as_mut()
.expect("dependent database privileges require an explicit ACL");
for entry in acl
.iter_mut()
.filter(|entry| lost.contains(acl_grantor(entry, &owner)))
{
entry.privileges.remove(privilege.mask());
entry.grant_options.remove(privilege.mask());
}
remove_empty_entries(acl);
}
}
fn remove_empty_entries(acl: &mut Vec<DatabaseAclEntry>) {
acl.retain(|entry| !entry.privileges.is_empty() || !entry.grant_options.is_empty());
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DatabasePrivileges {
pub connect: bool,
pub create: bool,
pub temporary: bool,
}
impl DatabasePrivileges {
pub const ALL: Self = Self {
connect: true,
create: true,
temporary: true,
};
pub const fn intersects(self, other: Self) -> bool {
(self.connect && other.connect)
|| (self.create && other.create)
|| (self.temporary && other.temporary)
}
pub fn insert(&mut self, other: Self) {
self.connect |= other.connect;
self.create |= other.create;
self.temporary |= other.temporary;
}
pub fn remove(&mut self, other: Self) {
self.connect &= !other.connect;
self.create &= !other.create;
self.temporary &= !other.temporary;
}
pub const fn is_empty(self) -> bool {
!self.connect && !self.create && !self.temporary
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DatabaseAclEntry {
pub role: String,
pub grantor: Option<String>,
pub privileges: DatabasePrivileges,
pub grant_options: DatabasePrivileges,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DatabaseSecurity {
pub role_owner: String,
pub acl: Option<Vec<DatabaseAclEntry>>,
}
impl DatabaseSecurity {
pub fn bootstrap() -> Self {
Self {
role_owner: "uqa".into(),
acl: None,
}
}
}
pub fn resolve_database_grant_targets(databases: &[String]) -> Result<(), SQLError> {
for database in databases {
if database != DATABASE_NAME {
return Err(SQLError::Routine {
sqlstate: "3D000".into(),
message: format!("database \"{database}\" does not exist"),
});
}
}
Ok(())
}
pub fn apply_database_acl(
statement: &GrantDatabaseStmt,
grantees: &[String],
privileges: &[DatabaseAclPrivilege],
current_user: &str,
roles: &BTreeMap<String, RoleDefinition>,
memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
current: &DatabaseSecurity,
) -> Result<(DatabaseSecurity, usize), SQLError> {
let grantors = privileges
.iter()
.map(|privilege| {
(
*privilege,
select_acl_grantor(current, *privilege, current_user, roles, memberships),
)
})
.collect::<Vec<_>>();
let grantable = grantors
.iter()
.filter(|(_, grantor)| grantor.is_some())
.count();
let mut next = current.clone();
for (privilege, grantor) in grantors {
let Some(grantor) = grantor else {
continue;
};
if statement.is_grant {
grant_acl(
&mut next,
privilege,
grantees,
&grantor,
statement.grant_option,
);
} else {
revoke_acl(
&mut next,
privilege,
grantees,
&grantor,
statement.grant_option_only,
statement.revoke_behavior == DatabaseRevokeBehavior::Cascade,
)?;
}
}
Ok((next, grantable))
}
pub fn validate_database_acl_roles(
statement: &GrantDatabaseStmt,
grantees: &[String],
requested_grantor: Option<&str>,
current_user: &str,
roles: &BTreeMap<String, RoleDefinition>,
) -> Result<(), SQLError> {
for role in grantees {
if role != "PUBLIC" && !roles.contains_key(role) {
return Err(SQLError::Routine {
sqlstate: "42704".into(),
message: format!("role \"{role}\" does not exist"),
});
}
}
if statement.is_grant && statement.grant_option && grantees.iter().any(|role| role == "PUBLIC")
{
return Err(SQLError::Routine {
sqlstate: "0LP01".into(),
message: "grant options can only be granted to roles".into(),
});
}
if let Some(requested_grantor) = requested_grantor {
if !roles.contains_key(requested_grantor) {
return Err(SQLError::Routine {
sqlstate: "42704".into(),
message: format!("role \"{requested_grantor}\" does not exist"),
});
}
if requested_grantor != current_user {
return Err(SQLError::Routine {
sqlstate: "0A000".into(),
message: "grantor must be current user".into(),
});
}
}
Ok(())
}
pub fn database_acl_warning(is_grant: bool, partial: bool, name: &str) -> (&'static str, String) {
let message = match (is_grant, partial) {
(true, true) => format!("not all privileges were granted for \"{name}\""),
(true, false) => format!("no privileges were granted for \"{name}\""),
(false, true) => format!("not all privileges could be revoked for \"{name}\""),
(false, false) => format!("no privileges could be revoked for \"{name}\""),
};
("WARNING", message)
}
pub fn validate_stored_database_security(
security: &DatabaseSecurity,
roles: &BTreeMap<String, RoleDefinition>,
) -> Result<(), String> {
if !roles.contains_key(&security.role_owner) {
return Err(format!(
"persisted database owner `{}` does not exist",
security.role_owner
));
}
if let Some(acl) = security.acl.as_ref() {
for entry in acl {
let grantor = entry.grantor.as_deref().unwrap_or(&security.role_owner);
if (entry.role != "PUBLIC" && !roles.contains_key(&entry.role))
|| !roles.contains_key(grantor)
{
return Err(format!(
"persisted database ACL `{}` from `{grantor}` references a missing role",
entry.role
));
}
}
}
Ok(())
}