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