uqa_sql/routines/
privilege_inquiry.rs1use 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;