Skip to main content

uqa_sql/catalog/security/
schema_binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Schema registries retain role incarnations while command views use current names.
8
9use super::{
10    role_bindings::{bind_role, role_name},
11    SchemaSecurity,
12};
13use crate::catalog::roles::{RoleDefinition, RoleIdentity};
14use std::collections::BTreeMap;
15use uqa_core::{
16    catalog_acl::AclGrantee,
17    catalog_role::BoundAclEntry,
18    catalog_schema::{BoundSchemaRow, SchemaAclEntry, SchemaPrivileges, SchemaTupleIdentity},
19};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct BoundSchemaSecurity {
23    pub tuple: Option<SchemaTupleIdentity>,
24    pub role_owner: RoleIdentity,
25    pub acl: Option<Vec<BoundAclEntry<SchemaPrivileges>>>,
26}
27
28impl BoundSchemaSecurity {
29    pub fn namespace_oid(&self, name: &str) -> i64 {
30        self.tuple
31            .map_or_else(|| crate::catalog::oids::schema_oid(name), |tuple| tuple.oid)
32    }
33    pub fn owner(role_owner: RoleIdentity) -> Self {
34        Self {
35            tuple: None,
36            role_owner,
37            acl: None,
38        }
39    }
40
41    pub fn bootstrap(name: &str) -> Self {
42        let mut security = Self::from_row(BoundSchemaRow::bootstrap(name)).1;
43        security.tuple = Some(SchemaTupleIdentity::initial(
44            u32::try_from(crate::catalog::oids::schema_oid(name)).expect("bootstrap schema OID"),
45        ));
46        security
47    }
48
49    pub fn with_public_privileges(create: bool) -> Self {
50        let owner = RoleIdentity::BOOTSTRAP;
51        Self {
52            tuple: None,
53            role_owner: owner,
54            acl: Some(vec![
55                BoundAclEntry {
56                    role: Some(owner),
57                    grantor: owner,
58                    privileges: SchemaPrivileges::ALL,
59                    grant_options: SchemaPrivileges::default(),
60                },
61                BoundAclEntry {
62                    role: None,
63                    grantor: owner,
64                    privileges: SchemaPrivileges {
65                        usage: true,
66                        create,
67                    },
68                    grant_options: SchemaPrivileges::default(),
69                },
70            ]),
71        }
72    }
73
74    pub fn bind(
75        security: &SchemaSecurity,
76        roles: &BTreeMap<String, RoleDefinition>,
77    ) -> Result<Self, String> {
78        let bind = |name: &str| bind_role(roles, name, "schema");
79        Ok(Self {
80            tuple: None,
81            role_owner: bind(&security.role_owner)?,
82            acl: security
83                .acl
84                .as_ref()
85                .map(|entries| {
86                    entries
87                        .iter()
88                        .map(|entry| {
89                            Ok(BoundAclEntry {
90                                role: entry.role.role_name().map(bind).transpose()?,
91                                grantor: bind(
92                                    entry.grantor.as_deref().unwrap_or(&security.role_owner),
93                                )?,
94                                privileges: entry.privileges,
95                                grant_options: entry.grant_options,
96                            })
97                        })
98                        .collect::<Result<_, String>>()
99                })
100                .transpose()?,
101        })
102    }
103
104    pub fn resolve(
105        &self,
106        roles: &BTreeMap<String, RoleDefinition>,
107    ) -> Result<SchemaSecurity, String> {
108        let name = |identity| role_name(roles, identity, "schema").map(str::to_owned);
109        Ok(SchemaSecurity {
110            role_owner: name(self.role_owner)?,
111            acl: self
112                .acl
113                .as_ref()
114                .map(|entries| {
115                    entries
116                        .iter()
117                        .map(|entry| {
118                            Ok(SchemaAclEntry {
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<_, String>>()
130                })
131                .transpose()?,
132        })
133    }
134
135    pub fn validate(&self, roles: &BTreeMap<String, RoleDefinition>) -> Result<(), String> {
136        if self.tuple.is_some_and(|tuple| !tuple.is_valid()) {
137            return Err("invalid schema catalog tuple identity".into());
138        }
139        role_name(roles, self.role_owner, "schema")?;
140        for entry in self.acl.iter().flatten() {
141            if let Some(grantee) = entry.role {
142                role_name(roles, grantee, "schema")?;
143            }
144            role_name(roles, entry.grantor, "schema")?;
145        }
146        Ok(())
147    }
148
149    pub fn depends_on(&self, role: RoleIdentity) -> bool {
150        self.role_owner == role
151            || self
152                .acl
153                .iter()
154                .flatten()
155                .any(|entry| entry.role == Some(role) || entry.grantor == role)
156    }
157
158    pub fn from_row(row: BoundSchemaRow) -> (String, Self) {
159        (
160            row.name,
161            Self {
162                tuple: row.tuple,
163                role_owner: row.role_owner,
164                acl: row.acl,
165            },
166        )
167    }
168
169    pub fn row(&self, name: impl Into<String>) -> BoundSchemaRow {
170        BoundSchemaRow {
171            name: name.into(),
172            tuple: self.tuple,
173            role_owner: self.role_owner,
174            acl: self.acl.clone(),
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests;