uqa-sql 0.3.6

PostgreSQL-compatible SQL compiler built on libpg_query
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Routine execution authorization, owner transitions, and grant-option reachability.

use super::{registration::RoutineSupportAuthority, routine_kind, routine_local_name};
use crate::{
    ast::{
        AlterRoutineOwnerStmt, AlterRoutineStmt, CreateFunction, GrantRoutineStmt, RoutineAclEntry,
    },
    catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey},
    SQLError,
};
use std::collections::{BTreeMap, BTreeSet};

pub trait RoutineExecutionAuthority: RoutineSupportAuthority {
    fn current_user_name(&self) -> String;
    fn current_user_has_role_privileges(&self, role: &str) -> bool;
}

pub fn routine_owner_identity(stmt: &AlterRoutineOwnerStmt) -> AlterRoutineStmt {
    AlterRoutineStmt {
        kind: stmt.kind,
        name: stmt.name.clone(),
        arg_types: stmt.arg_types.clone(),
        arg_type_references: stmt.arg_type_references.clone(),
        volatility: None,
        strict: None,
        security_definer: None,
        leakproof: None,
        parallel: None,
        support: None,
        config_actions: Vec::new(),
    }
}

pub fn ensure_routine_execute_privilege(
    authority: &dyn RoutineExecutionAuthority,
    definition: &CreateFunction,
) -> Result<(), SQLError> {
    ensure_routine_execute_privilege_named(
        authority,
        definition,
        &routine_local_name(&definition.name)?,
    )
}

pub fn ensure_routine_execute_privilege_named(
    authority: &dyn RoutineExecutionAuthority,
    definition: &CreateFunction,
    display_name: &str,
) -> Result<(), SQLError> {
    let current = authority.current_user_name();
    let allowed = authority.current_user_is_superuser()
        || authority.current_user_has_role_privileges(&definition.owner)
        || definition.execute_acl.as_ref().is_none_or(|acl| {
            acl.iter().any(|entry| {
                entry.role == "PUBLIC"
                    || entry.role == current
                    || authority.current_user_has_role_privileges(&entry.role)
            })
        });
    if allowed {
        Ok(())
    } else {
        Err(SQLError::Routine {
            sqlstate: "42501".into(),
            message: format!(
                "permission denied for {} {}",
                routine_kind(definition),
                display_name
            ),
        })
    }
}

pub fn validate_routine_acl_roles(
    stmt: &GrantRoutineStmt,
    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 stmt.is_grant && stmt.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(())
}

fn routine_acl_grantor<'a>(entry: &'a RoutineAclEntry, owner: &'a str) -> &'a str {
    entry.grantor.as_deref().unwrap_or(owner)
}

fn materialize_routine_acl(definition: &mut CreateFunction) -> &mut Vec<RoutineAclEntry> {
    if definition.execute_acl.is_none() {
        definition.execute_acl = Some(vec![RoutineAclEntry {
            role: "PUBLIC".into(),
            grantor: Some(definition.owner.clone()),
            grant_option: false,
        }]);
    }
    definition
        .execute_acl
        .as_mut()
        .expect("routine ACL was materialized")
}

fn routine_grant_option_roles(definition: &CreateFunction) -> BTreeSet<String> {
    routine_grant_option_roles_for(&definition.owner, definition.execute_acl.as_deref())
}

fn routine_grant_option_roles_for(
    owner: &str,
    acl: Option<&[RoutineAclEntry]>,
) -> BTreeSet<String> {
    let mut reachable = BTreeSet::from([owner.to_string()]);
    let Some(acl) = acl else {
        return reachable;
    };
    loop {
        let mut changed = false;
        for entry in acl {
            if entry.role != "PUBLIC"
                && entry.grant_option
                && reachable.contains(routine_acl_grantor(entry, owner))
            {
                changed |= reachable.insert(entry.role.clone());
            }
        }
        if !changed {
            return reachable;
        }
    }
}

pub fn select_routine_acl_grantor(
    definition: &CreateFunction,
    current_user: &str,
    roles: &BTreeMap<String, RoleDefinition>,
    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
) -> Option<String> {
    if role_inherits(roles, memberships, current_user, &definition.owner) {
        return Some(definition.owner.clone());
    }
    let grant_options = routine_grant_option_roles(definition);
    if grant_options.contains(current_user) {
        return Some(current_user.to_string());
    }
    definition.execute_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 grant_routine_acl(
    definition: &mut CreateFunction,
    grantee: &str,
    grantor: &str,
    grant_option: bool,
) {
    if grantee == definition.owner {
        return;
    }
    if definition.execute_acl.is_none()
        && grantee == "PUBLIC"
        && grantor == definition.owner
        && !grant_option
    {
        return;
    }
    let owner = definition.owner.clone();
    let acl = materialize_routine_acl(definition);
    if let Some(entry) = acl
        .iter_mut()
        .find(|entry| entry.role == grantee && routine_acl_grantor(entry, &owner) == grantor)
    {
        entry.grant_option |= grant_option;
    } else {
        acl.push(RoutineAclEntry {
            role: grantee.to_string(),
            grantor: Some(grantor.to_string()),
            grant_option,
        });
    }
}

pub fn revoke_routine_acl(
    definition: &mut CreateFunction,
    grantee: &str,
    grantor: &str,
    grant_option_only: bool,
    cascade: bool,
) -> Result<bool, SQLError> {
    if grantee == definition.owner {
        return Ok(false);
    }
    let owner = definition.owner.clone();
    let before_grant_options = routine_grant_option_roles(definition);
    let acl = materialize_routine_acl(definition);
    let Some(position) = acl
        .iter()
        .position(|entry| entry.role == grantee && routine_acl_grantor(entry, &owner) == grantor)
    else {
        return Ok(false);
    };
    if grant_option_only {
        if !acl[position].grant_option {
            return Ok(false);
        }
        acl[position].grant_option = false;
    } else {
        acl.remove(position);
    }
    revoke_dependent_routine_acl(definition, &before_grant_options, cascade)?;
    Ok(true)
}

fn revoke_dependent_routine_acl(
    definition: &mut CreateFunction,
    before_grant_options: &BTreeSet<String>,
    cascade: bool,
) -> Result<(), SQLError> {
    loop {
        let current_grant_options = routine_grant_option_roles(definition);
        let lost = before_grant_options
            .difference(&current_grant_options)
            .cloned()
            .collect::<BTreeSet<_>>();
        if lost.is_empty() {
            return Ok(());
        }
        let owner = definition.owner.clone();
        let dependent_exists = definition.execute_acl.as_ref().is_some_and(|acl| {
            acl.iter()
                .any(|entry| lost.contains(routine_acl_grantor(entry, &owner)))
        });
        if !dependent_exists {
            return Ok(());
        }
        if !cascade {
            return Err(SQLError::Routine {
                sqlstate: "2BP01".into(),
                message: "dependent privileges exist".into(),
            });
        }
        definition
            .execute_acl
            .as_mut()
            .expect("dependent ACLs require an explicit ACL")
            .retain(|entry| !lost.contains(routine_acl_grantor(entry, &owner)));
    }
}

pub fn rewrite_routine_acl_owner(
    definition: &mut CreateFunction,
    old_owner: &str,
    new_owner: &str,
) {
    let Some(acl) = definition.execute_acl.as_mut() else {
        return;
    };
    for entry in acl.iter_mut() {
        if entry.role == old_owner {
            entry.role = new_owner.to_string();
        }
        if entry.grantor.as_deref() == Some(old_owner) {
            entry.grantor = Some(new_owner.to_string());
        }
    }
    let mut merged: Vec<RoutineAclEntry> = Vec::with_capacity(acl.len());
    for entry in std::mem::take(acl) {
        if let Some(existing) = merged.iter_mut().find(|existing| {
            existing.role == entry.role
                && routine_acl_grantor(existing, new_owner)
                    == routine_acl_grantor(&entry, new_owner)
        }) {
            existing.grant_option |= entry.grant_option;
        } else {
            merged.push(entry);
        }
    }
    *acl = merged;
}

pub fn routine_acl_warning(is_grant: bool, name: &str) -> (&'static str, String) {
    let local_name = name.rsplit('.').next().unwrap_or(name);
    (
        "WARNING",
        if is_grant {
            format!("no privileges were granted for \"{local_name}\"")
        } else {
            format!("no privileges could be revoked for \"{local_name}\"")
        },
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    fn grant(grantee: &str, grantor: &str) -> RoutineAclEntry {
        RoutineAclEntry {
            role: grantee.into(),
            grantor: Some(grantor.into()),
            grant_option: true,
        }
    }

    #[test]
    fn routine_grant_option_reachability_requires_an_owner_root() {
        let disconnected_cycle = [grant("delegate", "leaf"), grant("leaf", "delegate")];
        assert_eq!(
            routine_grant_option_roles_for("owner", Some(&disconnected_cycle)),
            BTreeSet::from(["owner".into()])
        );

        let rooted_cycle = [
            grant("delegate", "owner"),
            grant("leaf", "delegate"),
            grant("delegate", "leaf"),
        ];
        assert_eq!(
            routine_grant_option_roles_for("owner", Some(&rooted_cycle)),
            BTreeSet::from(["delegate".into(), "leaf".into(), "owner".into()])
        );
    }

    #[test]
    fn routine_grant_option_reachability_accepts_an_independent_owner_path() {
        let acl = [
            grant("delegate", "owner"),
            grant("leaf", "delegate"),
            grant("leaf", "owner"),
            grant("tail", "leaf"),
        ];
        assert_eq!(
            routine_grant_option_roles_for("owner", Some(&acl[2..])),
            BTreeSet::from(["leaf".into(), "owner".into(), "tail".into()])
        );
    }
}