Skip to main content

uqa_sql/catalog/security/
object_acl.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Check, grant and revoke the ACL of an object with one grantable privilege, as `PostgreSQL`'s `aclchk.c` does for routine `EXECUTE` and type `USAGE`. An absent ACL is the default ACL: the privilege for PUBLIC and for the owner. Owners always hold grant options.
8
9use crate::ast::ObjectAclEntry;
10use crate::catalog::roles::{
11    identity::RoleSubject, role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey,
12    RoleReference,
13};
14use crate::SQLError;
15use std::collections::{BTreeMap, BTreeSet};
16use uqa_core::{catalog_acl::AclGrantee, catalog_role::RoleIdentity};
17
18/// Whether a role holding `has_role` memberships may use the object, or grant it when `grant_option` is set.
19pub fn privilege_allowed(
20    owner: &RoleIdentity,
21    acl: Option<&[ObjectAclEntry]>,
22    grant_option: bool,
23    superuser: bool,
24    has_role: impl Fn(&RoleIdentity) -> bool,
25) -> bool {
26    if superuser || (grant_option && has_role(owner)) {
27        return true;
28    }
29    acl.map_or(!grant_option, |acl| {
30        acl.iter().any(|entry| {
31            (!grant_option || entry.grant_option)
32                && ((entry.role.is_none() && !grant_option)
33                    || entry.role.as_ref().is_some_and(&has_role))
34        })
35    })
36}
37
38/// Roles whose grant option is reachable from the owner through grant-option entries.
39pub fn grant_option_roles(
40    owner: RoleIdentity,
41    acl: Option<&[ObjectAclEntry]>,
42) -> BTreeSet<RoleIdentity> {
43    let mut reachable = BTreeSet::from([owner]);
44    let Some(acl) = acl else {
45        return reachable;
46    };
47    loop {
48        let mut changed = false;
49        for entry in acl {
50            if let Some(role) = entry.role {
51                if entry.grant_option && reachable.contains(&entry.grantor) {
52                    changed |= reachable.insert(role);
53                }
54            }
55        }
56        if !changed {
57            return reachable;
58        }
59    }
60}
61
62/// `select_best_grantor`: the owner when the current user inherits it, then the current user's own grant option, then an inherited role's grant option. `None` means the command grants or revokes nothing and warns.
63pub fn select_grantor(
64    owner: RoleIdentity,
65    acl: Option<&[ObjectAclEntry]>,
66    current_user: &(impl RoleSubject + ?Sized),
67    roles: &BTreeMap<String, RoleDefinition>,
68    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
69) -> Option<RoleIdentity> {
70    if role_inherits(roles, memberships, current_user, &owner) {
71        return Some(owner);
72    }
73    let grant_options = grant_option_roles(owner, acl);
74    if let Some(identity) = current_user
75        .role_definition(roles)
76        .map(RoleDefinition::identity)
77        .filter(|identity| grant_options.contains(identity))
78    {
79        return Some(identity);
80    }
81    acl.and_then(|acl| {
82        acl.iter()
83            .filter_map(|entry| entry.role)
84            .filter(|role| grant_options.contains(role))
85            .find(|role| role_inherits(roles, memberships, current_user, role))
86    })
87}
88
89fn materialize(
90    owner: RoleIdentity,
91    acl: &mut Option<Vec<ObjectAclEntry>>,
92) -> &mut Vec<ObjectAclEntry> {
93    acl.get_or_insert_with(|| {
94        vec![
95            ObjectAclEntry {
96                role: None,
97                grantor: owner,
98                grant_option: false,
99            },
100            ObjectAclEntry {
101                role: Some(owner),
102                grantor: owner,
103                grant_option: false,
104            },
105        ]
106    })
107}
108
109/// The ACL a GRANT or REVOKE stores: `ExecGrant_common` substitutes the default ACL for a missing one and always writes its result, so the default becomes explicit even when the command changes nothing.
110#[must_use]
111pub fn explicit_acl(
112    owner: RoleIdentity,
113    mut acl: Option<Vec<ObjectAclEntry>>,
114) -> Option<Vec<ObjectAclEntry>> {
115    materialize(owner, &mut acl);
116    acl
117}
118
119/// Add the privilege, merging the grant option into an existing entry from the same grantor.
120pub fn grant(
121    owner: RoleIdentity,
122    acl: &mut Option<Vec<ObjectAclEntry>>,
123    grantee: Option<RoleIdentity>,
124    grantor: RoleIdentity,
125    grant_option: bool,
126) {
127    let entries = materialize(owner, acl);
128    if let Some(entry) = entries
129        .iter_mut()
130        .find(|entry| entry.role == grantee && entry.grantor == grantor)
131    {
132        entry.grant_option |= grant_option;
133    } else {
134        entries.push(ObjectAclEntry {
135            role: grantee,
136            grantor,
137            grant_option,
138        });
139    }
140}
141
142/// Remove the privilege or only its grant option; revoking what the grantor did not grant changes nothing, and `ExecGrant_*` warns only when the grantor holds no grant option. Privileges granted through a lost grant option require CASCADE.
143pub fn revoke(
144    owner: RoleIdentity,
145    acl: &mut Option<Vec<ObjectAclEntry>>,
146    grantee: Option<RoleIdentity>,
147    grantor: RoleIdentity,
148    grant_option_only: bool,
149    cascade: bool,
150) -> Result<(), SQLError> {
151    let before = grant_option_roles(owner, acl.as_deref());
152    let entries = materialize(owner, acl);
153    let Some(position) = entries
154        .iter()
155        .position(|entry| entry.role == grantee && entry.grantor == grantor)
156    else {
157        return Ok(());
158    };
159    if grant_option_only {
160        if !entries[position].grant_option {
161            return Ok(());
162        }
163        entries[position].grant_option = false;
164    } else {
165        entries.remove(position);
166    }
167    revoke_dependents(owner, acl, &before, cascade)
168}
169
170fn revoke_dependents(
171    owner: RoleIdentity,
172    acl: &mut Option<Vec<ObjectAclEntry>>,
173    before: &BTreeSet<RoleIdentity>,
174    cascade: bool,
175) -> Result<(), SQLError> {
176    loop {
177        let current = grant_option_roles(owner, acl.as_deref());
178        let lost = before
179            .difference(&current)
180            .copied()
181            .collect::<BTreeSet<_>>();
182        let Some(entries) = acl.as_mut() else {
183            return Ok(());
184        };
185        if lost.is_empty() || !entries.iter().any(|entry| lost.contains(&entry.grantor)) {
186            return Ok(());
187        }
188        if !cascade {
189            return Err(SQLError::Diagnostic {
190                sqlstate: "2BP01".into(),
191                message: "dependent privileges exist".into(),
192                detail: None,
193                hint: Some("Use CASCADE to revoke them too.".into()),
194            });
195        }
196        entries.retain(|entry| !lost.contains(&entry.grantor));
197    }
198}
199
200/// `aclnewowner`: the new owner replaces the old one as grantee and grantor, and entries that become identical merge.
201pub fn rewrite_owner(
202    acl: &mut Option<Vec<ObjectAclEntry>>,
203    old_owner: RoleIdentity,
204    new_owner: RoleIdentity,
205) {
206    let Some(entries) = acl.as_mut() else {
207        return;
208    };
209    for entry in entries.iter_mut() {
210        if entry.role == Some(old_owner) {
211            entry.role = Some(new_owner);
212        }
213        if entry.grantor == old_owner {
214            entry.grantor = new_owner;
215        }
216    }
217    let mut merged: Vec<ObjectAclEntry> = Vec::with_capacity(entries.len());
218    for entry in std::mem::take(entries) {
219        if let Some(existing) = merged
220            .iter_mut()
221            .find(|existing| existing.role == entry.role && existing.grantor == entry.grantor)
222        {
223            existing.grant_option |= entry.grant_option;
224        } else {
225            merged.push(entry);
226        }
227    }
228    *entries = merged;
229}
230
231/// Validate the role identities and grant paths of an ACL: every endpoint names a role incarnation, PUBLIC holds no grant option, no grant path repeats, and every grantor's grant option is reachable from the owner.
232pub fn validate_acl(
233    owner: RoleIdentity,
234    acl: Option<&[ObjectAclEntry]>,
235    object_kind: &str,
236) -> Result<(), SQLError> {
237    let invalid =
238        |message: &str| SQLError::Internal(format!("invalid {object_kind} authority: {message}"));
239    if !owner.is_valid() {
240        return Err(invalid("missing owner incarnation"));
241    }
242    let reachable = grant_option_roles(owner, acl);
243    let mut paths = BTreeSet::new();
244    for entry in acl.into_iter().flatten() {
245        if !entry.grantor.is_valid() || entry.role.is_some_and(|role| !role.is_valid()) {
246            return Err(invalid("missing ACL endpoint incarnation"));
247        }
248        if entry.role.is_none() && entry.grant_option {
249            return Err(invalid("PUBLIC cannot retain a grant option"));
250        }
251        if !paths.insert((entry.role, entry.grantor)) {
252            return Err(invalid("duplicate ACL grant path"));
253        }
254        if !reachable.contains(&entry.grantor) {
255            return Err(invalid("ACL grantor has no owner-rooted grant option"));
256        }
257    }
258    Ok(())
259}
260
261/// The owner and every role named by the ACL.
262pub fn acl_roles(owner: RoleIdentity, acl: Option<&[ObjectAclEntry]>) -> BTreeSet<RoleIdentity> {
263    let mut identities = BTreeSet::from([owner]);
264    for entry in acl.into_iter().flatten() {
265        identities.extend(entry.role);
266        identities.insert(entry.grantor);
267    }
268    identities
269}
270
271/// Record the names of roles that an ACL change newly references, excluding the owners.
272pub fn added_acl_roles(
273    before: (RoleIdentity, Option<&[ObjectAclEntry]>),
274    after: (RoleIdentity, Option<&[ObjectAclEntry]>),
275    roles: &BTreeMap<String, RoleDefinition>,
276    object_kind: &str,
277    added: &mut BTreeSet<String>,
278) -> Result<(), SQLError> {
279    let mut old = acl_roles(before.0, before.1);
280    old.remove(&before.0);
281    let mut new = acl_roles(after.0, after.1);
282    new.remove(&after.0);
283    validate_acl(after.0, after.1, object_kind)?;
284    for identity in new.difference(&old) {
285        added.insert(
286            identity
287                .role_name(roles)
288                .ok_or_else(|| {
289                    SQLError::Internal(format!(
290                        "invalid {object_kind} authority: missing ACL incarnation"
291                    ))
292                })?
293                .to_owned(),
294        );
295    }
296    Ok(())
297}
298
299/// Bind grantee names to role incarnations; PUBLIC stays unbound.
300pub fn bind_grantees(
301    grantees: &[AclGrantee],
302    roles: &BTreeMap<String, RoleDefinition>,
303) -> Result<Vec<Option<RoleIdentity>>, SQLError> {
304    grantees
305        .iter()
306        .map(|grantee| {
307            grantee
308                .role_name()
309                .map(|name| {
310                    RoleReference::from(name)
311                        .bind(roles)
312                        .map(|role| role.identity())
313                })
314                .transpose()
315        })
316        .collect()
317}
318
319/// The WARNING of a GRANT or REVOKE whose current user holds no grant option, naming the object without its schema.
320pub fn acl_warning(is_grant: bool, local_name: &str) -> crate::SQLNotice {
321    super::acl_warning::acl_warning(is_grant, false, local_name)
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    fn role(value: u8) -> RoleIdentity {
329        RoleIdentity {
330            oid: i64::from(value),
331            object_id: [value; 16],
332        }
333    }
334
335    fn grant_entry(grantee: u8, grantor: u8) -> ObjectAclEntry {
336        ObjectAclEntry {
337            role: Some(role(grantee)),
338            grantor: role(grantor),
339            grant_option: true,
340        }
341    }
342
343    #[test]
344    fn grant_option_reachability_requires_an_owner_root() {
345        let disconnected_cycle = [grant_entry(2, 3), grant_entry(3, 2)];
346        assert_eq!(
347            grant_option_roles(role(1), Some(&disconnected_cycle)),
348            BTreeSet::from([role(1)])
349        );
350        let rooted_cycle = [grant_entry(2, 1), grant_entry(3, 2), grant_entry(2, 3)];
351        assert_eq!(
352            grant_option_roles(role(1), Some(&rooted_cycle)),
353            BTreeSet::from([role(1), role(2), role(3)])
354        );
355    }
356
357    #[test]
358    fn grant_option_reachability_accepts_an_independent_owner_path() {
359        let acl = [
360            grant_entry(2, 1),
361            grant_entry(3, 2),
362            grant_entry(3, 1),
363            grant_entry(4, 3),
364        ];
365        assert_eq!(
366            grant_option_roles(role(1), Some(&acl[2..])),
367            BTreeSet::from([role(1), role(3), role(4)])
368        );
369    }
370
371    #[test]
372    fn revoking_a_grant_option_with_dependent_grants_requires_cascade() {
373        let owner = role(1);
374        let mut acl = None;
375        grant(owner, &mut acl, Some(role(2)), owner, true);
376        grant(owner, &mut acl, Some(role(3)), role(2), false);
377        let error = revoke(owner, &mut acl.clone(), Some(role(2)), owner, true, false).unwrap_err();
378        assert_eq!(error.sqlstate(), Some("2BP01"));
379        assert_eq!(error.hint(), Some("Use CASCADE to revoke them too."));
380        revoke(owner, &mut acl, Some(role(2)), owner, true, true).unwrap();
381        assert_eq!(
382            acl.unwrap(),
383            [
384                ObjectAclEntry {
385                    role: None,
386                    grantor: owner,
387                    grant_option: false
388                },
389                ObjectAclEntry {
390                    role: Some(owner),
391                    grantor: owner,
392                    grant_option: false
393                },
394                ObjectAclEntry {
395                    role: Some(role(2)),
396                    grantor: owner,
397                    grant_option: false
398                },
399            ]
400        );
401    }
402}