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