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