Skip to main content

uqa_sql/semantics/
mutation_privileges.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Mutation target privilege analysis.
8use crate::{
9    catalog::security::table::TableAclPrivilege,
10    plan::UpdatePlan,
11    semantics::{privileges::TargetSelectPrivilegeRequest, view_privileges::ViewPrivilegeCatalog},
12    SQLError, ScalarExpr,
13};
14pub trait MutationPrivilegeCatalog: ViewPrivilegeCatalog {
15    fn bound_table_column_names(&self, table: &str) -> Result<Vec<String>, SQLError>;
16    fn ensure_table_privilege_for(
17        &self,
18        table: &str,
19        subject: &str,
20        privilege: TableAclPrivilege,
21    ) -> Result<(), SQLError>;
22    fn ensure_column_privilege_for(
23        &self,
24        table: &str,
25        column: &str,
26        subject: &str,
27        privilege: TableAclPrivilege,
28    ) -> Result<(), SQLError>;
29    fn ensure_any_column_privilege_for(
30        &self,
31        table: &str,
32        subject: &str,
33        privilege: TableAclPrivilege,
34    ) -> Result<(), SQLError>;
35}
36pub fn ensure_update_target_privileges<'a>(
37    catalog: &dyn MutationPrivilegeCatalog,
38    statement: &'a UpdatePlan,
39) -> Result<Vec<&'a ScalarExpr>, SQLError> {
40    let privilege_subject = statement
41        .target_privilege_subject
42        .clone()
43        .unwrap_or_else(|| catalog.current_user_name());
44    for assignment in &statement.assignments {
45        catalog.ensure_column_privilege_for(
46            &statement.table,
47            &assignment.column,
48            &privilege_subject,
49            TableAclPrivilege::Update,
50        )?;
51    }
52    let expressions = statement
53        .assignments
54        .iter()
55        .map(|assignment| &assignment.value)
56        .chain(statement.predicate.iter())
57        .chain(
58            statement
59                .returning
60                .iter()
61                .map(|projection| &projection.expr),
62        )
63        .collect::<Vec<_>>();
64    catalog.ensure_target_select(TargetSelectPrivilegeRequest {
65        table: &statement.table,
66        privilege_subject: statement.target_privilege_subject.as_deref(),
67        target_qualifier: &statement.target_qualifier,
68        returning_aliases: &statement.returning_aliases,
69        expressions: &expressions,
70        subqueries: &statement.subqueries,
71        required_columns: &[],
72    })?;
73    Ok(expressions)
74}
75
76/// Analyze INSERT and ON CONFLICT privileges before any trigger or row mutation.
77pub fn ensure_insert_target_privileges(
78    catalog: &dyn MutationPrivilegeCatalog,
79    stmt: &crate::plan::InsertPlan,
80    conflict_update_columns: Option<&[String]>,
81) -> Result<(), SQLError> {
82    use crate::plan::{ConflictActionPlan, ConflictPlan};
83    let default_values =
84        stmt.source.is_none() && stmt.columns.is_empty() && stmt.rows.iter().all(Vec::is_empty);
85    let privilege_subject = stmt
86        .target_privilege_subject
87        .clone()
88        .unwrap_or_else(|| catalog.current_user_name());
89    if default_values {
90        catalog.ensure_any_column_privilege_for(
91            &stmt.table,
92            &privilege_subject,
93            TableAclPrivilege::Insert,
94        )?;
95    } else {
96        let insert_columns = if stmt.columns.is_empty() {
97            let supplied = stmt.source.as_deref().map_or_else(
98                || stmt.rows.first().map(Vec::len),
99                |source| {
100                    crate::semantics::projection::query_plan_output_columns(source)
101                        .map(|columns| columns.len())
102                },
103            );
104            let columns = catalog.bound_table_column_names(&stmt.table)?;
105            match supplied {
106                Some(supplied) => columns.into_iter().take(supplied).collect(),
107                None => columns,
108            }
109        } else {
110            stmt.columns.clone()
111        };
112        for column in insert_columns {
113            catalog.ensure_column_privilege_for(
114                &stmt.table,
115                &column,
116                &privilege_subject,
117                TableAclPrivilege::Insert,
118            )?;
119        }
120    }
121    if let Some(columns) = conflict_update_columns {
122        for column in columns {
123            catalog.ensure_column_privilege_for(
124                &stmt.table,
125                column,
126                &privilege_subject,
127                TableAclPrivilege::Update,
128            )?;
129        }
130    }
131    let mut privilege_expressions = stmt
132        .returning
133        .iter()
134        .map(|projection| &projection.expr)
135        .collect::<Vec<_>>();
136    if let Some(conflict) = &stmt.on_conflict {
137        privilege_expressions.extend(conflict.expressions.iter());
138        privilege_expressions.extend(conflict.predicate.iter().map(Box::as_ref));
139    }
140    if let Some(ConflictPlan {
141        action:
142            ConflictActionPlan::Update {
143                assignments,
144                predicate,
145            },
146        ..
147    }) = stmt.on_conflict.as_ref()
148    {
149        privilege_expressions.extend(assignments.iter().map(|assignment| &assignment.value));
150        privilege_expressions.extend(predicate.iter().map(Box::as_ref));
151    }
152    catalog.ensure_target_select(TargetSelectPrivilegeRequest {
153        table: &stmt.table,
154        privilege_subject: stmt.target_privilege_subject.as_deref(),
155        target_qualifier: &stmt.target_qualifier,
156        returning_aliases: &stmt.returning_aliases,
157        expressions: &privilege_expressions,
158        subqueries: &stmt.subqueries,
159        required_columns: stmt
160            .on_conflict
161            .as_ref()
162            .map_or(&[][..], |conflict| conflict.conflict_columns.as_slice()),
163    })?;
164    Ok(())
165}
166
167/// Fill missing DML privilege subjects from the surrounding WITH definition.
168pub fn inherit_command_privilege_subject(
169    command: &mut crate::plan::CommandPlan,
170    subject: String,
171) -> Result<(), SQLError> {
172    use crate::plan::CommandPlan;
173    let (statement_subject, target_subject) = match command {
174        CommandPlan::Insert(plan) => (
175            &mut plan.statement_privilege_subject,
176            &mut plan.target_privilege_subject,
177        ),
178        CommandPlan::Update(plan) => (
179            &mut plan.statement_privilege_subject,
180            &mut plan.target_privilege_subject,
181        ),
182        CommandPlan::Delete(plan) => (
183            &mut plan.statement_privilege_subject,
184            &mut plan.target_privilege_subject,
185        ),
186        CommandPlan::Merge(plan) => (
187            &mut plan.statement_privilege_subject,
188            &mut plan.target_privilege_subject,
189        ),
190        _ => {
191            return Err(SQLError::Internal(
192                "non-DML command in a WITH definition".into(),
193            ))
194        }
195    };
196    statement_subject.get_or_insert(subject.clone());
197    target_subject.get_or_insert(subject);
198    Ok(())
199}