use alloc::string::{String, ToString};
use alloc::vec::Vec;
use spg_sql::ast::{
ColumnName, Expr, GrantObject, GrantStatement, SelectItem, SelectStatement, Statement, TableRef,
};
use spg_storage::{AclItem, TableSchema, priv_bits};
use crate::session::LOGIN_ROLE;
use crate::{Engine, EngineError};
const PRIV_LETTERS: [(u16, char); 13] = [
(priv_bits::INSERT, 'a'),
(priv_bits::SELECT, 'r'),
(priv_bits::UPDATE, 'w'),
(priv_bits::DELETE, 'd'),
(priv_bits::TRUNCATE, 'D'),
(priv_bits::REFERENCES, 'x'),
(priv_bits::TRIGGER, 't'),
(priv_bits::MAINTAIN, 'm'),
(priv_bits::USAGE, 'U'),
(priv_bits::CREATE, 'C'),
(priv_bits::CONNECT, 'c'),
(priv_bits::TEMPORARY, 'T'),
(priv_bits::EXECUTE, 'X'),
];
pub(crate) fn priv_from_word(w: &str) -> Option<u16> {
let bare = w
.trim()
.split_once(" WITH ")
.map_or(w.trim(), |(a, _)| a.trim());
Some(match bare.to_ascii_uppercase().as_str() {
"SELECT" => priv_bits::SELECT,
"INSERT" => priv_bits::INSERT,
"UPDATE" => priv_bits::UPDATE,
"DELETE" => priv_bits::DELETE,
"TRUNCATE" => priv_bits::TRUNCATE,
"REFERENCES" => priv_bits::REFERENCES,
"TRIGGER" => priv_bits::TRIGGER,
"MAINTAIN" => priv_bits::MAINTAIN,
"USAGE" => priv_bits::USAGE,
"CREATE" => priv_bits::CREATE,
"CONNECT" => priv_bits::CONNECT,
"TEMPORARY" | "TEMP" => priv_bits::TEMPORARY,
"EXECUTE" => priv_bits::EXECUTE,
_ => return None,
})
}
pub(crate) fn priv_word(bit: u16) -> &'static str {
match bit {
priv_bits::SELECT => "SELECT",
priv_bits::INSERT => "INSERT",
priv_bits::UPDATE => "UPDATE",
priv_bits::DELETE => "DELETE",
priv_bits::TRUNCATE => "TRUNCATE",
priv_bits::REFERENCES => "REFERENCES",
priv_bits::TRIGGER => "TRIGGER",
priv_bits::MAINTAIN => "MAINTAIN",
priv_bits::USAGE => "USAGE",
priv_bits::CREATE => "CREATE",
priv_bits::CONNECT => "CONNECT",
priv_bits::TEMPORARY => "TEMPORARY",
priv_bits::EXECUTE => "EXECUTE",
_ => "",
}
}
pub(crate) fn priv_iter(mask: u16) -> impl Iterator<Item = u16> {
PRIV_LETTERS
.into_iter()
.filter(move |(bit, _)| mask & *bit != 0)
.map(|(bit, _)| bit)
}
fn render_aclitem(a: &AclItem) -> String {
let mut s = a.grantee.clone();
s.push('=');
for (bit, letter) in PRIV_LETTERS {
if a.privs & bit != 0 {
s.push(letter);
if a.grantable & bit != 0 {
s.push('*');
}
}
}
s.push('/');
s.push_str(&a.grantor);
s
}
pub(crate) fn render_acl_list(acl: &[AclItem]) -> Option<String> {
if acl.is_empty() {
return None;
}
let items: Vec<String> = acl.iter().map(render_aclitem).collect();
Some(alloc::format!("{{{}}}", items.join(",")))
}
pub(crate) fn render_relacl(schema: &TableSchema) -> Option<String> {
render_acl_list(&schema.acl)
}
pub(crate) fn catalog_object_privs(
cat: &spg_storage::Catalog,
on_schema: bool,
roles: &alloc::collections::BTreeSet<String>,
) -> u16 {
let acl = if on_schema {
cat.schema_acl()
} else {
cat.database_acl()
};
if acl.is_empty() {
return if on_schema {
priv_bits::USAGE
} else {
priv_bits::CONNECT | priv_bits::TEMPORARY
};
}
let mut held = 0;
for a in acl {
if a.grantee.is_empty() || roles.iter().any(|r| a.grantee.eq_ignore_ascii_case(r)) {
held |= a.privs;
}
}
held
}
pub(crate) fn render_nspacl(cat: &spg_storage::Catalog) -> String {
if let Some(rendered) = render_acl_list(cat.schema_acl()) {
return rendered;
}
alloc::format!("{{{o}=UC/{o},=U/{o}}}", o = SCHEMA_OWNER_ROLE)
}
pub(crate) fn column_privs(
col: &spg_storage::ColumnSchema,
roles: &alloc::collections::BTreeSet<String>,
) -> u16 {
let mut held = 0;
for a in &col.acl {
if a.grantee.is_empty() || roles.iter().any(|r| a.grantee.eq_ignore_ascii_case(r)) {
held |= a.privs;
}
}
held
}
pub(crate) fn privs_of_roles(
schema: &TableSchema,
owner: &str,
roles: &alloc::collections::BTreeSet<String>,
) -> u16 {
if roles.iter().any(|r| r.eq_ignore_ascii_case(owner)) {
return priv_bits::ALL;
}
let mut held = 0;
for a in &schema.acl {
if a.grantee.is_empty() || roles.iter().any(|r| a.grantee.eq_ignore_ascii_case(r)) {
held |= a.privs;
}
}
held
}
#[derive(Default)]
pub(crate) struct ColRead {
pub all: bool,
pub cols: alloc::collections::BTreeSet<String>,
}
impl Engine {
pub(crate) fn table_owner<'a>(&self, schema: &'a TableSchema) -> &'a str {
schema.owner.as_deref().unwrap_or(LOGIN_ROLE)
}
pub(crate) fn acl_holds(&self, table: &str, wanted: u16) -> bool {
self.acl_holds_as(table, wanted, None)
}
pub(crate) fn acl_holds_as(&self, table: &str, wanted: u16, as_role: Option<&str>) -> bool {
let superuser = match as_role {
Some(r) => self.role_is_superuser(r),
None => self.is_superuser(),
};
if superuser {
return true;
}
let role = as_role.unwrap_or_else(|| self.current_role());
let baseline = self.coarse_role_privs(role);
let Some(t) = self.active_catalog().get(table) else {
return true;
};
let owner = self.table_owner(t.schema()).to_string();
let roles = self.users.effective_roles(role);
(privs_of_roles(t.schema(), &owner, &roles) | baseline) & wanted == wanted
}
fn coarse_role_privs(&self, role: &str) -> u16 {
use spg_storage::priv_bits;
if !self.session_is_authenticated()
|| !role.eq_ignore_ascii_case(self.session_user())
|| self
.session_params
.contains_key(crate::session::CURRENT_ROLE_KEY)
{
return 0;
}
match self.effective_users().get(role).map(|r| r.role) {
Some(crate::users::Role::Admin) => priv_bits::ALL,
Some(crate::users::Role::ReadWrite) => {
priv_bits::SELECT | priv_bits::INSERT | priv_bits::UPDATE | priv_bits::DELETE
}
Some(crate::users::Role::ReadOnly) => priv_bits::SELECT,
None => 0,
}
}
pub(crate) fn acl_require(&self, table: &str, wanted: u16) -> Result<(), EngineError> {
if self.acl_holds(table, wanted) {
Ok(())
} else {
Err(EngineError::Unsupported(alloc::format!(
"permission denied for table {table}"
)))
}
}
pub(crate) fn acl_require_owner(&self, table: &str) -> Result<(), EngineError> {
if self.is_superuser() {
return Ok(());
}
let Some(t) = self.active_catalog().get(table) else {
return Ok(());
};
if self
.table_owner(t.schema())
.eq_ignore_ascii_case(self.current_role())
{
Ok(())
} else {
Err(EngineError::Unsupported(alloc::format!(
"must be owner of table {table}"
)))
}
}
fn exec_grant_functions_or_all_tables(
&mut self,
g: &GrantStatement,
grant: bool,
) -> Result<crate::QueryResult, EngineError> {
for r in &g.grantees {
self.acl_check_role_exists(r)?;
}
match &g.object {
GrantObject::Functions(names) => {
let mut mask = 0u16;
if g.privileges.is_empty() {
mask = priv_bits::ALL_FUNCTION;
} else {
for p in &g.privileges {
mask |= if p.word.eq_ignore_ascii_case("ALL") {
priv_bits::ALL_FUNCTION
} else {
priv_from_word(&p.word).ok_or_else(|| {
EngineError::Unsupported(alloc::format!(
"unrecognized privilege type \"{}\"",
p.word.to_ascii_lowercase()
))
})?
};
}
}
let mut keys: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
for (n, sig) in names {
let key = match sig {
Some(types) => {
let repr = alloc::format!("({})", types.join(", "));
let k = spg_storage::function_signature_key(n, &repr);
if self.active_catalog().function_by_key(&k).is_none() {
return Err(EngineError::Unsupported(alloc::format!(
"function {n}({}) does not exist",
types.join(", ")
)));
}
k
}
None => {
let all = self.active_catalog().functions_named(n);
match all.len() {
0 => {
return Err(EngineError::Unsupported(alloc::format!(
"function {n} does not exist"
)));
}
1 => spg_storage::function_signature_key(n, &all[0].args_repr),
_ => {
return Err(EngineError::Unsupported(alloc::format!(
"function name \"{n}\" is not unique"
)));
}
}
}
};
keys.push(key);
}
for k in &keys {
self.acl_apply_function(k, mask, &g.grantees, grant, g.grant_option)?;
}
}
_ => {
let mut mask = 0u16;
if g.privileges.is_empty() {
mask = priv_bits::ALL;
} else {
for p in &g.privileges {
mask |= if p.word.eq_ignore_ascii_case("ALL") {
priv_bits::ALL
} else {
priv_from_word(&p.word).ok_or_else(|| {
EngineError::Unsupported(alloc::format!(
"unrecognized privilege type \"{}\"",
p.word.to_ascii_lowercase()
))
})?
};
}
}
let tables = self.active_catalog().table_names();
for t in tables {
self.acl_apply(&t, mask, &g.grantees, grant, g.grant_option)?;
}
}
}
Ok(crate::QueryResult::CommandOk {
affected: 0,
modified_catalog: true,
})
}
fn exec_grant_non_table(
&mut self,
g: &GrantStatement,
grant: bool,
) -> Result<crate::QueryResult, EngineError> {
let all_mask = match &g.object {
GrantObject::Sequences(_) => priv_bits::ALL_SEQUENCE,
GrantObject::Schemas(_) => priv_bits::ALL_SCHEMA,
_ => priv_bits::ALL_DATABASE,
};
let mut mask = 0u16;
if g.privileges.is_empty() {
mask = all_mask;
} else {
for p in &g.privileges {
mask |= if p.word.eq_ignore_ascii_case("ALL") {
all_mask
} else {
priv_from_word(&p.word).ok_or_else(|| {
EngineError::Unsupported(alloc::format!(
"unrecognized privilege type \"{}\"",
p.word.to_ascii_lowercase()
))
})?
};
}
}
for r in &g.grantees {
self.acl_check_role_exists(r)?;
}
match &g.object {
GrantObject::Sequences(names) => {
for n in names {
if self.active_catalog().sequence(n).is_none() {
return Err(EngineError::Unsupported(alloc::format!(
"relation \"{n}\" does not exist"
)));
}
}
for n in names {
self.acl_apply_sequence(n, mask, &g.grantees, grant, g.grant_option)?;
}
}
GrantObject::Schemas(names) => {
for n in names {
if !spg_storage::is_builtin_schema(n) {
return Err(EngineError::Unsupported(alloc::format!(
"schema \"{n}\" does not exist"
)));
}
}
self.acl_apply_catalog(true, mask, &g.grantees, grant, g.grant_option)?;
}
_ => {
self.acl_apply_catalog(false, mask, &g.grantees, grant, g.grant_option)?;
}
}
Ok(crate::QueryResult::CommandOk {
affected: 0,
modified_catalog: true,
})
}
pub(crate) fn acl_apply_columns(
&mut self,
table: &str,
mask: u16,
columns: &[String],
grantees: &[String],
grant: bool,
grant_option: bool,
) -> Result<(), EngineError> {
self.acl_require_owner(table)?;
let grantor = self.current_role().to_string();
let cat = self.active_catalog_mut();
let t = cat.get_mut(table).ok_or_else(|| {
EngineError::Unsupported(alloc::format!("relation \"{table}\" does not exist"))
})?;
for cname in columns {
let Some(col) = t
.schema_mut()
.columns
.iter_mut()
.find(|sc| sc.name.eq_ignore_ascii_case(cname))
else {
continue;
};
for g in grantees {
let at = col
.acl
.iter()
.position(|a| a.grantee.eq_ignore_ascii_case(g));
if grant {
match at {
Some(i) => {
col.acl[i].privs |= mask;
if grant_option {
col.acl[i].grantable |= mask;
}
}
None => col.acl.push(AclItem {
grantee: g.clone(),
privs: mask,
grantable: if grant_option { mask } else { 0 },
grantor: grantor.clone(),
}),
}
} else if let Some(i) = at {
if grant_option {
col.acl[i].grantable &= !mask;
} else {
col.acl[i].privs &= !mask;
col.acl[i].grantable &= !mask;
}
if col.acl[i].privs == 0 {
col.acl.remove(i);
}
}
}
}
Ok(())
}
fn exec_role_membership(
&mut self,
roles: &[String],
members: &[String],
grant: bool,
) -> Result<crate::QueryResult, EngineError> {
for r in roles {
self.acl_check_role_exists(r)?;
}
for m in members {
self.acl_check_role_exists(m)?;
}
let store = self.role_ddl_users_mut();
for r in roles {
for m in members {
if grant {
store.add_member(r, m);
} else {
store.drop_member(r, m);
}
}
}
Ok(crate::QueryResult::CommandOk {
affected: 0,
modified_catalog: true,
})
}
pub(crate) fn acl_check_role_exists(&self, role: &str) -> Result<(), EngineError> {
if role.is_empty() || role.eq_ignore_ascii_case(LOGIN_ROLE) || self.role_exists(role) {
return Ok(());
}
Err(EngineError::Unsupported(alloc::format!(
"role \"{role}\" does not exist"
)))
}
pub(crate) fn acl_apply(
&mut self,
table: &str,
mask: u16,
grantees: &[String],
grant: bool,
grant_option: bool,
) -> Result<(), EngineError> {
self.acl_require_owner(table)?;
let grantor = self.current_role().to_string();
let owner = {
let t = self.active_catalog().get(table).ok_or_else(|| {
EngineError::Unsupported(alloc::format!("relation \"{table}\" does not exist"))
})?;
self.table_owner(t.schema()).to_string()
};
let cat = self.active_catalog_mut();
let t = cat.get_mut(table).ok_or_else(|| {
EngineError::Unsupported(alloc::format!("relation \"{table}\" does not exist"))
})?;
let acl = &mut t.schema_mut().acl;
if acl.is_empty() {
if !grant {
return Ok(());
}
acl.push(AclItem {
grantee: owner.clone(),
privs: priv_bits::ALL,
grantable: 0,
grantor: owner.clone(),
});
}
for g in grantees {
let pos = acl.iter().position(|a| a.grantee.eq_ignore_ascii_case(g));
if grant {
match pos {
Some(i) => {
acl[i].privs |= mask;
if grant_option {
acl[i].grantable |= mask;
}
}
None => acl.push(AclItem {
grantee: g.clone(),
privs: mask,
grantable: if grant_option { mask } else { 0 },
grantor: grantor.clone(),
}),
}
} else if let Some(i) = pos {
if grant_option {
acl[i].grantable &= !mask;
} else {
acl[i].privs &= !mask;
acl[i].grantable &= !mask;
}
if acl[i].privs == 0 && !acl[i].grantee.eq_ignore_ascii_case(&owner) {
acl.remove(i);
}
}
}
Ok(())
}
}
pub(crate) fn collect_read_tables(
stmt: &SelectStatement,
into: &mut alloc::collections::BTreeSet<String>,
) {
fn walk_table(t: &TableRef, into: &mut alloc::collections::BTreeSet<String>) {
if let Some(sub) = &t.lateral_subquery {
collect_read_tables(sub, into);
return;
}
if t.unnest_expr.is_some()
|| t.generate_series_args.is_some()
|| t.jsonb_each_text_arg.is_some()
|| t.table_fn_call.is_some()
{
return;
}
into.insert(t.name.clone());
}
fn walk_expr(e: &Expr, into: &mut alloc::collections::BTreeSet<String>) {
match e {
Expr::ScalarSubquery(s) => collect_read_tables(s, into),
Expr::Exists { subquery, .. } => collect_read_tables(subquery, into),
Expr::InSubquery { expr, subquery, .. } => {
walk_expr(expr, into);
collect_read_tables(subquery, into);
}
Expr::RowInSubquery { row, subquery, .. }
| Expr::RowCmpSubquery { row, subquery, .. } => {
row.iter().for_each(|x| walk_expr(x, into));
collect_read_tables(subquery, into);
}
Expr::Binary { lhs, rhs, .. } => {
walk_expr(lhs, into);
walk_expr(rhs, into);
}
Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk_expr(expr, into),
Expr::FunctionCall { args, .. } => args.iter().for_each(|a| walk_expr(a, into)),
Expr::Case {
operand,
branches,
else_branch,
} => {
if let Some(o) = operand {
walk_expr(o, into);
}
for (c, v) in branches {
walk_expr(c, into);
walk_expr(v, into);
}
if let Some(x) = else_branch {
walk_expr(x, into);
}
}
Expr::InList { expr, list, .. } => {
walk_expr(expr, into);
list.iter().for_each(|it| walk_expr(it, into));
}
Expr::AnyAll { expr, array, .. } => {
walk_expr(expr, into);
walk_expr(array, into);
}
Expr::Array(items) => items.iter().for_each(|it| walk_expr(it, into)),
Expr::ArraySubscript { target, index } => {
walk_expr(target, into);
walk_expr(index, into);
}
_ => {}
}
}
if let Some(from) = &stmt.from {
walk_table(&from.primary, into);
for j in &from.joins {
walk_table(&j.table, into);
if let Some(on) = &j.on {
walk_expr(on, into);
}
}
}
for item in &stmt.items {
if let SelectItem::Expr { expr, .. } = item {
walk_expr(expr, into);
}
}
if let Some(w) = &stmt.where_ {
walk_expr(w, into);
}
if let Some(h) = &stmt.having {
walk_expr(h, into);
}
if let Some(gs) = &stmt.group_by {
gs.iter().for_each(|g| walk_expr(g, into));
}
for o in &stmt.order_by {
walk_expr(&o.expr, into);
}
for (_, peer) in &stmt.unions {
collect_read_tables(peer, into);
}
for cte in &stmt.ctes {
if let Some(s) = cte.body.as_select() {
collect_read_tables(s, into);
}
}
}
impl Engine {
pub(crate) fn acl_check_statement(&self, stmt: &Statement) -> Result<(), EngineError> {
if self.is_superuser() {
return Ok(());
}
let mut reads: alloc::collections::BTreeMap<String, ColRead> =
alloc::collections::BTreeMap::new();
match stmt {
Statement::Select(s) => {
self.collect_select_reads(s, &mut reads);
}
Statement::Insert(i) => {
let icols: alloc::vec::Vec<String> = i.columns.clone().unwrap_or_default();
self.acl_require_write_columns(&i.table, &icols, priv_bits::INSERT)?;
if let Some(src) = &i.select_source {
self.collect_select_reads(src, &mut reads);
}
}
Statement::Update(u) => {
let targets: alloc::vec::Vec<String> =
u.assignments.iter().map(|(t, _)| t.clone()).collect();
self.acl_require_write_columns(&u.table, &targets, priv_bits::UPDATE)?;
let mut sub = SelectStatement::default();
sub.from = Some(spg_sql::ast::FromClause {
primary: bare_table_ref(u.table.clone()),
joins: alloc::vec::Vec::new(),
});
sub.where_ = u.where_.clone();
for (_, e) in &u.assignments {
sub.items.push(SelectItem::Expr {
expr: e.clone(),
alias: None,
});
}
if let Some(r) = &u.returning {
for item in r {
sub.items.push(item.clone());
}
}
self.collect_select_reads(&sub, &mut reads);
if let Some(r) = reads.get(&u.table)
&& !r.all
&& r.cols.is_empty()
{
reads.remove(&u.table);
}
}
Statement::Delete(d) => {
self.acl_require(&d.table, priv_bits::DELETE)?;
let mut sub = SelectStatement::default();
sub.from = Some(spg_sql::ast::FromClause {
primary: bare_table_ref(d.table.clone()),
joins: alloc::vec::Vec::new(),
});
sub.where_ = d.where_.clone();
if let Some(r) = &d.returning {
for item in r {
sub.items.push(item.clone());
}
}
self.collect_select_reads(&sub, &mut reads);
if let Some(r) = reads.get(&d.table)
&& !r.all
&& r.cols.is_empty()
{
reads.remove(&d.table);
}
}
Statement::Truncate { tables, .. } => {
for t in tables {
self.acl_require(t, priv_bits::TRUNCATE)?;
}
}
Statement::DropTable { names, .. } => {
for n in names {
self.acl_require_owner(n)?;
}
}
Statement::AlterTable(a) => self.acl_require_owner(&a.name)?,
Statement::CreateIndex(c) => self.acl_require_owner(&c.table)?,
Statement::CreateTable(_)
| Statement::CreateSequence(_)
| Statement::CreateView(_)
| Statement::CreateMaterializedView(_)
| Statement::CreateType(_) => self.acl_require_schema_create()?,
_ => {}
}
for (t, read) in &reads {
self.acl_require_read(t, read)?;
}
Ok(())
}
pub(crate) fn acl_check_select(&self, s: &SelectStatement) -> Result<(), EngineError> {
self.acl_check_select_as(s, None)
}
pub(crate) fn acl_check_select_as(
&self,
s: &SelectStatement,
as_role: Option<&str>,
) -> Result<(), EngineError> {
let superuser = match as_role {
Some(r) => self.role_is_superuser(r),
None => self.is_superuser(),
};
if superuser {
return Ok(());
}
let mut reads: alloc::collections::BTreeMap<String, ColRead> =
alloc::collections::BTreeMap::new();
self.collect_select_reads(s, &mut reads);
for (t, read) in &reads {
self.acl_require_read_as(t, read, as_role)?;
}
Ok(())
}
pub(crate) fn exec_grant(
&mut self,
g: &GrantStatement,
grant: bool,
) -> Result<crate::QueryResult, EngineError> {
if let GrantObject::Roles(roles) = &g.object {
return self.exec_role_membership(roles, &g.grantees, grant);
}
if let GrantObject::Sequences(_) | GrantObject::Schemas(_) | GrantObject::Databases(_) =
&g.object
{
return self.exec_grant_non_table(g, grant);
}
if let GrantObject::Functions(_) | GrantObject::AllTablesInSchema = &g.object {
return self.exec_grant_functions_or_all_tables(g, grant);
}
let GrantObject::Tables(tables) = &g.object else {
return Ok(crate::QueryResult::CommandOk {
affected: 0,
modified_catalog: false,
});
};
let mut table_mask = 0u16;
let mut column_masks: alloc::vec::Vec<(u16, &[String])> = alloc::vec::Vec::new();
if g.privileges.is_empty() {
table_mask = priv_bits::ALL;
} else {
for p in &g.privileges {
let bit = if p.word.eq_ignore_ascii_case("ALL") {
priv_bits::ALL
} else {
priv_from_word(&p.word).ok_or_else(|| {
EngineError::Unsupported(alloc::format!(
"unrecognized privilege type \"{}\"",
p.word.to_ascii_lowercase()
))
})?
};
if p.columns.is_empty() {
table_mask |= bit;
} else {
column_masks.push((bit, &p.columns));
}
}
}
for t in tables {
let Some(tb) = self.active_catalog().get(t) else {
return Err(EngineError::Unsupported(alloc::format!(
"relation \"{t}\" does not exist"
)));
};
for (_, cols) in &column_masks {
for c in *cols {
if !tb
.schema()
.columns
.iter()
.any(|sc| sc.name.eq_ignore_ascii_case(c))
{
return Err(EngineError::Unsupported(alloc::format!(
"column \"{c}\" of relation \"{t}\" does not exist"
)));
}
}
}
}
for r in &g.grantees {
self.acl_check_role_exists(r)?;
}
for t in tables {
if table_mask != 0 {
self.acl_apply(t, table_mask, &g.grantees, grant, g.grant_option)?;
}
for (bit, cols) in &column_masks {
self.acl_apply_columns(t, *bit, cols, &g.grantees, grant, g.grant_option)?;
}
}
Ok(crate::QueryResult::CommandOk {
affected: 0,
modified_catalog: true,
})
}
}
impl Engine {
pub(crate) fn acl_require_read(&self, table: &str, read: &ColRead) -> Result<(), EngineError> {
self.acl_require_read_as(table, read, None)
}
pub(crate) fn acl_require_read_as(
&self,
table: &str,
read: &ColRead,
as_role: Option<&str>,
) -> Result<(), EngineError> {
if self.acl_holds_as(table, priv_bits::SELECT, as_role) {
return Ok(());
}
let denied =
|| EngineError::Unsupported(alloc::format!("permission denied for table {table}"));
let Some(t) = self.active_catalog().get(table) else {
return Ok(());
};
let roles = self
.users
.effective_roles(as_role.unwrap_or_else(|| self.current_role()));
let cols = &t.schema().columns;
if read.all {
return if cols
.iter()
.all(|c| column_privs(c, &roles) & priv_bits::SELECT != 0)
&& !cols.is_empty()
{
Ok(())
} else {
Err(denied())
};
}
if read.cols.is_empty() {
return if cols
.iter()
.any(|c| column_privs(c, &roles) & priv_bits::SELECT != 0)
{
Ok(())
} else {
Err(denied())
};
}
for name in &read.cols {
let Some(c) = cols.iter().find(|c| c.name.eq_ignore_ascii_case(name)) else {
continue;
};
if column_privs(c, &roles) & priv_bits::SELECT == 0 {
return Err(denied());
}
}
Ok(())
}
pub(crate) fn acl_require_write_columns(
&self,
table: &str,
columns: &[String],
wanted: u16,
) -> Result<(), EngineError> {
if self.acl_holds(table, wanted) {
return Ok(());
}
let Some(t) = self.active_catalog().get(table) else {
return Ok(());
};
let roles = self.users.effective_roles(self.current_role());
if columns.is_empty() {
return Err(EngineError::Unsupported(alloc::format!(
"permission denied for table {table}"
)));
}
for name in columns {
let Some(c) = t
.schema()
.columns
.iter()
.find(|c| c.name.eq_ignore_ascii_case(name))
else {
continue;
};
if column_privs(c, &roles) & wanted == 0 {
return Err(EngineError::Unsupported(alloc::format!(
"permission denied for table {table}"
)));
}
}
Ok(())
}
pub(crate) fn collect_select_reads(
&self,
stmt: &SelectStatement,
into: &mut alloc::collections::BTreeMap<String, ColRead>,
) {
let cat = self.active_catalog();
let mut bases: alloc::vec::Vec<(String, Option<String>)> = alloc::vec::Vec::new();
let mut note = |t: &TableRef,
into: &mut alloc::collections::BTreeMap<String, ColRead>,
this: &Self| {
if let Some(sub) = &t.lateral_subquery {
this.collect_select_reads(sub, into);
return None;
}
if t.unnest_expr.is_some()
|| t.generate_series_args.is_some()
|| t.jsonb_each_text_arg.is_some()
|| t.table_fn_call.is_some()
{
return None;
}
into.entry(t.name.clone()).or_default();
Some((t.name.clone(), t.alias.clone()))
};
if let Some(from) = &stmt.from {
if let Some(b) = note(&from.primary, into, self) {
bases.push(b);
}
for j in &from.joins {
if let Some(b) = note(&j.table, into, self) {
bases.push(b);
}
}
}
let owns = |table: &str, col: &str| -> bool {
cat.get(table).is_some_and(|t| {
t.schema()
.columns
.iter()
.any(|c| c.name.eq_ignore_ascii_case(col))
})
};
let mut add_col =
|c: &ColumnName, into: &mut alloc::collections::BTreeMap<String, ColRead>| {
match &c.qualifier {
Some(q) => {
let target = bases.iter().find(|(t, a)| {
a.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(q))
|| t.eq_ignore_ascii_case(q)
});
if let Some((t, _)) = target {
into.entry(t.clone())
.or_default()
.cols
.insert(c.name.clone());
}
}
None => {
for (t, _) in &bases {
if owns(t, &c.name) {
into.entry(t.clone())
.or_default()
.cols
.insert(c.name.clone());
}
}
}
}
};
if stmt.items.iter().any(|i| matches!(i, SelectItem::Wildcard)) {
for (t, _) in &bases {
into.entry(t.clone()).or_default().all = true;
}
}
let mut walk = |e: &Expr, into: &mut alloc::collections::BTreeMap<String, ColRead>| {
self.walk_expr_reads(e, &mut add_col, into);
};
for item in &stmt.items {
if let SelectItem::Expr { expr, .. } = item {
walk(expr, into);
}
}
if let Some(w) = &stmt.where_ {
walk(w, into);
}
if let Some(h) = &stmt.having {
walk(h, into);
}
if let Some(gs) = &stmt.group_by {
for g in gs {
walk(g, into);
}
}
for o in &stmt.order_by {
walk(&o.expr, into);
}
if let Some(from) = &stmt.from {
for j in &from.joins {
if let Some(on) = &j.on {
walk(on, into);
}
}
}
for (_, peer) in &stmt.unions {
self.collect_select_reads(peer, into);
}
for cte in &stmt.ctes {
if let Some(s) = cte.body.as_select() {
self.collect_select_reads(s, into);
}
}
}
fn walk_expr_reads(
&self,
e: &Expr,
add_col: &mut impl FnMut(&ColumnName, &mut alloc::collections::BTreeMap<String, ColRead>),
into: &mut alloc::collections::BTreeMap<String, ColRead>,
) {
match e {
Expr::Column(c) => add_col(c, into),
Expr::ScalarSubquery(s) => self.collect_select_reads(s, into),
Expr::Exists { subquery, .. } => self.collect_select_reads(subquery, into),
Expr::InSubquery { expr, subquery, .. } => {
self.walk_expr_reads(expr, add_col, into);
self.collect_select_reads(subquery, into);
}
Expr::RowInSubquery { row, subquery, .. }
| Expr::RowCmpSubquery { row, subquery, .. } => {
for x in row {
self.walk_expr_reads(x, add_col, into);
}
self.collect_select_reads(subquery, into);
}
Expr::Binary { lhs, rhs, .. } => {
self.walk_expr_reads(lhs, add_col, into);
self.walk_expr_reads(rhs, add_col, into);
}
Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
self.walk_expr_reads(expr, add_col, into);
}
Expr::FunctionCall { args, .. } => {
for a in args {
self.walk_expr_reads(a, add_col, into);
}
}
Expr::Case {
operand,
branches,
else_branch,
} => {
if let Some(o) = operand {
self.walk_expr_reads(o, add_col, into);
}
for (c, v) in branches {
self.walk_expr_reads(c, add_col, into);
self.walk_expr_reads(v, add_col, into);
}
if let Some(x) = else_branch {
self.walk_expr_reads(x, add_col, into);
}
}
Expr::InList { expr, list, .. } => {
self.walk_expr_reads(expr, add_col, into);
for it in list {
self.walk_expr_reads(it, add_col, into);
}
}
Expr::AnyAll { expr, array, .. } => {
self.walk_expr_reads(expr, add_col, into);
self.walk_expr_reads(array, add_col, into);
}
Expr::Array(items) => {
for it in items {
self.walk_expr_reads(it, add_col, into);
}
}
Expr::ArraySubscript { target, index } => {
self.walk_expr_reads(target, add_col, into);
self.walk_expr_reads(index, add_col, into);
}
_ => {}
}
}
}
fn bare_table_ref(name: String) -> TableRef {
TableRef {
name,
alias: None,
only: false,
as_of_segment: None,
unnest_expr: None,
unnest_column_aliases: Vec::new(),
with_ordinality: false,
generate_series_args: None,
lateral_subquery: None,
jsonb_each_text_arg: None,
table_fn_call: None,
scalar_fn_item: false,
rows_from: None,
json_table: None,
}
}
pub(crate) const SCHEMA_OWNER_ROLE: &str = "pg_database_owner";
const DEFAULT_SCHEMA_PUBLIC: u16 = priv_bits::USAGE;
const DEFAULT_DATABASE_PUBLIC: u16 = priv_bits::CONNECT | priv_bits::TEMPORARY;
impl Engine {
pub(crate) fn schema_privs(&self, roles: &alloc::collections::BTreeSet<String>) -> u16 {
catalog_object_privs(self.active_catalog(), true, roles)
}
#[allow(dead_code)]
pub(crate) fn database_privs(&self, roles: &alloc::collections::BTreeSet<String>) -> u16 {
catalog_object_privs(self.active_catalog(), false, roles)
}
pub(crate) fn sequence_privs(
&self,
seq: &str,
roles: &alloc::collections::BTreeSet<String>,
) -> u16 {
let Some(s) = self.active_catalog().sequence(seq) else {
return 0;
};
let owner = s.owner.as_deref().unwrap_or(crate::session::LOGIN_ROLE);
if roles.iter().any(|r| r.eq_ignore_ascii_case(owner)) {
return priv_bits::ALL_SEQUENCE;
}
let mut held = 0;
for a in &s.acl {
if a.grantee.is_empty() || roles.iter().any(|r| a.grantee.eq_ignore_ascii_case(r)) {
held |= a.privs;
}
}
held
}
pub(crate) fn acl_require_sequence(&self, seq: &str, wanted: u16) -> Result<(), EngineError> {
if self.is_superuser() {
return Ok(());
}
if self.active_catalog().sequence(seq).is_none() {
return Ok(());
}
let roles = self.users.effective_roles(self.current_role());
if self.sequence_privs(seq, &roles) & wanted != 0 {
Ok(())
} else {
Err(EngineError::Unsupported(alloc::format!(
"permission denied for sequence {seq}"
)))
}
}
pub(crate) fn acl_require_schema_create(&self) -> Result<(), EngineError> {
if self.is_superuser() {
return Ok(());
}
let roles = self.users.effective_roles(self.current_role());
if self.schema_privs(&roles) & priv_bits::CREATE != 0 {
Ok(())
} else {
Err(EngineError::Unsupported(
"permission denied for schema public".into(),
))
}
}
pub(crate) fn acl_apply_catalog(
&mut self,
on_schema: bool,
mask: u16,
grantees: &[String],
grant: bool,
grant_option: bool,
) -> Result<(), EngineError> {
let owner = if on_schema {
alloc::string::String::from(SCHEMA_OWNER_ROLE)
} else {
alloc::string::String::from(self.current_role())
};
let (default_public, owner_all) = if on_schema {
(DEFAULT_SCHEMA_PUBLIC, priv_bits::ALL_SCHEMA)
} else {
(DEFAULT_DATABASE_PUBLIC, priv_bits::ALL_DATABASE)
};
let cat = self.active_catalog_mut();
let acl = if on_schema {
cat.schema_acl_mut()
} else {
cat.database_acl_mut()
};
if acl.is_empty() {
acl.push(AclItem {
grantee: owner.clone(),
privs: owner_all,
grantable: 0,
grantor: owner.clone(),
});
acl.push(AclItem {
grantee: String::new(),
privs: default_public,
grantable: 0,
grantor: owner.clone(),
});
}
for g in grantees {
let at = acl.iter().position(|a| a.grantee.eq_ignore_ascii_case(g));
if grant {
match at {
Some(i) => {
acl[i].privs |= mask;
if grant_option {
acl[i].grantable |= mask;
}
}
None => acl.push(AclItem {
grantee: g.clone(),
privs: mask,
grantable: if grant_option { mask } else { 0 },
grantor: owner.clone(),
}),
}
} else if let Some(i) = at {
if grant_option {
acl[i].grantable &= !mask;
} else {
acl[i].privs &= !mask;
acl[i].grantable &= !mask;
}
if acl[i].privs == 0 && !acl[i].grantee.eq_ignore_ascii_case(&owner) {
acl.remove(i);
}
}
}
Ok(())
}
pub(crate) fn acl_apply_sequence(
&mut self,
seq: &str,
mask: u16,
grantees: &[String],
grant: bool,
grant_option: bool,
) -> Result<(), EngineError> {
let grantor = alloc::string::String::from(self.current_role());
let owner = self
.active_catalog()
.sequences_all()
.get(seq)
.and_then(|s| s.owner.clone())
.unwrap_or_else(|| alloc::string::String::from(crate::session::LOGIN_ROLE));
let cat = self.active_catalog_mut();
let s = cat.sequence_mut(seq).ok_or_else(|| {
EngineError::Unsupported(alloc::format!("relation \"{seq}\" does not exist"))
})?;
if s.acl.is_empty() {
if !grant {
return Ok(());
}
s.acl.push(AclItem {
grantee: owner.clone(),
privs: priv_bits::ALL_SEQUENCE,
grantable: 0,
grantor: owner,
});
}
for g in grantees {
let at = s.acl.iter().position(|a| a.grantee.eq_ignore_ascii_case(g));
if grant {
match at {
Some(i) => {
s.acl[i].privs |= mask;
if grant_option {
s.acl[i].grantable |= mask;
}
}
None => s.acl.push(AclItem {
grantee: g.clone(),
privs: mask,
grantable: if grant_option { mask } else { 0 },
grantor: grantor.clone(),
}),
}
} else if let Some(i) = at
&& !s.acl[i].grantee.is_empty()
{
s.acl[i].privs &= !mask;
s.acl[i].grantable &= !mask;
if s.acl[i].privs == 0 {
s.acl.remove(i);
}
}
}
Ok(())
}
}
pub(crate) fn function_arg_count(args_repr: &str) -> usize {
let inner = args_repr
.trim()
.trim_start_matches('(')
.trim_end_matches(')');
if inner.trim().is_empty() {
0
} else {
inner.split(',').count()
}
}
pub(crate) fn function_privs(
def: &spg_storage::FunctionDef,
roles: &alloc::collections::BTreeSet<String>,
) -> u16 {
if def.acl.is_empty() {
return priv_bits::EXECUTE;
}
let owner = def.owner.as_deref().unwrap_or(crate::session::LOGIN_ROLE);
if roles.iter().any(|r| r.eq_ignore_ascii_case(owner)) {
return priv_bits::ALL_FUNCTION;
}
let mut held = 0;
for a in &def.acl {
if a.grantee.is_empty() || roles.iter().any(|r| a.grantee.eq_ignore_ascii_case(r)) {
held |= a.privs;
}
}
held
}
impl Engine {
pub(crate) fn acl_apply_function(
&mut self,
name: &str,
mask: u16,
grantees: &[String],
grant: bool,
grant_option: bool,
) -> Result<(), EngineError> {
let grantor = alloc::string::String::from(self.current_role());
let owner = self
.active_catalog()
.functions()
.get(name)
.and_then(|f| f.owner.clone())
.unwrap_or_else(|| alloc::string::String::from(crate::session::LOGIN_ROLE));
let cat = self.active_catalog_mut();
let f = cat.function_mut(name).ok_or_else(|| {
EngineError::Unsupported(alloc::format!("function {name} does not exist"))
})?;
if f.acl.is_empty() {
f.acl.push(AclItem {
grantee: owner.clone(),
privs: priv_bits::EXECUTE,
grantable: 0,
grantor: owner.clone(),
});
f.acl.push(AclItem {
grantee: String::new(),
privs: priv_bits::EXECUTE,
grantable: 0,
grantor: owner.clone(),
});
}
for g in grantees {
let at = f.acl.iter().position(|a| a.grantee.eq_ignore_ascii_case(g));
if grant {
match at {
Some(i) => {
f.acl[i].privs |= mask;
if grant_option {
f.acl[i].grantable |= mask;
}
}
None => f.acl.push(AclItem {
grantee: g.clone(),
privs: mask,
grantable: if grant_option { mask } else { 0 },
grantor: grantor.clone(),
}),
}
} else if let Some(i) = at {
f.acl[i].privs &= !mask;
f.acl[i].grantable &= !mask;
if f.acl[i].privs == 0 && !f.acl[i].grantee.eq_ignore_ascii_case(&owner) {
f.acl.remove(i);
}
}
}
Ok(())
}
}