Skip to main content

uqa_sql/catalog/security/
columns.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Table-shaped relation column ACL grant paths and privilege checks.
8
9use crate::catalog::roles::identity::RoleSubject;
10use std::collections::{BTreeMap, BTreeSet};
11use uqa_core::catalog_acl::AclGrantee;
12
13use super::{TableAclEntry, TablePrivileges};
14use crate::ast::RoleAttribute;
15use crate::SQLError;
16
17use super::table::{acl_grantor, grant_option_roles, TableAclPrivilege, TablePrivilegeCheck};
18use super::TableSecurity;
19use crate::catalog::roles::{role_inherits, RoleDefinition, RoleMembership, RoleMembershipKey};
20
21pub fn column_grant_option_roles(
22    security: &TableSecurity,
23    column: &str,
24    privilege: TableAclPrivilege,
25) -> BTreeSet<String> {
26    let mut reachable = grant_option_roles(security, privilege);
27    let Some(acl) = security.column_acls.get(column) else {
28        return reachable;
29    };
30    loop {
31        let mut changed = false;
32        for entry in acl {
33            let Some(role) = entry.role.role_name() else {
34                continue;
35            };
36            if entry.grant_options.intersects(privilege.mask())
37                && reachable.contains(acl_grantor(entry, &security.role_owner))
38            {
39                changed |= reachable.insert(role.to_owned());
40            }
41        }
42        if !changed {
43            return reachable;
44        }
45    }
46}
47
48pub fn select_column_acl_grantor(
49    security: &TableSecurity,
50    column: &str,
51    privilege: TableAclPrivilege,
52    current_user: &(impl RoleSubject + ?Sized),
53    roles: &BTreeMap<String, RoleDefinition>,
54    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
55) -> Option<String> {
56    let current_user = current_user.role_name(roles)?;
57    if role_inherits(roles, memberships, current_user, &security.role_owner) {
58        return Some(security.role_owner.clone());
59    }
60    let grant_options = column_grant_option_roles(security, column, privilege);
61    if grant_options.contains(current_user) {
62        return Some(current_user.to_string());
63    }
64    grant_options
65        .into_iter()
66        .filter(|role| role != &security.role_owner)
67        .find(|role| role_inherits(roles, memberships, current_user, role))
68}
69
70pub fn role_has_column_privilege(
71    security: &TableSecurity,
72    column: &str,
73    subject: &(impl RoleSubject + ?Sized),
74    check: TablePrivilegeCheck,
75    roles: &BTreeMap<String, RoleDefinition>,
76    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
77) -> bool {
78    if subject
79        .role_definition(roles)
80        .is_some_and(|role| role.has(RoleAttribute::Superuser))
81    {
82        return true;
83    }
84    if check.grant_option {
85        return column_grant_option_roles(security, column, check.privilege)
86            .iter()
87            .any(|role| role_inherits(roles, memberships, subject, role));
88    }
89    if super::table::role_has_privilege(security, subject, check, roles, memberships) {
90        return true;
91    }
92    security.column_acls.get(column).is_some_and(|acl| {
93        acl.iter().any(|entry| {
94            entry.privileges.intersects(check.privilege.mask())
95                && (entry.role.is_public()
96                    || role_inherits(roles, memberships, subject, &entry.role))
97        })
98    })
99}
100
101pub fn grant_column_acl(
102    security: &mut TableSecurity,
103    column: &str,
104    privilege: TableAclPrivilege,
105    grantees: &[AclGrantee],
106    grantor: &str,
107    grant_option: bool,
108) {
109    let owner = security.role_owner.clone();
110    let acl = security.column_acls.entry(column.to_string()).or_default();
111    for grantee in grantees {
112        let position = acl
113            .iter()
114            .position(|entry| entry.role == *grantee && acl_grantor(entry, &owner) == grantor)
115            .unwrap_or_else(|| {
116                acl.push(TableAclEntry {
117                    role: grantee.clone(),
118                    grantor: Some(grantor.to_string()),
119                    privileges: TablePrivileges::default(),
120                    grant_options: TablePrivileges::default(),
121                });
122                acl.len() - 1
123            });
124        let entry = &mut acl[position];
125        entry.privileges.insert(privilege.mask());
126        if grant_option && grantee.role_name().is_some_and(|name| name != owner) {
127            entry.grant_options.insert(privilege.mask());
128        }
129    }
130}
131
132pub fn revoke_column_acl(
133    security: &mut TableSecurity,
134    column: &str,
135    privilege: TableAclPrivilege,
136    grantees: &[AclGrantee],
137    grantor: &str,
138    grant_option_only: bool,
139    cascade: bool,
140) -> Result<(), SQLError> {
141    let before = column_grant_option_roles(security, column, privilege);
142    let owner = security.role_owner.clone();
143    let Some(acl) = security.column_acls.get_mut(column) else {
144        return Ok(());
145    };
146    for entry in acl
147        .iter_mut()
148        .filter(|entry| grantees.contains(&entry.role) && acl_grantor(entry, &owner) == grantor)
149    {
150        entry.grant_options.remove(privilege.mask());
151        if !grant_option_only {
152            entry.privileges.remove(privilege.mask());
153        }
154    }
155    remove_empty_entries(acl);
156    revoke_dependent_column_acl(security, column, privilege, &before, cascade)
157}
158
159pub(super) fn revoke_dependent_column_acl(
160    security: &mut TableSecurity,
161    column: &str,
162    privilege: TableAclPrivilege,
163    before: &BTreeSet<String>,
164    cascade: bool,
165) -> Result<(), SQLError> {
166    loop {
167        let current = column_grant_option_roles(security, column, privilege);
168        let lost = before
169            .difference(&current)
170            .cloned()
171            .collect::<BTreeSet<_>>();
172        if lost.is_empty() {
173            return Ok(());
174        }
175        let owner = security.role_owner.clone();
176        let dependent = security.column_acls.get(column).is_some_and(|acl| {
177            acl.iter().any(|entry| {
178                lost.contains(acl_grantor(entry, &owner))
179                    && (entry.privileges.intersects(privilege.mask())
180                        || entry.grant_options.intersects(privilege.mask()))
181            })
182        });
183        if !dependent {
184            return Ok(());
185        }
186        if !cascade {
187            return Err(SQLError::Routine {
188                sqlstate: "2BP01".into(),
189                message: "dependent privileges exist".into(),
190            });
191        }
192        let acl = security
193            .column_acls
194            .get_mut(column)
195            .expect("dependent column privileges require an explicit ACL");
196        for entry in acl
197            .iter_mut()
198            .filter(|entry| lost.contains(acl_grantor(entry, &owner)))
199        {
200            entry.privileges.remove(privilege.mask());
201            entry.grant_options.remove(privilege.mask());
202        }
203        remove_empty_entries(acl);
204    }
205}
206
207fn remove_empty_entries(acl: &mut Vec<TableAclEntry>) {
208    acl.retain(|entry| !entry.privileges.is_empty() || !entry.grant_options.is_empty());
209}