1use super::{
10 columns::role_has_column_privilege as column_privilege_check,
11 sequence_inquiry::SequencePrivilegeInquiry,
12 table::{parse_column_privilege_checks, parse_privilege_checks, role_has_privilege},
13 TableSecurity,
14};
15use crate::{
16 catalog::{
17 resolution::RelationResolution,
18 roles::{guards::RoleCatalogGuards, RoleDefinition, RoleReferenceNames},
19 },
20 SQLError,
21};
22use std::collections::BTreeMap;
23use uqa_core::{RelationIdentity, Value};
24
25pub trait TablePrivilegeCatalog {
26 fn visible_relation_kind(&self, reference: &str) -> Result<RelationResolution, SQLError>;
27 fn resolve_table_privilege_oid(
28 &self,
29 oid: i64,
30 ) -> Result<Option<ResolvedTablePrivilegeTarget>, SQLError>;
31 fn table_privilege_security(
32 &self,
33 target: &ResolvedTablePrivilegeTarget,
34 ) -> Result<TableSecurity, SQLError>;
35 fn column_privilege_relation(
36 &self,
37 target: &ResolvedTablePrivilegeTarget,
38 ) -> Result<ColumnPrivilegeRelation, SQLError>;
39}
40pub struct TablePrivilegeInquiry<'a> {
41 pub names: &'a dyn RoleReferenceNames,
42 pub roles: &'a dyn RoleCatalogGuards,
43 pub sequences: &'a SequencePrivilegeInquiry<'a>,
44 pub catalog: &'a dyn TablePrivilegeCatalog,
45}
46
47pub enum ResolvedTablePrivilegeTarget {
48 Table(RelationIdentity),
49 View(RelationIdentity),
50 ForeignTable(RelationIdentity),
51 Sequence(RelationIdentity),
52}
53
54enum ResolvedColumnPrivilegeTarget {
55 User(String),
56 System,
57}
58
59const POSTGRES_SYSTEM_COLUMNS: [&str; 6] = ["ctid", "xmin", "cmin", "xmax", "cmax", "tableoid"];
60
61pub struct ColumnPrivilegeRelation {
62 pub relation: RelationIdentity,
63 pub security: TableSecurity,
64 pub columns: Vec<String>,
65 pub has_system_columns: bool,
66}
67
68fn resolve_column_privilege_target(
69 relation: &RelationIdentity,
70 columns: &[String],
71 has_system_columns: bool,
72 value: &Value,
73) -> Result<Option<ResolvedColumnPrivilegeTarget>, SQLError> {
74 match value {
75 Value::Str(column) | Value::FixedChar(column) => {
76 if columns.iter().any(|definition| definition == column) {
77 Ok(Some(ResolvedColumnPrivilegeTarget::User(column.clone())))
78 } else if has_system_columns && POSTGRES_SYSTEM_COLUMNS.contains(&column.as_str()) {
79 Ok(Some(ResolvedColumnPrivilegeTarget::System))
80 } else {
81 Err(SQLError::Routine {
82 sqlstate: "42703".into(),
83 message: format!(
84 "column \"{column}\" of relation \"{}\" does not exist",
85 relation.name
86 ),
87 })
88 }
89 }
90 Value::Int(attnum) if *attnum > 0 => Ok(usize::try_from(*attnum - 1)
91 .ok()
92 .and_then(|index| columns.get(index))
93 .map(|column| ResolvedColumnPrivilegeTarget::User(column.clone()))),
94 Value::Int(attnum) if has_system_columns && (-6..=-1).contains(attnum) => {
95 Ok(Some(ResolvedColumnPrivilegeTarget::System))
96 }
97 Value::Int(_) => Ok(None),
98 other => Err(SQLError::TypeMismatch(format!(
99 "has_column_privilege column must be text or smallint, got {other:?}"
100 ))),
101 }
102}
103
104fn resolve_table_privilege_role(
105 value: &Value,
106 roles: &BTreeMap<String, RoleDefinition>,
107) -> Result<Option<String>, SQLError> {
108 match value {
109 Value::Str(name) | Value::FixedChar(name) => {
110 if roles.contains_key(name) {
111 Ok(Some(name.clone()))
112 } else {
113 Err(SQLError::Routine {
114 sqlstate: "42704".into(),
115 message: format!("role \"{name}\" does not exist"),
116 })
117 }
118 }
119 Value::Int(oid) => Ok(roles
120 .values()
121 .find(|role| role.oid == *oid)
122 .map(|role| role.name.clone())),
123 other => Err(SQLError::TypeMismatch(format!(
124 "has_table_privilege role must be name or oid, got {other:?}"
125 ))),
126 }
127}
128
129fn column_privilege_arguments(
130 arguments: &[Value],
131) -> Result<(Option<&Value>, &Value, &Value, &Value), SQLError> {
132 match arguments {
133 [table, column, privilege] => Ok((None, table, column, privilege)),
134 [subject, table, column, privilege] => Ok((Some(subject), table, column, privilege)),
135 _ => Err(SQLError::BadArity {
136 name: "has_column_privilege".into(),
137 expected: "3 or 4".into(),
138 actual: arguments.len(),
139 }),
140 }
141}
142
143impl TablePrivilegeInquiry<'_> {
144 pub fn has_table_privilege_value(&self, arguments: &[Value]) -> Result<Value, SQLError> {
145 if arguments.iter().any(|argument| argument == &Value::Null) {
146 return Ok(Value::Null);
147 }
148 let (subject_value, table_value, privilege_value) = match arguments {
149 [table, privilege] => (None, table, privilege),
150 [subject, table, privilege] => (Some(subject), table, privilege),
151 _ => {
152 return Err(SQLError::BadArity {
153 name: "has_table_privilege".into(),
154 expected: "2 or 3".into(),
155 actual: arguments.len(),
156 })
157 }
158 };
159 let current_user = subject_value
160 .is_none()
161 .then(|| self.names.current_user_name());
162 let subject = {
163 let roles = self.roles.role_definitions();
164 subject_value.map_or_else(
165 || Ok(current_user),
166 |value| resolve_table_privilege_role(value, &roles),
167 )?
168 };
169 let Some(target) = self.resolve_table_privilege_target(table_value)? else {
170 return Ok(Value::Null);
171 };
172 let privilege = match privilege_value {
173 Value::Str(privilege) | Value::FixedChar(privilege) => privilege,
174 other => {
175 return Err(SQLError::TypeMismatch(format!(
176 "has_table_privilege privilege must be text, got {other:?}"
177 )))
178 }
179 };
180 let checks = parse_privilege_checks(privilege)?;
181 let Some(subject) = subject else {
182 return Ok(Value::Bool(false));
183 };
184 if let ResolvedTablePrivilegeTarget::Sequence(relation) = &target {
185 for check in checks {
186 if self.sequences.role_has_sequence_table_privilege(
187 relation,
188 &subject,
189 check.privilege,
190 check.grant_option,
191 )? {
192 return Ok(Value::Bool(true));
193 }
194 }
195 return Ok(Value::Bool(false));
196 }
197 let security = self.catalog.table_privilege_security(&target)?;
198 let roles = self.roles.role_definitions();
199 let memberships = self.roles.role_memberships();
200 Ok(Value::Bool(checks.into_iter().any(|check| {
201 role_has_privilege(&security, &subject, check, &roles, &memberships)
202 })))
203 }
204
205 pub fn has_column_privilege_value(&self, arguments: &[Value]) -> Result<Value, SQLError> {
206 if arguments.iter().any(|argument| argument == &Value::Null) {
207 return Ok(Value::Null);
208 }
209 let (subject_value, table_value, column_value, privilege_value) =
210 column_privilege_arguments(arguments)?;
211 let current_user = subject_value
212 .is_none()
213 .then(|| self.names.current_user_name());
214 let subject = {
215 let roles = self.roles.role_definitions();
216 subject_value.map_or_else(
217 || Ok(current_user),
218 |value| resolve_table_privilege_role(value, &roles),
219 )?
220 };
221 let Some(target) = self.resolve_table_privilege_target(table_value)? else {
222 return Ok(Value::Null);
223 };
224 if let ResolvedTablePrivilegeTarget::Sequence(relation) = &target {
225 return self.has_sequence_column_privilege_value(
226 relation,
227 subject.as_deref(),
228 column_value,
229 privilege_value,
230 );
231 }
232 let metadata = self.catalog.column_privilege_relation(&target)?;
233 let Some(column) = resolve_column_privilege_target(
234 &metadata.relation,
235 &metadata.columns,
236 metadata.has_system_columns,
237 column_value,
238 )?
239 else {
240 return Ok(Value::Null);
241 };
242 let privilege = match privilege_value {
243 Value::Str(privilege) | Value::FixedChar(privilege) => privilege,
244 other => {
245 return Err(SQLError::TypeMismatch(format!(
246 "has_column_privilege privilege must be text, got {other:?}"
247 )))
248 }
249 };
250 let checks = parse_column_privilege_checks(privilege)?;
251 let Some(subject) = subject else {
252 return Ok(Value::Bool(false));
253 };
254 let roles = self.roles.role_definitions();
255 let memberships = self.roles.role_memberships();
256 Ok(Value::Bool(checks.into_iter().any(|check| match &column {
257 ResolvedColumnPrivilegeTarget::User(column) => column_privilege_check(
258 &metadata.security,
259 column,
260 &subject,
261 check,
262 &roles,
263 &memberships,
264 ),
265 ResolvedColumnPrivilegeTarget::System => {
266 role_has_privilege(&metadata.security, &subject, check, &roles, &memberships)
267 }
268 })))
269 }
270
271 fn has_sequence_column_privilege_value(
272 &self,
273 relation: &RelationIdentity,
274 subject: Option<&str>,
275 column_value: &Value,
276 privilege_value: &Value,
277 ) -> Result<Value, SQLError> {
278 let valid_column = match column_value {
279 Value::Str(column) | Value::FixedChar(column) => {
280 matches!(column.as_str(), "last_value" | "log_cnt" | "is_called")
281 || POSTGRES_SYSTEM_COLUMNS.contains(&column.as_str())
282 }
283 Value::Int(attnum) => (1..=3).contains(attnum) || (-6..=-1).contains(attnum),
284 other => {
285 return Err(SQLError::TypeMismatch(format!(
286 "has_column_privilege column must be text or smallint, got {other:?}"
287 )))
288 }
289 };
290 if !valid_column {
291 return match column_value {
292 Value::Str(column) | Value::FixedChar(column) => Err(SQLError::Routine {
293 sqlstate: "42703".into(),
294 message: format!(
295 "column \"{column}\" of relation \"{}\" does not exist",
296 relation.name
297 ),
298 }),
299 Value::Int(_) => Ok(Value::Null),
300 _ => unreachable!("column value type was validated above"),
301 };
302 }
303 let privilege = match privilege_value {
304 Value::Str(privilege) | Value::FixedChar(privilege) => privilege,
305 other => {
306 return Err(SQLError::TypeMismatch(format!(
307 "has_column_privilege privilege must be text, got {other:?}"
308 )))
309 }
310 };
311 let checks = parse_column_privilege_checks(privilege)?;
312 let Some(subject) = subject else {
313 return Ok(Value::Bool(false));
314 };
315 for check in checks {
316 if self.sequences.role_has_sequence_table_privilege(
317 relation,
318 subject,
319 check.privilege,
320 check.grant_option,
321 )? {
322 return Ok(Value::Bool(true));
323 }
324 }
325 Ok(Value::Bool(false))
326 }
327
328 fn resolve_table_privilege_target(
329 &self,
330 value: &Value,
331 ) -> Result<Option<ResolvedTablePrivilegeTarget>, SQLError> {
332 match value {
333 Value::Str(reference) | Value::FixedChar(reference) => {
334 let (name, kind) = match self.catalog.visible_relation_kind(reference)? {
335 RelationResolution::Found(name, kind) => (name, kind),
336 RelationResolution::MissingSchema(schema) => {
337 return Err(SQLError::Routine {
338 sqlstate: "3F000".into(),
339 message: format!("schema \"{schema}\" does not exist"),
340 })
341 }
342 RelationResolution::MissingRelation => {
343 return Err(SQLError::Routine {
344 sqlstate: "42P01".into(),
345 message: format!("relation \"{reference}\" does not exist"),
346 })
347 }
348 };
349 if !matches!(
350 kind,
351 "table" | "view" | "materialized view" | "foreign table" | "sequence"
352 ) {
353 return Err(SQLError::Unsupported(format!(
354 "has_table_privilege for {kind} is not supported"
355 )));
356 }
357 let relation = RelationIdentity::from_legacy_name(&name).map_err(|error| {
358 SQLError::Internal(format!("resolve table `{name}`: {error}"))
359 })?;
360 Ok(Some(match kind {
361 "table" => ResolvedTablePrivilegeTarget::Table(relation),
362 "view" | "materialized view" => ResolvedTablePrivilegeTarget::View(relation),
363 "foreign table" => ResolvedTablePrivilegeTarget::ForeignTable(relation),
364 "sequence" => ResolvedTablePrivilegeTarget::Sequence(relation),
365 _ => unreachable!("relation kind was validated above"),
366 }))
367 }
368 Value::Int(oid) => self.catalog.resolve_table_privilege_oid(*oid),
369 other => Err(SQLError::TypeMismatch(format!(
370 "has_table_privilege table must be text or oid, got {other:?}"
371 ))),
372 }
373 }
374}