uqa_sql/catalog/security/
columns.rs1use 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(¤t)
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}