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