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 const BUILTIN_NAMES: [&'static str; 3] = ["pg_catalog", "information_schema", "ag_catalog"];
30
31    /// The initial namespace authority shared by memory and durable catalogs.
32    pub fn initial_catalog() -> BTreeMap<String, Self> {
33        std::iter::once(("public".into(), Self::bootstrap("public")))
34            .chain(
35                Self::BUILTIN_NAMES
36                    .map(|name| (name.into(), Self::builtin(name).expect("built-in schema"))),
37            )
38            .collect()
39    }
40
41    pub fn builtin(name: &str) -> Option<Self> {
42        let mut security = match name {
43            "pg_catalog" | "information_schema" => Self::with_public_privileges(false),
44            "ag_catalog" => Self::bootstrap(name),
45            _ => return None,
46        };
47        security.tuple = Self::bootstrap(name).tuple;
48        Some(security)
49    }
50
51    pub fn namespace_oid(&self, name: &str) -> i64 {
52        self.tuple
53            .map_or_else(|| crate::catalog::oids::schema_oid(name), |tuple| tuple.oid)
54    }
55    pub fn owner(role_owner: RoleIdentity) -> Self {
56        Self {
57            tuple: None,
58            role_owner,
59            acl: None,
60        }
61    }
62
63    pub fn bootstrap(name: &str) -> Self {
64        Self::bootstrap_with_oid(
65            name,
66            u32::try_from(crate::catalog::oids::schema_oid(name)).expect("bootstrap schema OID"),
67        )
68    }
69
70    /// A bootstrap schema whose namespace took `oid`, as a graph's schema takes the OID its creation allocated.
71    pub fn bootstrap_with_oid(name: &str, oid: u32) -> Self {
72        let mut security = Self::from_row(BoundSchemaRow::bootstrap(name)).1;
73        security.tuple = Some(SchemaTupleIdentity::initial(oid));
74        security
75    }
76
77    pub fn with_public_privileges(create: bool) -> Self {
78        let owner = RoleIdentity::BOOTSTRAP;
79        Self {
80            tuple: None,
81            role_owner: owner,
82            acl: Some(vec![
83                BoundAclEntry {
84                    role: Some(owner),
85                    grantor: owner,
86                    privileges: SchemaPrivileges::ALL,
87                    grant_options: SchemaPrivileges::default(),
88                },
89                BoundAclEntry {
90                    role: None,
91                    grantor: owner,
92                    privileges: SchemaPrivileges {
93                        usage: true,
94                        create,
95                    },
96                    grant_options: SchemaPrivileges::default(),
97                },
98            ]),
99        }
100    }
101
102    pub fn bind(
103        security: &SchemaSecurity,
104        roles: &BTreeMap<String, RoleDefinition>,
105    ) -> Result<Self, String> {
106        let bind = |name: &str| bind_role(roles, name, "schema");
107        Ok(Self {
108            tuple: None,
109            role_owner: bind(&security.role_owner)?,
110            acl: security
111                .acl
112                .as_ref()
113                .map(|entries| {
114                    entries
115                        .iter()
116                        .map(|entry| {
117                            Ok(BoundAclEntry {
118                                role: entry.role.role_name().map(bind).transpose()?,
119                                grantor: bind(
120                                    entry.grantor.as_deref().unwrap_or(&security.role_owner),
121                                )?,
122                                privileges: entry.privileges,
123                                grant_options: entry.grant_options,
124                            })
125                        })
126                        .collect::<Result<_, String>>()
127                })
128                .transpose()?,
129        })
130    }
131
132    pub fn resolve(
133        &self,
134        roles: &BTreeMap<String, RoleDefinition>,
135    ) -> Result<SchemaSecurity, String> {
136        let name = |identity| role_name(roles, identity, "schema").map(str::to_owned);
137        Ok(SchemaSecurity {
138            role_owner: name(self.role_owner)?,
139            acl: self
140                .acl
141                .as_ref()
142                .map(|entries| {
143                    entries
144                        .iter()
145                        .map(|entry| {
146                            Ok(SchemaAclEntry {
147                                role: entry
148                                    .role
149                                    .map(name)
150                                    .transpose()?
151                                    .map_or(AclGrantee::Public, AclGrantee::Role),
152                                grantor: Some(name(entry.grantor)?),
153                                privileges: entry.privileges,
154                                grant_options: entry.grant_options,
155                            })
156                        })
157                        .collect::<Result<_, String>>()
158                })
159                .transpose()?,
160        })
161    }
162
163    pub fn validate(&self, roles: &BTreeMap<String, RoleDefinition>) -> Result<(), String> {
164        if self.tuple.is_some_and(|tuple| !tuple.is_valid()) {
165            return Err("invalid schema catalog tuple identity".into());
166        }
167        role_name(roles, self.role_owner, "schema")?;
168        for entry in self.acl.iter().flatten() {
169            if let Some(grantee) = entry.role {
170                role_name(roles, grantee, "schema")?;
171            }
172            role_name(roles, entry.grantor, "schema")?;
173        }
174        Ok(())
175    }
176
177    pub fn depends_on(&self, role: RoleIdentity) -> bool {
178        self.role_owner == role
179            || self
180                .acl
181                .iter()
182                .flatten()
183                .any(|entry| entry.role == Some(role) || entry.grantor == role)
184    }
185
186    pub fn from_row(row: BoundSchemaRow) -> (String, Self) {
187        (
188            row.name,
189            Self {
190                tuple: row.tuple,
191                role_owner: row.role_owner,
192                acl: row.acl,
193            },
194        )
195    }
196
197    pub fn row(&self, name: impl Into<String>) -> BoundSchemaRow {
198        BoundSchemaRow {
199            name: name.into(),
200            tuple: self.tuple,
201            role_owner: self.role_owner,
202            acl: self.acl.clone(),
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests;