Skip to main content

uqa_sql/catalog/security/
view_ownership.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! View ownership, schema-owner drop authority, and materialized-view maintenance rules.
8use crate::{
9    catalog::{
10        roles::{guards::RoleCatalogGuards, role_inherits, RoleReferenceNames},
11        security::table::{role_has_table_privilege, TableAclPrivilege},
12        stored_view::StoredView,
13        view::StoredViewKind,
14    },
15    SQLError,
16};
17use uqa_core::RelationIdentity;
18
19pub use super::ownership::RelationOwnerSchemas as ViewOwnerSchemas;
20#[derive(Clone, Copy)]
21pub struct ViewOwnershipContext<'a> {
22    pub session: &'a dyn RoleReferenceNames,
23    pub roles: &'a dyn RoleCatalogGuards,
24    pub schemas: &'a dyn ViewOwnerSchemas,
25}
26fn current_user_has_role_privileges(context: ViewOwnershipContext<'_>, target: &str) -> bool {
27    let current = context.session.current_user_name();
28    let roles = context.roles.role_definitions();
29    let memberships = context.roles.role_memberships();
30    role_inherits(&roles, &memberships, &current, target)
31}
32
33fn view_kind_name(view: &StoredView) -> &'static str {
34    match view.kind {
35        StoredViewKind::View => "view",
36        StoredViewKind::Materialized => "materialized view",
37    }
38}
39
40pub fn ensure_view_owner(
41    context: ViewOwnershipContext<'_>,
42    canonical_name: &str,
43    view: &StoredView,
44) -> Result<String, SQLError> {
45    if current_user_has_role_privileges(context, &view.role_owner) {
46        return Ok(view.role_owner.clone());
47    }
48    let relation = RelationIdentity::from_legacy_name(canonical_name)
49        .map_err(|error| SQLError::Internal(format!("resolve view `{canonical_name}`: {error}")))?;
50    Err(SQLError::Routine {
51        sqlstate: "42501".into(),
52        message: format!(
53            "must be owner of {} {}",
54            view_kind_name(view),
55            relation.name
56        ),
57    })
58}
59
60pub fn ensure_view_drop_authority(
61    context: ViewOwnershipContext<'_>,
62    canonical_name: &str,
63    view: &StoredView,
64) -> Result<(), SQLError> {
65    let relation = RelationIdentity::from_legacy_name(canonical_name)
66        .map_err(|error| SQLError::Internal(format!("resolve view `{canonical_name}`: {error}")))?;
67    if current_user_has_role_privileges(context, &view.role_owner)
68        || context
69            .schemas
70            .schema_security(&relation.schema)
71            .is_some_and(|security| current_user_has_role_privileges(context, &security.role_owner))
72    {
73        return Ok(());
74    }
75    Err(SQLError::Routine {
76        sqlstate: "42501".into(),
77        message: format!(
78            "must be owner of {} {}",
79            view_kind_name(view),
80            relation.name
81        ),
82    })
83}
84
85pub fn ensure_materialized_view_maintenance(
86    context: ViewOwnershipContext<'_>,
87    canonical_name: &str,
88    view: &StoredView,
89) -> Result<(), SQLError> {
90    debug_assert_eq!(view.kind, StoredViewKind::Materialized);
91    let current_user = context.session.current_user_name();
92    let roles = context.roles.role_definitions();
93    let memberships = context.roles.role_memberships();
94    if role_has_table_privilege(
95        &view.security(),
96        &current_user,
97        TableAclPrivilege::Maintain,
98        &roles,
99        &memberships,
100    ) {
101        return Ok(());
102    }
103    let relation = RelationIdentity::from_legacy_name(canonical_name).map_err(|error| {
104        SQLError::Internal(format!(
105            "resolve materialized view `{canonical_name}`: {error}"
106        ))
107    })?;
108    Err(SQLError::Routine {
109        sqlstate: "42501".into(),
110        message: format!("permission denied for materialized view {}", relation.name),
111    })
112}
113#[cfg(test)]
114mod tests;