Skip to main content

uqa_sql/catalog/security/
table_binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Table and column ACLs retain role incarnations independently of display names.
8
9use super::{role_bindings, TableAclEntry, TablePrivileges, TableSecurity};
10use crate::catalog::roles::{identity::RoleBinding, RoleDefinition, RoleIdentity, RoleReference};
11use std::collections::BTreeMap;
12use uqa_core::catalog_acl::AclGrantee;
13use uqa_core::catalog_role::BoundAclEntry;
14
15pub type BoundTableAclEntry = BoundAclEntry<TablePrivileges>;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct BoundTableSecurity {
19    pub role_owner: RoleIdentity,
20    pub acl: Option<Vec<BoundTableAclEntry>>,
21    pub column_acls: BTreeMap<String, Vec<BoundTableAclEntry>>,
22    pub acl_revisions: uqa_core::catalog_acl::RelationAclRevisions,
23}
24
25impl BoundTableSecurity {
26    pub fn remove_column_acl(&mut self, column: &str) {
27        self.column_acls.remove(column);
28        self.acl_revisions.columns.remove(column);
29    }
30
31    pub fn rename_column_acl(&mut self, from: &str, to: &str) {
32        if let Some(acl) = self.column_acls.remove(from) {
33            self.column_acls.insert(to.to_owned(), acl);
34        }
35        if let Some(revision) = self.acl_revisions.columns.remove(from) {
36            self.acl_revisions.columns.insert(to.to_owned(), revision);
37        }
38    }
39
40    pub fn from_row(row: uqa_core::catalog_acl::BoundRelationSecurity) -> Self {
41        Self {
42            role_owner: row.role_owner,
43            acl: row.acl,
44            column_acls: row.column_acls,
45            acl_revisions: row.acl_revisions,
46        }
47    }
48
49    pub fn row(&self) -> uqa_core::catalog_acl::BoundRelationSecurity {
50        uqa_core::catalog_acl::BoundRelationSecurity {
51            role_owner: self.role_owner,
52            acl: self.acl.clone(),
53            column_acls: self.column_acls.clone(),
54            acl_revisions: self.acl_revisions.clone(),
55        }
56    }
57
58    pub fn owner(role_owner: RoleIdentity) -> Self {
59        Self {
60            role_owner,
61            acl: None,
62            column_acls: BTreeMap::new(),
63            acl_revisions: uqa_core::catalog_acl::RelationAclRevisions::default(),
64        }
65    }
66
67    pub fn owner_reference(
68        &self,
69        roles: &BTreeMap<String, RoleDefinition>,
70    ) -> Result<RoleReference, crate::SQLError> {
71        let name = role_bindings::role_name(roles, self.role_owner, "table")
72            .map_err(crate::SQLError::Internal)?;
73        Ok(RoleReference::Bound(std::sync::Arc::new(
74            RoleBinding::from_definition(&roles[name])?,
75        )))
76    }
77
78    pub fn bind(
79        security: &TableSecurity,
80        roles: &BTreeMap<String, RoleDefinition>,
81    ) -> Result<Self, String> {
82        let bind = |name: &str| role_bindings::bind_role(roles, name, "table");
83        let entries = |entries: &[TableAclEntry]| {
84            entries
85                .iter()
86                .map(|entry| {
87                    Ok(BoundTableAclEntry {
88                        role: entry.role.role_name().map(bind).transpose()?,
89                        grantor: bind(entry.grantor.as_deref().unwrap_or(&security.role_owner))?,
90                        privileges: entry.privileges,
91                        grant_options: entry.grant_options,
92                    })
93                })
94                .collect::<Result<Vec<_>, String>>()
95        };
96        Ok(Self {
97            role_owner: bind(&security.role_owner)?,
98            acl_revisions: uqa_core::catalog_acl::RelationAclRevisions::default(),
99            acl: security.acl.as_deref().map(entries).transpose()?,
100            column_acls: security
101                .column_acls
102                .iter()
103                .map(|(column, acl)| Ok((column.clone(), entries(acl)?)))
104                .collect::<Result<_, String>>()?,
105        })
106    }
107
108    /// Resolve names only from the role view accompanying this security snapshot.
109    pub fn resolve(
110        &self,
111        roles: &BTreeMap<String, RoleDefinition>,
112    ) -> Result<TableSecurity, String> {
113        let name = |identity| role_bindings::role_name(roles, identity, "table").map(str::to_owned);
114        let entries = |entries: &[BoundTableAclEntry]| {
115            entries
116                .iter()
117                .map(|entry| {
118                    Ok(TableAclEntry {
119                        role: entry
120                            .role
121                            .map(&name)
122                            .transpose()?
123                            .map_or(AclGrantee::Public, AclGrantee::Role),
124                        grantor: Some(name(entry.grantor)?),
125                        privileges: entry.privileges,
126                        grant_options: entry.grant_options,
127                    })
128                })
129                .collect::<Result<Vec<_>, String>>()
130        };
131        Ok(TableSecurity {
132            role_owner: name(self.role_owner)?,
133            acl: self.acl.as_deref().map(entries).transpose()?,
134            column_acls: self
135                .column_acls
136                .iter()
137                .map(|(column, acl)| Ok((column.clone(), entries(acl)?)))
138                .collect::<Result<_, String>>()?,
139        })
140    }
141
142    pub fn validate(
143        &self,
144        columns: Option<&[String]>,
145        roles: &BTreeMap<String, RoleDefinition>,
146    ) -> Result<(), String> {
147        if self.acl_revisions.relation == Some([0; 16])
148            || self.acl_revisions.columns.iter().any(|(column, revision)| {
149                *revision == [0; 16] || columns.is_some_and(|columns| !columns.contains(column))
150            })
151        {
152            return Err("invalid relation or attribute ACL tuple identity".into());
153        }
154        super::table::validate_table_security_invariants(&self.resolve(roles)?, columns, roles)
155    }
156
157    pub fn depends_on(&self, role: RoleIdentity) -> bool {
158        self.role_owner == role
159            || self
160                .acl
161                .iter()
162                .flatten()
163                .chain(self.column_acls.values().flatten())
164                .any(|entry| entry.role == Some(role) || entry.grantor == role)
165    }
166}
167
168#[cfg(test)]
169mod tests;