1#[cfg(test)]
9mod tests;
10use super::{
11 columns::{grant_column_acl, revoke_column_acl, select_column_acl_grantor},
12 table::{
13 grant_acl, revoke_acl, select_acl_grantor, validate_table_security_invariants,
14 RequestedTablePrivileges, TableAclPrivilege,
15 },
16 BoundTableSecurity, TableSecurity,
17};
18use crate::catalog::roles::identity::RoleSubject;
19use crate::catalog::{
20 roles::{RoleDefinition, RoleMembership, RoleMembershipKey},
21 stored_view::StoredView,
22};
23use crate::{
24 ast::{GrantTableStmt, SequencePrivilege, TablePrivilege, TableRevokeBehavior},
25 SQLError,
26};
27use std::collections::{BTreeMap, BTreeSet};
28use uqa_core::catalog_acl::AclGrantee;
29use uqa_core::RelationIdentity;
30pub type ViewPrivilegeUpdate = (RelationIdentity, StoredView);
31pub type ForeignTablePrivilegeUpdate = (RelationIdentity, BoundTableSecurity);
32pub type ForeignTableGrantTarget<'a> = (
33 &'a ResolvedTableGrantTarget,
34 BoundTableSecurity,
35 Vec<String>,
36);
37pub mod targets;
38pub struct ResolvedTableGrantTarget {
39 pub requested: String,
40 pub name: String,
41 pub relation: RelationIdentity,
42 pub kind: &'static str,
43 pub acl_columns: Option<BTreeSet<String>>,
45}
46impl ResolvedTableGrantTarget {
47 pub fn includes_acl_tuple(&self, column: Option<&str>) -> bool {
48 column.is_none_or(|column| {
49 self.acl_columns
50 .as_ref()
51 .is_none_or(|columns| columns.contains(column))
52 })
53 }
54}
55
56fn apply_table_acl(
57 statement: &GrantTableStmt,
58 grantees: &[AclGrantee],
59 privileges: &[TableAclPrivilege],
60 current_user: &(impl RoleSubject + ?Sized),
61 roles: &BTreeMap<String, RoleDefinition>,
62 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
63 current: &TableSecurity,
64) -> Result<(TableSecurity, usize), SQLError> {
65 let grantors = privileges
66 .iter()
67 .map(|privilege| {
68 (
69 *privilege,
70 select_acl_grantor(current, *privilege, current_user, roles, memberships),
71 )
72 })
73 .collect::<Vec<_>>();
74 let grantable = grantors
75 .iter()
76 .filter(|(_, grantor)| grantor.is_some())
77 .count();
78 let mut next = current.clone();
79 for (privilege, grantor) in grantors {
80 let Some(grantor) = grantor else {
81 continue;
82 };
83 if statement.is_grant {
84 grant_acl(
85 &mut next,
86 privilege,
87 grantees,
88 &grantor,
89 statement.grant_option,
90 );
91 } else {
92 revoke_acl(
93 &mut next,
94 privilege,
95 grantees,
96 &grantor,
97 statement.grant_option_only,
98 statement.revoke_behavior == TableRevokeBehavior::Cascade,
99 )?;
100 }
101 }
102 Ok((next, grantable))
103}
104
105fn apply_column_acl(
106 application: &TableGrantApplication<'_>,
107 privileges: &[(TableAclPrivilege, String)],
108 authorization: &TableSecurity,
109 current: &TableSecurity,
110 selected: Option<&BTreeSet<String>>,
111) -> Result<(TableSecurity, usize), SQLError> {
112 let TableGrantApplication {
113 statement,
114 grantees,
115 current_user,
116 roles,
117 memberships,
118 ..
119 } = application;
120 let grantors = privileges
121 .iter()
122 .map(|(privilege, column)| {
123 (
124 *privilege,
125 column.clone(),
126 select_column_acl_grantor(
127 authorization,
128 column,
129 *privilege,
130 current_user,
131 roles,
132 memberships,
133 ),
134 )
135 })
136 .collect::<Vec<_>>();
137 let grantable = grantors
138 .iter()
139 .filter(|(privilege, column, grantor)| {
140 grantor.is_some()
141 && application
142 .requested
143 .columns
144 .contains(&(*privilege, column.clone()))
145 })
146 .count();
147 let mut next = current.clone();
148 for (privilege, column, grantor) in grantors {
149 if selected.is_some_and(|columns| !columns.contains(&column)) {
150 continue;
151 }
152 let Some(grantor) = grantor else {
153 continue;
154 };
155 if statement.is_grant {
156 grant_column_acl(
157 &mut next,
158 &column,
159 privilege,
160 grantees,
161 &grantor,
162 statement.grant_option,
163 );
164 } else {
165 revoke_column_acl(
166 &mut next,
167 &column,
168 privilege,
169 grantees,
170 &grantor,
171 statement.grant_option_only,
172 statement.revoke_behavior == TableRevokeBehavior::Cascade,
173 )?;
174 }
175 }
176 next.column_acls.retain(|_, acl| !acl.is_empty());
177 Ok((next, grantable))
178}
179
180pub struct TableGrantApplication<'a> {
181 pub statement: &'a GrantTableStmt,
182 pub grantees: &'a [AclGrantee],
183 pub requested: &'a RequestedTablePrivileges,
184 pub current_user: &'a dyn RoleSubject,
185 pub roles: &'a BTreeMap<String, RoleDefinition>,
186 pub memberships: &'a BTreeMap<RoleMembershipKey, RoleMembership>,
187}
188
189impl TableGrantApplication<'_> {
190 pub fn replaced_tuples(
192 &self,
193 before: &TableSecurity,
194 after: &TableSecurity,
195 ) -> Vec<Option<String>> {
196 let mut tuples = Vec::new();
197 let implicit_columns = !self.statement.is_grant
198 && self.requested.table.iter().any(|privilege| {
199 matches!(
200 privilege,
201 TableAclPrivilege::Select
202 | TableAclPrivilege::Insert
203 | TableAclPrivilege::Update
204 | TableAclPrivilege::References
205 )
206 });
207 if !self.requested.table.is_empty() {
208 tuples.push(None);
209 }
210 let columns = before
211 .column_acls
212 .keys()
213 .chain(after.column_acls.keys())
214 .chain(self.requested.columns.iter().map(|(_, column)| column))
215 .collect::<std::collections::BTreeSet<_>>();
216 for column in columns {
217 if before.column_acls.get(column) != after.column_acls.get(column)
218 || ((implicit_columns
219 || self
220 .requested
221 .columns
222 .iter()
223 .any(|(_, name)| name == column))
224 && after
225 .column_acls
226 .get(column)
227 .is_some_and(|acl| !acl.is_empty()))
228 {
229 tuples.push(Some(column.clone()));
230 }
231 }
232 tuples
233 }
234
235 pub fn apply(&self, current: &TableSecurity) -> Result<(TableSecurity, usize), SQLError> {
236 self.apply_columns(current, None)
237 }
238
239 pub fn apply_to(
240 &self,
241 target: &ResolvedTableGrantTarget,
242 current: &TableSecurity,
243 ) -> Result<(TableSecurity, usize), SQLError> {
244 self.apply_columns(current, target.acl_columns.as_ref())
245 }
246
247 pub fn replaces_attribute(
249 &self,
250 current: &TableSecurity,
251 column: &str,
252 ) -> Result<bool, SQLError> {
253 let selected = BTreeSet::from([column.to_owned()]);
254 let (next, _) = self.apply_columns(current, Some(&selected))?;
255 Ok(self
256 .replaced_tuples(current, &next)
257 .iter()
258 .any(|tuple| tuple.as_deref() == Some(column)))
259 }
260
261 fn apply_columns(
262 &self,
263 current: &TableSecurity,
264 selected: Option<&BTreeSet<String>>,
265 ) -> Result<(TableSecurity, usize), SQLError> {
266 let (next, table_grantable) = apply_table_acl(
267 self.statement,
268 self.grantees,
269 &self.requested.table,
270 self.current_user,
271 self.roles,
272 self.memberships,
273 current,
274 )?;
275 let mut columns = self.requested.columns.clone();
276 let mut implied = Vec::new();
277 if !self.statement.is_grant {
278 for privilege in &self.requested.table {
279 if matches!(
280 privilege,
281 TableAclPrivilege::Select
282 | TableAclPrivilege::Insert
283 | TableAclPrivilege::Update
284 | TableAclPrivilege::References
285 ) {
286 for column in current.column_acls.keys() {
287 let key = (*privilege, column.clone());
288 if !columns.contains(&key) {
289 columns.push(key.clone());
290 }
291 implied.push(key);
292 }
293 }
294 }
295 }
296 let (mut next, column_grantable) =
297 apply_column_acl(self, &columns, current, &next, selected)?;
298 for (privilege, column) in implied {
300 if selected.is_some_and(|columns| !columns.contains(&column)) {
301 continue;
302 }
303 let before = super::columns::column_grant_option_roles(current, &column, privilege);
304 super::columns::revoke_dependent_column_acl(
305 &mut next,
306 &column,
307 privilege,
308 &before,
309 self.statement.revoke_behavior == TableRevokeBehavior::Cascade,
310 )?;
311 }
312 next.column_acls.retain(|_, acl| !acl.is_empty());
313 Ok((next, table_grantable + column_grantable))
314 }
315
316 pub fn record_warning(
317 &self,
318 grantable: usize,
319 relation: &RelationIdentity,
320 notices: &mut Vec<crate::SQLNotice>,
321 ) {
322 let requested = self.requested.table.len() + self.requested.columns.len();
323 if grantable != requested {
324 notices.push(super::acl_warning::acl_warning(
325 self.statement.is_grant,
326 grantable != 0,
327 &relation.name,
328 ));
329 }
330 }
331}
332
333pub fn validate_requested_columns(
334 target: &RelationIdentity,
335 columns: &[String],
336 requested: &RequestedTablePrivileges,
337) -> Result<(), SQLError> {
338 for (_, requested_column) in &requested.columns {
339 if !columns.contains(requested_column) {
340 return Err(SQLError::Routine {
341 sqlstate: "42703".into(),
342 message: format!(
343 "column \"{requested_column}\" of relation \"{}\" does not exist",
344 target.name
345 ),
346 });
347 }
348 }
349 Ok(())
350}
351
352pub fn validate_table_grant_target_kinds(
353 statement: &GrantTableStmt,
354 targets: &[ResolvedTableGrantTarget],
355) -> Result<(), SQLError> {
356 for target in targets {
357 let refused = match target.kind {
359 "index" => Some("an index"),
360 "composite type" => Some("a composite type"),
361 _ => None,
362 };
363 if let Some(refused) = refused {
364 return Err(SQLError::Routine {
365 sqlstate: "42809".into(),
366 message: format!("\"{}\" is {refused}", target.relation.name),
367 });
368 }
369 if !matches!(
370 target.kind,
371 "table" | "view" | "materialized view" | "foreign table" | "sequence"
372 ) {
373 return Err(SQLError::Unsupported(format!(
374 "{} privileges for \"{}\" are not supported",
375 target.kind, target.requested
376 )));
377 }
378 }
379 if let Some(column) = statement
380 .privileges
381 .iter()
382 .flat_map(|privilege| &privilege.columns)
383 .next()
384 {
385 if let Some(target) = targets.iter().find(|target| target.kind == "sequence") {
386 return Err(SQLError::Routine {
387 sqlstate: "42703".into(),
388 message: format!(
389 "column \"{column}\" of relation \"{}\" does not exist",
390 target.relation.name
391 ),
392 });
393 }
394 }
395 Ok(())
396}
397
398pub fn validate_table_acl_roles(
399 statement: &GrantTableStmt,
400 grantees: &[AclGrantee],
401 requested_grantor: Option<&str>,
402 current_user: &(impl RoleSubject + ?Sized),
403 roles: &BTreeMap<String, RoleDefinition>,
404) -> Result<(), SQLError> {
405 for role in grantees {
406 if role
407 .role_name()
408 .is_some_and(|name| !roles.contains_key(name))
409 {
410 return Err(SQLError::Routine {
411 sqlstate: "42704".into(),
412 message: format!("role \"{role}\" does not exist"),
413 });
414 }
415 }
416 if statement.is_grant && statement.grant_option && grantees.iter().any(AclGrantee::is_public) {
417 return Err(SQLError::Routine {
418 sqlstate: "0LP01".into(),
419 message: "grant options can only be granted to roles".into(),
420 });
421 }
422 if let Some(requested_grantor) = requested_grantor {
423 if !roles.contains_key(requested_grantor) {
424 return Err(SQLError::Routine {
425 sqlstate: "42704".into(),
426 message: format!("role \"{requested_grantor}\" does not exist"),
427 });
428 }
429 if current_user.role_name(roles) != Some(requested_grantor) {
430 return Err(SQLError::Routine {
431 sqlstate: "0A000".into(),
432 message: "grantor must be current user".into(),
433 });
434 }
435 }
436 Ok(())
437}
438
439pub fn table_sequence_privileges(
440 privileges: &[crate::ast::TablePrivilegeSpec],
441) -> (Vec<SequencePrivilege>, bool) {
442 if privileges.is_empty() {
443 return (
444 vec![
445 SequencePrivilege::Select,
446 SequencePrivilege::Update,
447 SequencePrivilege::Usage,
448 ],
449 false,
450 );
451 }
452 let mut mapped = Vec::new();
453 let mut inapplicable = false;
454 for spec in privileges {
455 let privilege = if spec.columns.is_empty() {
456 match &spec.privilege {
457 TablePrivilege::Select => Some(SequencePrivilege::Select),
458 TablePrivilege::Update => Some(SequencePrivilege::Update),
459 TablePrivilege::Usage => Some(SequencePrivilege::Usage),
460 _ => {
461 inapplicable = true;
462 None
463 }
464 }
465 } else {
466 Some(SequencePrivilege::ColumnsUnsupported)
467 };
468 if let Some(privilege) = privilege {
469 if !mapped.contains(&privilege) {
470 mapped.push(privilege);
471 }
472 }
473 }
474 (mapped, inapplicable)
475}
476
477pub fn view_privilege_updates(
478 targets: Vec<(&ResolvedTableGrantTarget, StoredView)>,
479 application: &TableGrantApplication<'_>,
480 notices: &mut Vec<crate::SQLNotice>,
481 dependencies: &mut std::collections::BTreeSet<String>,
482) -> Result<Vec<ViewPrivilegeUpdate>, SQLError> {
483 let mut updates = Vec::new();
484 for (target, mut view) in targets {
485 let current = view
486 .security
487 .resolve(application.roles)
488 .map_err(SQLError::Internal)?;
489 let (next, grantable) = application.apply_to(target, ¤t)?;
490 crate::catalog::security::dependencies::added_table_acl_roles(
491 ¤t,
492 &next,
493 dependencies,
494 );
495 let columns = view.output_columns.as_deref().ok_or_else(|| {
496 SQLError::Internal(format!(
497 "loaded view `{}` has no durable public column metadata",
498 target.relation.qualified_name()
499 ))
500 })?;
501 validate_table_security_invariants(&next, Some(columns), application.roles).map_err(
502 |error| {
503 SQLError::Internal(format!(
504 "view `{}` produced invalid privilege metadata: {error}",
505 target.relation.qualified_name()
506 ))
507 },
508 )?;
509 application.record_warning(grantable, &target.relation, notices);
510 if application
511 .replaced_tuples(¤t, &next)
512 .iter()
513 .any(|column| target.includes_acl_tuple(column.as_deref()))
514 {
515 view.set_security(
516 BoundTableSecurity::bind(&next, application.roles).map_err(SQLError::Internal)?,
517 );
518 updates.push((target.relation.clone(), view));
519 }
520 }
521 Ok(updates)
522}
523pub fn foreign_table_privilege_updates(
524 targets: Vec<ForeignTableGrantTarget<'_>>,
525 application: &TableGrantApplication<'_>,
526 notices: &mut Vec<crate::SQLNotice>,
527 dependencies: &mut std::collections::BTreeSet<String>,
528) -> Result<Vec<ForeignTablePrivilegeUpdate>, SQLError> {
529 let mut updates = Vec::new();
530 for (target, current, columns) in targets {
531 let current = current
532 .resolve(application.roles)
533 .map_err(SQLError::Internal)?;
534 let (next, grantable) = application.apply_to(target, ¤t)?;
535 crate::catalog::security::dependencies::added_table_acl_roles(
536 ¤t,
537 &next,
538 dependencies,
539 );
540 validate_table_security_invariants(&next, Some(&columns), application.roles).map_err(
541 |error| {
542 SQLError::Internal(format!(
543 "foreign table `{}` produced invalid privilege metadata: {error}",
544 target.relation.qualified_name()
545 ))
546 },
547 )?;
548 application.record_warning(grantable, &target.relation, notices);
549 if application
550 .replaced_tuples(¤t, &next)
551 .iter()
552 .any(|column| target.includes_acl_tuple(column.as_deref()))
553 {
554 updates.push((
555 target.relation.clone(),
556 BoundTableSecurity::bind(&next, application.roles).map_err(SQLError::Internal)?,
557 ));
558 }
559 }
560 Ok(updates)
561}