Skip to main content

uqa_sql/catalog/security/
sequence.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Sequence ACL privilege sets, grant paths, and dependency-aware revocation.
8
9use crate::catalog::roles::identity::RoleSubject;
10use std::collections::{BTreeMap, BTreeSet};
11use uqa_core::catalog_acl::AclGrantee;
12
13use crate::ast::{RoleAttribute, SequencePrivilege};
14use crate::SQLError;
15use uqa_core::catalog_sequence::{SequenceAclEntry, SequencePrivileges};
16
17use super::SequenceSecurity;
18use crate::catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey};
19
20mod invariants;
21pub use invariants::validate_sequence_security_invariants;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub enum AclPrivilege {
25    Select,
26    Update,
27    Usage,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct PrivilegeCheck {
32    pub privilege: AclPrivilege,
33    pub grant_option: bool,
34}
35
36impl AclPrivilege {
37    pub const fn mask(self) -> SequencePrivileges {
38        match self {
39            Self::Select => SequencePrivileges {
40                select: true,
41                update: false,
42                usage: false,
43            },
44            Self::Update => SequencePrivileges {
45                select: false,
46                update: true,
47                usage: false,
48            },
49            Self::Usage => SequencePrivileges {
50                select: false,
51                update: false,
52                usage: true,
53            },
54        }
55    }
56}
57
58pub fn requested_acl_privileges(
59    requested: &[SequencePrivilege],
60) -> Result<Vec<AclPrivilege>, SQLError> {
61    requested
62        .iter()
63        .map(|privilege| match privilege {
64            SequencePrivilege::Select => Ok(AclPrivilege::Select),
65            SequencePrivilege::Update => Ok(AclPrivilege::Update),
66            SequencePrivilege::Usage => Ok(AclPrivilege::Usage),
67            SequencePrivilege::ColumnsUnsupported => Err(SQLError::Routine {
68                sqlstate: "0LP01".into(),
69                message: "column privileges are only valid for relations".into(),
70            }),
71            SequencePrivilege::Unsupported(name) => Err(SQLError::Routine {
72                sqlstate: "0LP01".into(),
73                message: format!("invalid privilege type {name} for sequence"),
74            }),
75        })
76        .collect()
77}
78
79pub fn parse_privilege_checks(value: &str) -> Result<Vec<PrivilegeCheck>, SQLError> {
80    value
81        .split(',')
82        .map(|item| {
83            let item = item.trim();
84            let upper = item.to_ascii_uppercase();
85            let (name, grant_option) = upper
86                .strip_suffix(" WITH GRANT OPTION")
87                .map_or((upper.as_str(), false), |name| (name.trim_end(), true));
88            let privilege = match name {
89                "SELECT" => AclPrivilege::Select,
90                "UPDATE" => AclPrivilege::Update,
91                "USAGE" => AclPrivilege::Usage,
92                _ => {
93                    return Err(SQLError::Routine {
94                        sqlstate: "22023".into(),
95                        message: format!("unrecognized privilege type: \"{item}\""),
96                    })
97                }
98            };
99            Ok(PrivilegeCheck {
100                privilege,
101                grant_option,
102            })
103        })
104        .collect()
105}
106
107fn acl_grantor<'a>(entry: &'a SequenceAclEntry, owner: &'a str) -> &'a str {
108    entry.grantor.as_deref().unwrap_or(owner)
109}
110
111fn materialize_acl(security: &mut SequenceSecurity) {
112    if security.acl.is_none() {
113        security.acl = Some(vec![SequenceAclEntry {
114            role: security.role_owner.clone().into(),
115            grantor: Some(security.role_owner.clone()),
116            privileges: SequencePrivileges::ALL,
117            grant_options: SequencePrivileges::default(),
118        }]);
119    }
120}
121
122fn grant_option_roles(security: &SequenceSecurity, privilege: AclPrivilege) -> BTreeSet<String> {
123    let mut reachable = BTreeSet::from([security.role_owner.clone()]);
124    let Some(acl) = security.acl.as_ref() else {
125        return reachable;
126    };
127    loop {
128        let mut changed = false;
129        for entry in acl {
130            let Some(role) = entry.role.role_name() else {
131                continue;
132            };
133            if entry.grant_options.intersects(privilege.mask())
134                && reachable.contains(acl_grantor(entry, &security.role_owner))
135            {
136                changed |= reachable.insert(role.to_owned());
137            }
138        }
139        if !changed {
140            return reachable;
141        }
142    }
143}
144
145pub fn select_acl_grantor(
146    security: &SequenceSecurity,
147    privilege: AclPrivilege,
148    current_user: &(impl RoleSubject + ?Sized),
149    roles: &BTreeMap<String, RoleDefinition>,
150    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
151) -> Option<String> {
152    let current_user = current_user.role_name(roles)?;
153    if role_inherits(roles, memberships, current_user, &security.role_owner) {
154        return Some(security.role_owner.clone());
155    }
156    let grant_options = grant_option_roles(security, privilege);
157    if grant_options.contains(current_user) {
158        return Some(current_user.to_string());
159    }
160    security.acl.as_ref().and_then(|acl| {
161        acl.iter()
162            .filter_map(|entry| entry.role.role_name())
163            .filter(|role| grant_options.contains(*role))
164            .find(|role| role_inherits(roles, memberships, current_user, *role))
165            .map(str::to_owned)
166    })
167}
168
169pub fn role_has_privilege(
170    security: &SequenceSecurity,
171    subject: &(impl RoleSubject + ?Sized),
172    check: PrivilegeCheck,
173    roles: &BTreeMap<String, RoleDefinition>,
174    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
175) -> bool {
176    if subject
177        .role_definition(roles)
178        .is_some_and(|role| role.has(RoleAttribute::Superuser))
179    {
180        return true;
181    }
182    if check.grant_option {
183        return grant_option_roles(security, check.privilege)
184            .iter()
185            .any(|role| role_inherits(roles, memberships, subject, role));
186    }
187    match security.acl.as_ref() {
188        None => role_inherits(roles, memberships, subject, &security.role_owner),
189        Some(acl) => acl.iter().any(|entry| {
190            entry.privileges.intersects(check.privilege.mask())
191                && (entry.role.is_public()
192                    || role_inherits(roles, memberships, subject, &entry.role))
193        }),
194    }
195}
196
197pub fn grant_acl(
198    security: &mut SequenceSecurity,
199    privilege: AclPrivilege,
200    grantees: &[AclGrantee],
201    grantor: &str,
202    grant_option: bool,
203) {
204    materialize_acl(security);
205    let owner = security.role_owner.clone();
206    let acl = security
207        .acl
208        .as_mut()
209        .expect("sequence ACL was materialized");
210    for grantee in grantees {
211        let position = acl
212            .iter()
213            .position(|entry| entry.role == *grantee && acl_grantor(entry, &owner) == grantor)
214            .unwrap_or_else(|| {
215                acl.push(SequenceAclEntry {
216                    role: grantee.clone(),
217                    grantor: Some(grantor.to_string()),
218                    privileges: SequencePrivileges::default(),
219                    grant_options: SequencePrivileges::default(),
220                });
221                acl.len() - 1
222            });
223        let entry = &mut acl[position];
224        entry.privileges.insert(privilege.mask());
225        if grant_option && grantee.role_name().is_some_and(|name| name != owner) {
226            entry.grant_options.insert(privilege.mask());
227        }
228    }
229}
230
231pub fn revoke_acl(
232    security: &mut SequenceSecurity,
233    privilege: AclPrivilege,
234    grantees: &[AclGrantee],
235    grantor: &str,
236    grant_option_only: bool,
237    cascade: bool,
238) -> Result<(), SQLError> {
239    let before = grant_option_roles(security, privilege);
240    materialize_acl(security);
241    let owner = security.role_owner.clone();
242    let acl = security
243        .acl
244        .as_mut()
245        .expect("sequence ACL was materialized");
246    for entry in acl
247        .iter_mut()
248        .filter(|entry| grantees.contains(&entry.role) && acl_grantor(entry, &owner) == grantor)
249    {
250        entry.grant_options.remove(privilege.mask());
251        if !grant_option_only {
252            entry.privileges.remove(privilege.mask());
253        }
254    }
255    remove_empty_entries(acl);
256    revoke_dependent_acl(security, privilege, &before, cascade)
257}
258
259fn revoke_dependent_acl(
260    security: &mut SequenceSecurity,
261    privilege: AclPrivilege,
262    before: &BTreeSet<String>,
263    cascade: bool,
264) -> Result<(), SQLError> {
265    loop {
266        let current = grant_option_roles(security, privilege);
267        let lost = before
268            .difference(&current)
269            .cloned()
270            .collect::<BTreeSet<_>>();
271        if lost.is_empty() {
272            return Ok(());
273        }
274        let owner = security.role_owner.clone();
275        let dependent = security.acl.as_ref().is_some_and(|acl| {
276            acl.iter().any(|entry| {
277                lost.contains(acl_grantor(entry, &owner))
278                    && (entry.privileges.intersects(privilege.mask())
279                        || entry.grant_options.intersects(privilege.mask()))
280            })
281        });
282        if !dependent {
283            return Ok(());
284        }
285        if !cascade {
286            return Err(SQLError::Routine {
287                sqlstate: "2BP01".into(),
288                message: "dependent privileges exist".into(),
289            });
290        }
291        let acl = security
292            .acl
293            .as_mut()
294            .expect("dependent sequence privileges require an explicit ACL");
295        for entry in acl
296            .iter_mut()
297            .filter(|entry| lost.contains(acl_grantor(entry, &owner)))
298        {
299            entry.privileges.remove(privilege.mask());
300            entry.grant_options.remove(privilege.mask());
301        }
302        remove_empty_entries(acl);
303    }
304}
305
306fn remove_empty_entries(acl: &mut Vec<SequenceAclEntry>) {
307    acl.retain(|entry| !entry.privileges.is_empty() || !entry.grant_options.is_empty());
308}
309
310pub fn rewrite_acl_owner(security: &mut SequenceSecurity, new_owner: &str) {
311    let old_owner = std::mem::replace(&mut security.role_owner, new_owner.to_string());
312    let Some(acl) = security.acl.as_mut() else {
313        return;
314    };
315    for entry in acl.iter_mut() {
316        if entry.role.role_name() == Some(old_owner.as_str()) {
317            entry.role = new_owner.into();
318        }
319        if entry.grantor.as_deref() == Some(&old_owner) {
320            entry.grantor = Some(new_owner.to_string());
321        }
322    }
323    let mut merged: Vec<SequenceAclEntry> = Vec::with_capacity(acl.len());
324    for entry in std::mem::take(acl) {
325        if let Some(existing) = merged.iter_mut().find(|existing| {
326            existing.role == entry.role
327                && acl_grantor(existing, new_owner) == acl_grantor(&entry, new_owner)
328        }) {
329            existing.privileges.insert(entry.privileges);
330            existing.grant_options.insert(entry.grant_options);
331        } else {
332            merged.push(entry);
333        }
334    }
335    *acl = merged;
336}
337
338pub fn role_can_view_sequence(
339    security: &SequenceSecurity,
340    subject: &(impl RoleSubject + ?Sized),
341    roles: &BTreeMap<String, RoleDefinition>,
342    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
343) -> bool {
344    role_inherits(roles, memberships, subject, &security.role_owner)
345        || role_has_any_sequence_privilege(security, subject, roles, memberships)
346}
347
348pub fn role_has_any_sequence_privilege(
349    security: &SequenceSecurity,
350    subject: &(impl RoleSubject + ?Sized),
351    roles: &BTreeMap<String, RoleDefinition>,
352    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
353) -> bool {
354    [
355        AclPrivilege::Select,
356        AclPrivilege::Update,
357        AclPrivilege::Usage,
358    ]
359    .into_iter()
360    .any(|privilege| {
361        role_has_privilege(
362            security,
363            subject,
364            PrivilegeCheck {
365                privilege,
366                grant_option: false,
367            },
368            roles,
369            memberships,
370        )
371    })
372}
373
374pub fn role_can_select_sequence(
375    security: &SequenceSecurity,
376    subject: &(impl RoleSubject + ?Sized),
377    roles: &BTreeMap<String, RoleDefinition>,
378    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
379) -> bool {
380    role_has_privilege(
381        security,
382        subject,
383        PrivilegeCheck {
384            privilege: AclPrivilege::Select,
385            grant_option: false,
386        },
387        roles,
388        memberships,
389    )
390}
391
392pub fn role_can_read_sequence_value(
393    security: &SequenceSecurity,
394    subject: &(impl RoleSubject + ?Sized),
395    roles: &BTreeMap<String, RoleDefinition>,
396    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
397) -> bool {
398    [AclPrivilege::Select, AclPrivilege::Usage]
399        .into_iter()
400        .any(|privilege| {
401            role_has_privilege(
402                security,
403                subject,
404                PrivilegeCheck {
405                    privilege,
406                    grant_option: false,
407                },
408                roles,
409                memberships,
410            )
411        })
412}