uqa_sql/catalog/security/
schema_inquiry.rs1use super::{
10 schema::{
11 parse_privilege_checks, role_has_schema_privilege, role_has_schema_privilege_check,
12 SchemaAclPrivilege,
13 },
14 BoundSchemaSecurity,
15};
16use crate::catalog::roles::identity::RoleSubject;
17use crate::catalog::roles::RoleReference;
18use crate::catalog::temporary_namespace::{TemporaryNamespace, TemporaryNamespaceOids};
19use crate::{
20 catalog::roles::{guards::RoleCatalogGuards, RoleDefinition, RoleReferenceNames},
21 SQLError,
22};
23use std::collections::{BTreeMap, BTreeSet};
24use uqa_core::Value;
25
26pub type SchemaRegistryRead<'a> =
27 Box<dyn std::ops::Deref<Target = BTreeMap<String, BoundSchemaSecurity>> + 'a>;
28
29pub trait GraphNamespaceRead {
31 fn names(&self) -> Box<dyn Iterator<Item = &str> + '_>;
32 fn contains(&self, name: &str) -> bool;
33 fn namespace_oid(&self, name: &str) -> i64 {
35 crate::catalog::oids::schema_oid(name)
36 }
37}
38
39pub trait SchemaPrivilegeCatalog {
40 fn refresh_namespace_catalog(&self) -> Result<(), SQLError>;
41 fn schemas(&self) -> SchemaRegistryRead<'_>;
42 fn graphs(&self) -> Box<dyn GraphNamespaceRead + '_>;
43 fn temporary_namespace_oids(&self) -> Option<TemporaryNamespaceOids>;
45 fn temporary_namespace_allocated(&self) -> bool {
46 self.temporary_namespace_oids().is_some()
47 }
48 fn temporary_schema_name(&self) -> String;
49}
50
51pub struct SchemaPrivilegeInquiry<'a> {
52 pub catalog: &'a dyn SchemaPrivilegeCatalog,
53 pub names: &'a dyn RoleReferenceNames,
54 pub roles: &'a dyn RoleCatalogGuards,
55}
56
57impl SchemaPrivilegeInquiry<'_> {
58 pub fn schema_has_privilege_for_role(
59 &self,
60 schema: &str,
61 role: &(impl RoleSubject + ?Sized),
62 privilege: SchemaAclPrivilege,
63 ) -> bool {
64 let Some(security) = self.schema_security_for_privilege(schema) else {
65 return false;
66 };
67 let roles = self.roles.role_definitions();
68 let memberships = self.roles.role_memberships();
69 security.resolve(&roles).is_ok_and(|security| {
70 role_has_schema_privilege(&security, role, privilege, &roles, &memberships)
71 })
72 }
73
74 pub fn schema_security_for_privilege(&self, schema: &str) -> Option<BoundSchemaSecurity> {
75 if let Some(security) = self.catalog.schemas().get(schema) {
76 return Some(security.clone());
77 }
78 let graphs = self.catalog.graphs();
79 if graphs.contains(schema) && schema != self.catalog.temporary_schema_name() {
80 let oid = u32::try_from(graphs.namespace_oid(schema)).ok()?;
81 return Some(BoundSchemaSecurity::bootstrap_with_oid(schema, oid));
82 }
83 if let Some(temporary) = self.temporary_namespace() {
85 if schema == temporary.toast_schema() {
86 return Some(BoundSchemaSecurity::bootstrap_with_oid(
87 schema,
88 temporary.oids.toast_namespace,
89 ));
90 }
91 if schema == temporary.schema {
92 let mut security = BoundSchemaSecurity::with_public_privileges(true);
93 security.tuple = Some(uqa_core::catalog_schema::SchemaTupleIdentity::initial(
94 temporary.oids.namespace,
95 ));
96 return Some(security);
97 }
98 }
99 BoundSchemaSecurity::builtin(schema)
100 }
101
102 pub fn temporary_namespace(&self) -> Option<TemporaryNamespace> {
104 self.catalog
105 .temporary_namespace_oids()
106 .map(|oids| TemporaryNamespace {
107 schema: self.catalog.temporary_schema_name(),
108 oids,
109 })
110 }
111
112 pub fn require_schema_privilege(
113 &self,
114 schema: &str,
115 role: &(impl RoleSubject + ?Sized),
116 privilege: SchemaAclPrivilege,
117 ) -> Result<(), SQLError> {
118 if self.schema_has_privilege_for_role(schema, role, privilege) {
119 return Ok(());
120 }
121 Err(SQLError::Routine {
122 sqlstate: "42501".into(),
123 message: format!("permission denied for schema {schema}"),
124 })
125 }
126
127 pub fn has_schema_privilege_value(&self, arguments: &[Value]) -> Result<Value, SQLError> {
128 if arguments.iter().any(|argument| argument == &Value::Null) {
129 return Ok(Value::Null);
130 }
131 let (subject_value, schema_value, privilege_value) = match arguments {
132 [schema, privilege] => (None, schema, privilege),
133 [subject, schema, privilege] => (Some(subject), schema, privilege),
134 _ => {
135 return Err(SQLError::BadArity {
136 name: "has_schema_privilege".into(),
137 expected: "2 or 3".into(),
138 actual: arguments.len(),
139 })
140 }
141 };
142 let current_user = subject_value.is_none().then(|| self.names.current_role());
143 let subject = {
144 let roles = self.roles.role_definitions();
145 subject_value.map_or_else(
146 || Ok(current_user),
147 |value| {
148 resolve_schema_privilege_role(value, &roles)
149 .map(|role| role.map(RoleReference::from))
150 },
151 )?
152 };
153 let schema = self.resolve_schema_privilege_target(schema_value)?;
154 let privilege = match privilege_value {
155 Value::Str(privilege) | Value::FixedChar(privilege) => privilege,
156 other => {
157 return Err(SQLError::TypeMismatch(format!(
158 "has_schema_privilege privilege must be text, got {other:?}"
159 )))
160 }
161 };
162 let checks = parse_privilege_checks(privilege)?;
163 let roles = self.roles.role_definitions();
164 let memberships = self.roles.role_memberships();
165 let subject_is_superuser = subject.as_ref().is_some_and(|subject| {
166 subject
167 .role_definition(&roles)
168 .is_some_and(|role| role.has(crate::ast::RoleAttribute::Superuser))
169 });
170 let Some(schema) = schema else {
171 return if subject_is_superuser {
172 Ok(Value::Bool(true))
173 } else {
174 Ok(Value::Null)
175 };
176 };
177 let Some(subject) = subject else {
178 return Ok(Value::Bool(false));
179 };
180 let security = self.schema_security_for_privilege(&schema).ok_or_else(|| {
181 SQLError::Internal(format!("schema `{schema}` has no security metadata"))
182 })?;
183 let security = security.resolve(&roles).map_err(SQLError::Internal)?;
184 Ok(Value::Bool(checks.into_iter().any(|check| {
185 role_has_schema_privilege_check(&security, &subject, check, &roles, &memberships)
186 })))
187 }
188
189 fn resolve_schema_privilege_target(&self, value: &Value) -> Result<Option<String>, SQLError> {
190 let names = self.schema_privilege_namespace_names()?;
191 match value {
192 Value::Str(name) | Value::FixedChar(name) => {
193 if names.contains(name) {
194 Ok(Some(name.clone()))
195 } else {
196 Err(SQLError::Routine {
197 sqlstate: "3F000".into(),
198 message: format!("schema \"{name}\" does not exist"),
199 })
200 }
201 }
202 Value::Int(oid) => Ok(names.into_iter().find(|name| {
203 self.schema_security_for_privilege(name)
204 .is_some_and(|security| security.namespace_oid(name) == *oid)
205 })),
206 other => Err(SQLError::TypeMismatch(format!(
207 "has_schema_privilege schema must be text or oid, got {other:?}"
208 ))),
209 }
210 }
211
212 fn schema_privilege_namespace_names(&self) -> Result<BTreeSet<String>, SQLError> {
213 self.catalog.refresh_namespace_catalog()?;
214 let mut names = BTreeSet::from([
215 "pg_catalog".to_string(),
216 "information_schema".to_string(),
217 "ag_catalog".to_string(),
218 ]);
219 names.extend(self.catalog.schemas().keys().cloned());
220 names.extend(self.catalog.graphs().names().map(str::to_owned));
221 if let Some(temporary) = self.temporary_namespace() {
222 names.insert(temporary.toast_schema());
223 names.insert(temporary.schema);
224 }
225 Ok(names)
226 }
227}
228
229fn resolve_schema_privilege_role(
230 value: &Value,
231 roles: &BTreeMap<String, RoleDefinition>,
232) -> Result<Option<String>, SQLError> {
233 match value {
234 Value::Str(name) | Value::FixedChar(name) => {
235 if roles.contains_key(name) {
236 Ok(Some(name.clone()))
237 } else {
238 Err(SQLError::Routine {
239 sqlstate: "42704".into(),
240 message: format!("role \"{name}\" does not exist"),
241 })
242 }
243 }
244 Value::Int(oid) => Ok(roles
245 .values()
246 .find(|role| role.oid == *oid)
247 .map(|role| role.name.clone())),
248 other => Err(SQLError::TypeMismatch(format!(
249 "has_schema_privilege role must be name or oid, got {other:?}"
250 ))),
251 }
252}