uqa_sql/catalog/security/
schema_binding.rs1use 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 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 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;