Skip to main content

uqa_sql/routines/
privilege_inquiry.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Function privilege inquiry semantics with strict name resolution and nullable OID lookup.
8
9use super::security::routine_privilege_allowed;
10use crate::catalog::roles::{identity::RoleSubject, RoleReference};
11use crate::{
12    ast::{RoleAttribute, RoutineAclEntry},
13    catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey},
14    SQLError,
15};
16use std::collections::BTreeMap;
17use uqa_core::Value;
18
19pub struct RoutinePrivileges<'a> {
20    pub owner: crate::catalog::roles::RoleIdentity,
21    pub execute_acl: Option<&'a [RoutineAclEntry]>,
22}
23
24pub trait RoutinePrivilegeCatalog {
25    fn resolve_routine_name(&self, name: &str) -> Result<i64, SQLError>;
26    fn routine_privileges(&self, oid: i64) -> Result<Option<RoutinePrivileges<'_>>, SQLError>;
27}
28
29pub struct RoutinePrivilegeInquiry<'a> {
30    pub current_user: &'a RoleReference,
31    pub roles: &'a BTreeMap<String, RoleDefinition>,
32    pub memberships: &'a BTreeMap<RoleMembershipKey, RoleMembership>,
33    pub catalog: &'a dyn RoutinePrivilegeCatalog,
34}
35
36impl RoutinePrivilegeInquiry<'_> {
37    pub fn has_function_privilege_value(&self, arguments: &[Value]) -> Result<Value, SQLError> {
38        if arguments.contains(&Value::Null) {
39            return Ok(Value::Null);
40        }
41        let (subject, target, privilege) = match arguments {
42            [target, privilege] => (Some(self.current_user.clone()), target, privilege),
43            [subject, target, privilege] => (self.resolve_role(subject)?, target, privilege),
44            _ => {
45                return Err(SQLError::BadArity {
46                    name: "has_function_privilege".into(),
47                    expected: "2 or 3".into(),
48                    actual: arguments.len(),
49                })
50            }
51        };
52        let (oid, missing_is_null) = match target {
53            Value::Str(name) | Value::FixedChar(name) => {
54                let oid = self.catalog.resolve_routine_name(name)?;
55                if oid == 0 {
56                    return Err(SQLError::Routine {
57                        sqlstate: "42883".into(),
58                        message: format!("function \"{name}\" does not exist"),
59                    });
60                }
61                (oid, false)
62            }
63            Value::Int(oid) => (*oid, true),
64            other => {
65                return Err(SQLError::TypeMismatch(format!(
66                    "has_function_privilege function must be text or oid, got {other:?}"
67                )))
68            }
69        };
70        let checks = match privilege {
71            Value::Str(value) | Value::FixedChar(value) => parse_privileges(value)?,
72            other => {
73                return Err(SQLError::TypeMismatch(format!(
74                    "has_function_privilege privilege must be text, got {other:?}"
75                )))
76            }
77        };
78        if subject
79            .as_ref()
80            .and_then(|subject| subject.role_definition(self.roles))
81            .is_some_and(|role| role.attributes.contains(&RoleAttribute::Superuser))
82        {
83            return Ok(Value::Bool(true));
84        }
85        let Some(security) = self.catalog.routine_privileges(oid)? else {
86            return if missing_is_null {
87                Ok(Value::Null)
88            } else {
89                Err(SQLError::Internal(format!(
90                    "cache lookup failed for function {oid}"
91                )))
92            };
93        };
94        Ok(Value::Bool(checks.into_iter().any(|grant_option| {
95            routine_privilege_allowed(
96                &security.owner,
97                security.execute_acl,
98                grant_option,
99                false,
100                |role| {
101                    subject.as_ref().is_some_and(|subject| {
102                        role_inherits(self.roles, self.memberships, subject, role)
103                    })
104                },
105            )
106        })))
107    }
108
109    fn resolve_role(&self, value: &Value) -> Result<Option<RoleReference>, SQLError> {
110        match value {
111            Value::Str(name) | Value::FixedChar(name) if name == "public" => Ok(None),
112            Value::Str(name) | Value::FixedChar(name) => {
113                if self.roles.contains_key(name) {
114                    Ok(Some(name.clone().into()))
115                } else {
116                    Err(SQLError::Routine {
117                        sqlstate: "42704".into(),
118                        message: format!("role \"{name}\" does not exist"),
119                    })
120                }
121            }
122            Value::Int(oid) => Ok(self
123                .roles
124                .values()
125                .find(|role| role.oid == *oid)
126                .map(|role| role.name.clone().into())),
127            other => Err(SQLError::TypeMismatch(format!(
128                "has_function_privilege role must be name or oid, got {other:?}"
129            ))),
130        }
131    }
132}
133
134fn parse_privileges(value: &str) -> Result<Vec<bool>, SQLError> {
135    value
136        .split(',')
137        .map(|item| {
138            let item = item.trim_matches(|ch: char| ch.is_ascii_whitespace());
139            if item.eq_ignore_ascii_case("EXECUTE") {
140                Ok(false)
141            } else if item.eq_ignore_ascii_case("EXECUTE WITH GRANT OPTION") {
142                Ok(true)
143            } else {
144                Err(SQLError::Routine {
145                    sqlstate: "22023".into(),
146                    message: format!("unrecognized privilege type: \"{item}\""),
147                })
148            }
149        })
150        .collect()
151}
152
153#[cfg(test)]
154mod tests;