uqa_sql/catalog/security/
system_relations.rs1use super::{
10 table::{
11 role_has_privilege, validate_table_security_invariants, TableAclPrivilege,
12 TablePrivilegeCheck,
13 },
14 BoundTableSecurity, TableSecurity,
15};
16use crate::catalog::roles::identity::RoleSubject;
17use crate::{
18 ast::RoleAttribute,
19 catalog::{
20 roles::{RoleDefinition, RoleMembership, RoleMembershipKey},
21 SystemRelation,
22 },
23};
24use std::{collections::BTreeMap, ops::Deref};
25use uqa_core::RelationIdentity;
26
27#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub struct SystemAcl {
30 pub revision: [u8; 16],
31 pub acl: Vec<super::table_binding::BoundTableAclEntry>,
32}
33#[derive(Clone, Debug, Default, PartialEq, Eq)]
34pub struct SystemRelationSecurity {
35 pub table: Option<SystemAcl>,
36 pub columns: BTreeMap<String, SystemAcl>,
37}
38impl SystemRelationSecurity {
39 pub fn security(&self, relation: SystemRelation) -> BoundTableSecurity {
40 let mut security = relation.bootstrap_security();
41 if let Some(table) = &self.table {
42 security.acl = Some(table.acl.clone());
43 }
44 security.column_acls = self
45 .columns
46 .iter()
47 .filter(|(_, value)| !value.acl.is_empty())
48 .map(|(name, value)| (name.clone(), value.acl.clone()))
49 .collect();
50 security
51 }
52 pub fn entry(&self, column: Option<&str>) -> Option<&SystemAcl> {
53 column.map_or_else(|| self.table.as_ref(), |column| self.columns.get(column))
54 }
55}
56pub type SystemRelationSecurities = BTreeMap<RelationIdentity, SystemRelationSecurity>;
57pub type SystemRelationSecurityRead<'a> = Box<dyn Deref<Target = SystemRelationSecurities> + 'a>;
58
59pub trait SystemRelationSecurityCatalog {
60 fn system_relation_securities(&self) -> SystemRelationSecurityRead<'_>;
61 fn system_relation_security(&self, relation: SystemRelation) -> BoundTableSecurity {
62 security(&self.system_relation_securities(), relation)
63 }
64}
65
66pub fn security(
67 securities: &SystemRelationSecurities,
68 relation: SystemRelation,
69) -> BoundTableSecurity {
70 securities
71 .get(&RelationIdentity::new(
72 relation.namespace(),
73 relation.name(),
74 ))
75 .map(|entry| entry.security(relation))
76 .unwrap_or_else(|| relation.bootstrap_security())
77}
78
79pub const METADATA_PREFIX: &str = "uqa.system_relation_security.v1:";
80
81pub fn metadata_key(relation: SystemRelation, column: Option<&str>) -> String {
82 format!(
84 "{METADATA_PREFIX}{}:{}",
85 relation.qualified_name(),
86 column.unwrap_or("")
87 )
88}
89
90pub fn validate_security(
91 relation: SystemRelation,
92 security: &TableSecurity,
93 roles: &BTreeMap<String, RoleDefinition>,
94) -> Result<(), String> {
95 if super::role_bindings::bind_role(roles, &security.role_owner, "system relation")?
96 != crate::catalog::roles::RoleIdentity::BOOTSTRAP
97 {
98 return Err(format!(
99 "system relation `{}` has an invalid owner",
100 relation.qualified_name()
101 ));
102 }
103 validate_table_security_invariants(security, Some(&relation.column_names()), roles)
104}
105
106fn masks_table_write(
107 relation: SystemRelation,
108 subject: &(impl RoleSubject + ?Sized),
109 check: TablePrivilegeCheck,
110 roles: &BTreeMap<String, RoleDefinition>,
111) -> bool {
112 relation.namespace() == "pg_catalog"
113 && relation.kind() == "table"
114 && !check.grant_option
115 && matches!(
116 check.privilege,
117 TableAclPrivilege::Insert
118 | TableAclPrivilege::Update
119 | TableAclPrivilege::Delete
120 | TableAclPrivilege::Truncate
121 )
122 && !subject
123 .role_definition(roles)
124 .is_some_and(|role| role.has(RoleAttribute::Superuser))
125}
126
127pub fn has_table_privilege(
128 relation: SystemRelation,
129 security: &TableSecurity,
130 subject: &(impl RoleSubject + ?Sized),
131 check: TablePrivilegeCheck,
132 roles: &BTreeMap<String, RoleDefinition>,
133 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
134) -> bool {
135 !masks_table_write(relation, subject, check, roles)
136 && role_has_privilege(security, subject, check, roles, memberships)
137}
138
139pub fn has_column_privilege(
140 relation: SystemRelation,
141 security: &TableSecurity,
142 column: &str,
143 subject: &(impl RoleSubject + ?Sized),
144 check: TablePrivilegeCheck,
145 roles: &BTreeMap<String, RoleDefinition>,
146 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
147) -> bool {
148 if !masks_table_write(relation, subject, check, roles) {
149 return super::columns::role_has_column_privilege(
150 security,
151 column,
152 subject,
153 check,
154 roles,
155 memberships,
156 );
157 }
158 security.column_acls.get(column).is_some_and(|acl| {
160 acl.iter().any(|entry| {
161 entry.privileges.intersects(check.privilege.mask())
162 && (entry.role.is_public()
163 || crate::catalog::roles::role_inherits(
164 roles,
165 memberships,
166 subject,
167 &entry.role,
168 ))
169 })
170 })
171}
172
173#[cfg(test)]
174mod tests;