Skip to main content

uqa_sql/routines/
security.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Routine execution authorization, owner transitions, and grant-option reachability.
8
9use super::{registration::RoutineSupportAuthority, routine_kind, routine_local_name};
10use crate::{
11    ast::{
12        AlterRoutineOwnerStmt, AlterRoutineStmt, CreateFunction, GrantRoutineStmt, RoutineAclEntry,
13    },
14    catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey},
15    SQLError,
16};
17use std::collections::{BTreeMap, BTreeSet};
18
19pub trait RoutineExecutionAuthority: RoutineSupportAuthority {
20    fn current_user_name(&self) -> String;
21    fn current_user_has_role_privileges(&self, role: &str) -> bool;
22}
23
24pub fn routine_owner_identity(stmt: &AlterRoutineOwnerStmt) -> AlterRoutineStmt {
25    AlterRoutineStmt {
26        kind: stmt.kind,
27        name: stmt.name.clone(),
28        arg_types: stmt.arg_types.clone(),
29        arg_type_references: stmt.arg_type_references.clone(),
30        volatility: None,
31        strict: None,
32        security_definer: None,
33        leakproof: None,
34        parallel: None,
35        support: None,
36        config_actions: Vec::new(),
37    }
38}
39
40pub fn ensure_routine_execute_privilege(
41    authority: &dyn RoutineExecutionAuthority,
42    definition: &CreateFunction,
43) -> Result<(), SQLError> {
44    ensure_routine_execute_privilege_named(
45        authority,
46        definition,
47        &routine_local_name(&definition.name)?,
48    )
49}
50
51pub fn ensure_routine_execute_privilege_named(
52    authority: &dyn RoutineExecutionAuthority,
53    definition: &CreateFunction,
54    display_name: &str,
55) -> Result<(), SQLError> {
56    let current = authority.current_user_name();
57    let allowed = authority.current_user_is_superuser()
58        || authority.current_user_has_role_privileges(&definition.owner)
59        || definition.execute_acl.as_ref().is_none_or(|acl| {
60            acl.iter().any(|entry| {
61                entry.role == "PUBLIC"
62                    || entry.role == current
63                    || authority.current_user_has_role_privileges(&entry.role)
64            })
65        });
66    if allowed {
67        Ok(())
68    } else {
69        Err(SQLError::Routine {
70            sqlstate: "42501".into(),
71            message: format!(
72                "permission denied for {} {}",
73                routine_kind(definition),
74                display_name
75            ),
76        })
77    }
78}
79
80pub fn validate_routine_acl_roles(
81    stmt: &GrantRoutineStmt,
82    grantees: &[String],
83    requested_grantor: Option<&str>,
84    current_user: &str,
85    roles: &BTreeMap<String, RoleDefinition>,
86) -> Result<(), SQLError> {
87    for role in grantees {
88        if role != "PUBLIC" && !roles.contains_key(role) {
89            return Err(SQLError::Routine {
90                sqlstate: "42704".into(),
91                message: format!("role \"{role}\" does not exist"),
92            });
93        }
94    }
95    if stmt.is_grant && stmt.grant_option && grantees.iter().any(|role| role == "PUBLIC") {
96        return Err(SQLError::Routine {
97            sqlstate: "0LP01".into(),
98            message: "grant options can only be granted to roles".into(),
99        });
100    }
101    if let Some(requested_grantor) = requested_grantor {
102        if !roles.contains_key(requested_grantor) {
103            return Err(SQLError::Routine {
104                sqlstate: "42704".into(),
105                message: format!("role \"{requested_grantor}\" does not exist"),
106            });
107        }
108        if requested_grantor != current_user {
109            return Err(SQLError::Routine {
110                sqlstate: "0A000".into(),
111                message: "grantor must be current user".into(),
112            });
113        }
114    }
115    Ok(())
116}
117
118fn routine_acl_grantor<'a>(entry: &'a RoutineAclEntry, owner: &'a str) -> &'a str {
119    entry.grantor.as_deref().unwrap_or(owner)
120}
121
122fn materialize_routine_acl(definition: &mut CreateFunction) -> &mut Vec<RoutineAclEntry> {
123    if definition.execute_acl.is_none() {
124        definition.execute_acl = Some(vec![RoutineAclEntry {
125            role: "PUBLIC".into(),
126            grantor: Some(definition.owner.clone()),
127            grant_option: false,
128        }]);
129    }
130    definition
131        .execute_acl
132        .as_mut()
133        .expect("routine ACL was materialized")
134}
135
136fn routine_grant_option_roles(definition: &CreateFunction) -> BTreeSet<String> {
137    routine_grant_option_roles_for(&definition.owner, definition.execute_acl.as_deref())
138}
139
140fn routine_grant_option_roles_for(
141    owner: &str,
142    acl: Option<&[RoutineAclEntry]>,
143) -> BTreeSet<String> {
144    let mut reachable = BTreeSet::from([owner.to_string()]);
145    let Some(acl) = acl else {
146        return reachable;
147    };
148    loop {
149        let mut changed = false;
150        for entry in acl {
151            if entry.role != "PUBLIC"
152                && entry.grant_option
153                && reachable.contains(routine_acl_grantor(entry, owner))
154            {
155                changed |= reachable.insert(entry.role.clone());
156            }
157        }
158        if !changed {
159            return reachable;
160        }
161    }
162}
163
164pub fn select_routine_acl_grantor(
165    definition: &CreateFunction,
166    current_user: &str,
167    roles: &BTreeMap<String, RoleDefinition>,
168    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
169) -> Option<String> {
170    if role_inherits(roles, memberships, current_user, &definition.owner) {
171        return Some(definition.owner.clone());
172    }
173    let grant_options = routine_grant_option_roles(definition);
174    if grant_options.contains(current_user) {
175        return Some(current_user.to_string());
176    }
177    definition.execute_acl.as_ref().and_then(|acl| {
178        acl.iter()
179            .filter(|entry| entry.role != "PUBLIC" && grant_options.contains(&entry.role))
180            .find(|entry| role_inherits(roles, memberships, current_user, &entry.role))
181            .map(|entry| entry.role.clone())
182    })
183}
184
185pub fn grant_routine_acl(
186    definition: &mut CreateFunction,
187    grantee: &str,
188    grantor: &str,
189    grant_option: bool,
190) {
191    if grantee == definition.owner {
192        return;
193    }
194    if definition.execute_acl.is_none()
195        && grantee == "PUBLIC"
196        && grantor == definition.owner
197        && !grant_option
198    {
199        return;
200    }
201    let owner = definition.owner.clone();
202    let acl = materialize_routine_acl(definition);
203    if let Some(entry) = acl
204        .iter_mut()
205        .find(|entry| entry.role == grantee && routine_acl_grantor(entry, &owner) == grantor)
206    {
207        entry.grant_option |= grant_option;
208    } else {
209        acl.push(RoutineAclEntry {
210            role: grantee.to_string(),
211            grantor: Some(grantor.to_string()),
212            grant_option,
213        });
214    }
215}
216
217pub fn revoke_routine_acl(
218    definition: &mut CreateFunction,
219    grantee: &str,
220    grantor: &str,
221    grant_option_only: bool,
222    cascade: bool,
223) -> Result<bool, SQLError> {
224    if grantee == definition.owner {
225        return Ok(false);
226    }
227    let owner = definition.owner.clone();
228    let before_grant_options = routine_grant_option_roles(definition);
229    let acl = materialize_routine_acl(definition);
230    let Some(position) = acl
231        .iter()
232        .position(|entry| entry.role == grantee && routine_acl_grantor(entry, &owner) == grantor)
233    else {
234        return Ok(false);
235    };
236    if grant_option_only {
237        if !acl[position].grant_option {
238            return Ok(false);
239        }
240        acl[position].grant_option = false;
241    } else {
242        acl.remove(position);
243    }
244    revoke_dependent_routine_acl(definition, &before_grant_options, cascade)?;
245    Ok(true)
246}
247
248fn revoke_dependent_routine_acl(
249    definition: &mut CreateFunction,
250    before_grant_options: &BTreeSet<String>,
251    cascade: bool,
252) -> Result<(), SQLError> {
253    loop {
254        let current_grant_options = routine_grant_option_roles(definition);
255        let lost = before_grant_options
256            .difference(&current_grant_options)
257            .cloned()
258            .collect::<BTreeSet<_>>();
259        if lost.is_empty() {
260            return Ok(());
261        }
262        let owner = definition.owner.clone();
263        let dependent_exists = definition.execute_acl.as_ref().is_some_and(|acl| {
264            acl.iter()
265                .any(|entry| lost.contains(routine_acl_grantor(entry, &owner)))
266        });
267        if !dependent_exists {
268            return Ok(());
269        }
270        if !cascade {
271            return Err(SQLError::Routine {
272                sqlstate: "2BP01".into(),
273                message: "dependent privileges exist".into(),
274            });
275        }
276        definition
277            .execute_acl
278            .as_mut()
279            .expect("dependent ACLs require an explicit ACL")
280            .retain(|entry| !lost.contains(routine_acl_grantor(entry, &owner)));
281    }
282}
283
284pub fn rewrite_routine_acl_owner(
285    definition: &mut CreateFunction,
286    old_owner: &str,
287    new_owner: &str,
288) {
289    let Some(acl) = definition.execute_acl.as_mut() else {
290        return;
291    };
292    for entry in acl.iter_mut() {
293        if entry.role == old_owner {
294            entry.role = new_owner.to_string();
295        }
296        if entry.grantor.as_deref() == Some(old_owner) {
297            entry.grantor = Some(new_owner.to_string());
298        }
299    }
300    let mut merged: Vec<RoutineAclEntry> = Vec::with_capacity(acl.len());
301    for entry in std::mem::take(acl) {
302        if let Some(existing) = merged.iter_mut().find(|existing| {
303            existing.role == entry.role
304                && routine_acl_grantor(existing, new_owner)
305                    == routine_acl_grantor(&entry, new_owner)
306        }) {
307            existing.grant_option |= entry.grant_option;
308        } else {
309            merged.push(entry);
310        }
311    }
312    *acl = merged;
313}
314
315pub fn routine_acl_warning(is_grant: bool, name: &str) -> (&'static str, String) {
316    let local_name = name.rsplit('.').next().unwrap_or(name);
317    (
318        "WARNING",
319        if is_grant {
320            format!("no privileges were granted for \"{local_name}\"")
321        } else {
322            format!("no privileges could be revoked for \"{local_name}\"")
323        },
324    )
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    fn grant(grantee: &str, grantor: &str) -> RoutineAclEntry {
332        RoutineAclEntry {
333            role: grantee.into(),
334            grantor: Some(grantor.into()),
335            grant_option: true,
336        }
337    }
338
339    #[test]
340    fn routine_grant_option_reachability_requires_an_owner_root() {
341        let disconnected_cycle = [grant("delegate", "leaf"), grant("leaf", "delegate")];
342        assert_eq!(
343            routine_grant_option_roles_for("owner", Some(&disconnected_cycle)),
344            BTreeSet::from(["owner".into()])
345        );
346
347        let rooted_cycle = [
348            grant("delegate", "owner"),
349            grant("leaf", "delegate"),
350            grant("delegate", "leaf"),
351        ];
352        assert_eq!(
353            routine_grant_option_roles_for("owner", Some(&rooted_cycle)),
354            BTreeSet::from(["delegate".into(), "leaf".into(), "owner".into()])
355        );
356    }
357
358    #[test]
359    fn routine_grant_option_reachability_accepts_an_independent_owner_path() {
360        let acl = [
361            grant("delegate", "owner"),
362            grant("leaf", "delegate"),
363            grant("leaf", "owner"),
364            grant("tail", "leaf"),
365        ];
366        assert_eq!(
367            routine_grant_option_roles_for("owner", Some(&acl[2..])),
368            BTreeSet::from(["leaf".into(), "owner".into(), "tail".into()])
369        );
370    }
371}