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.role != "PUBLIC" && !roles.contains_key(&entry.role) {
22 return Err(format!(
23 "ACL references missing grantee role `{}`",
24 entry.role
25 ));
26 }
27 if grantor == "PUBLIC" || !roles.contains_key(grantor) {
28 return Err(format!("ACL references missing grantor role `{grantor}`"));
29 }
30 if !paths.insert((entry.role.as_str(), grantor)) {
31 return Err(format!(
32 "ACL contains duplicate grant path `{grantor}` -> `{}`",
33 entry.role
34 ));
35 }
36 if entry.privileges.is_empty() && entry.grant_options.is_empty() {
37 return Err("ACL contains an empty grant path".into());
38 }
39 if entry.role == "PUBLIC" && !entry.grant_options.is_empty() {
40 return Err("PUBLIC cannot hold grant options".into());
41 }
42 for privilege in TableAclPrivilege::ALL {
43 let mask = privilege.mask();
44 if entry.grant_options.intersects(mask) && !entry.privileges.intersects(mask) {
45 return Err("ACL grant option exists without its privilege".into());
46 }
47 if entry.privileges.intersects(mask) || entry.grant_options.intersects(mask) {
48 let reachable = column.map_or_else(
49 || super::grant_option_roles(security, privilege),
50 |column| column_grant_option_roles(security, column, privilege),
51 );
52 if !reachable.contains(grantor) {
53 return Err(format!(
54 "ACL grant path from `{grantor}` is not rooted at owner `{}`",
55 security.role_owner
56 ));
57 }
58 }
59 }
60 }
61 Ok(())
62 };
63
64 if let Some(acl) = security.acl.as_deref() {
65 validate_acl(acl, None)?;
66 }
67 if !security.column_acls.is_empty() && columns.is_none() {
68 return Err("column ACLs require durable public column metadata".into());
69 }
70 for (column, acl) in &security.column_acls {
71 if !columns.is_some_and(|columns| columns.iter().any(|candidate| candidate == column)) {
72 return Err(format!("column ACL references missing column `{column}`"));
73 }
74 for entry in acl {
75 if entry.privileges.delete
76 || entry.privileges.truncate
77 || entry.privileges.trigger
78 || entry.privileges.maintain
79 || entry.grant_options.delete
80 || entry.grant_options.truncate
81 || entry.grant_options.trigger
82 || entry.grant_options.maintain
83 {
84 return Err(format!(
85 "column ACL for `{column}` contains a relation-only privilege"
86 ));
87 }
88 }
89 validate_acl(acl, Some(column))?;
90 }
91 Ok(())
92}