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