uqa_sql/catalog/security/table/
invariants.rs1use super::{RoleDefinition, TableAclEntry, TableAclPrivilege, TableSecurity};
9use crate::catalog::security::columns::column_grant_option_roles;
10use std::collections::{BTreeMap, BTreeSet};
11
12pub fn validate_table_security_invariants(
13 security: &TableSecurity,
14 columns: Option<&[String]>,
15 roles: &BTreeMap<String, RoleDefinition>,
16) -> Result<(), String> {
17 let validate_acl = |acl: &[TableAclEntry], column: Option<&str>| -> Result<(), String> {
18 let mut paths = BTreeSet::new();
19 for entry in acl {
20 let grantor = super::acl_grantor(entry, &security.role_owner);
21 if entry
22 .role
23 .role_name()
24 .is_some_and(|name| !roles.contains_key(name))
25 {
26 return Err(format!(
27 "ACL references missing grantee role `{}`",
28 entry.role
29 ));
30 }
31 if !roles.contains_key(grantor) {
32 return Err(format!("ACL references missing grantor role `{grantor}`"));
33 }
34 if !paths.insert((&entry.role, grantor)) {
35 return Err(format!(
36 "ACL contains duplicate grant path `{grantor}` -> `{}`",
37 entry.role
38 ));
39 }
40 if entry.privileges.is_empty() && entry.grant_options.is_empty() {
41 return Err("ACL contains an empty grant path".into());
42 }
43 if entry.role.is_public() && !entry.grant_options.is_empty() {
44 return Err("PUBLIC cannot hold grant options".into());
45 }
46 for privilege in TableAclPrivilege::ALL {
47 let mask = privilege.mask();
48 if entry.grant_options.intersects(mask) && !entry.privileges.intersects(mask) {
49 return Err("ACL grant option exists without its privilege".into());
50 }
51 if entry.privileges.intersects(mask) || entry.grant_options.intersects(mask) {
52 let reachable = column.map_or_else(
53 || super::grant_option_roles(security, privilege),
54 |column| column_grant_option_roles(security, column, privilege),
55 );
56 if !reachable.contains(grantor) {
57 return Err(format!(
58 "ACL grant path from `{grantor}` is not rooted at owner `{}`",
59 security.role_owner
60 ));
61 }
62 }
63 }
64 }
65 Ok(())
66 };
67
68 if let Some(acl) = security.acl.as_deref() {
69 validate_acl(acl, None)?;
70 }
71 if !security.column_acls.is_empty() && columns.is_none() {
72 return Err("column ACLs require durable public column metadata".into());
73 }
74 for (column, acl) in &security.column_acls {
75 if !columns.is_some_and(|columns| columns.iter().any(|candidate| candidate == column)) {
76 return Err(format!("column ACL references missing column `{column}`"));
77 }
78 for entry in acl {
79 if entry.privileges.delete
80 || entry.privileges.truncate
81 || entry.privileges.trigger
82 || entry.privileges.maintain
83 || entry.grant_options.delete
84 || entry.grant_options.truncate
85 || entry.grant_options.trigger
86 || entry.grant_options.maintain
87 {
88 return Err(format!(
89 "column ACL for `{column}` contains a relation-only privilege"
90 ));
91 }
92 }
93 validate_acl(acl, Some(column))?;
94 }
95 Ok(())
96}