Skip to main content

uqa_sql/routines/security/
grants.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Ordered namespace expansion and declaration checks for routine privileges.
8
9use crate::{
10    ast::{AlterRoutineKind, GrantRoutineStmt, RoutinePrivilege},
11    catalog::roles::{identity::RoleSubject, RoleDefinition},
12    routines::{
13        lifecycle::{names::RoutineNameCatalog, RoutineRegistry},
14        SQLUserFunction,
15    },
16    SQLError,
17};
18use std::collections::BTreeMap;
19use uqa_core::{catalog_acl::AclGrantee, RelationIdentity};
20
21/// `objectsInSchemaToOids` includes aggregate and window functions in FUNCTIONS, but excludes procedures.
22pub fn kind_matches(kind: AlterRoutineKind, prokind: char) -> bool {
23    match kind {
24        AlterRoutineKind::Function => matches!(prokind, 'f' | 'a' | 'w'),
25        AlterRoutineKind::Procedure => prokind == 'p',
26        AlterRoutineKind::Routine => matches!(prokind, 'f' | 'a' | 'w' | 'p'),
27    }
28}
29
30/// Resolve schemas in written order, checking USAGE before inspecting their routines. Repeated schemas contribute repeated targets, including their warnings.
31pub fn routines_in_schemas(
32    catalog: &dyn RoutineNameCatalog,
33    registry: &RoutineRegistry,
34    schemas: &[String],
35    kind: AlterRoutineKind,
36    catalog_oid: impl Fn(&SQLUserFunction) -> Result<i64, SQLError>,
37) -> Result<Vec<(String, usize)>, SQLError> {
38    let mut targets = Vec::new();
39    for schema in schemas {
40        let security = catalog
41            .schema_security(schema)
42            .ok_or_else(|| SQLError::Routine {
43                sqlstate: "3F000".into(),
44                message: format!("schema \"{schema}\" does not exist"),
45            })?;
46        catalog.require_schema_usage(schema, &catalog.current_role())?;
47        let namespace_oid = security.namespace_oid(schema);
48        let mut matches = Vec::new();
49        for (name, overloads) in registry {
50            let relation = RelationIdentity::from_legacy_name(name).map_err(|error| {
51                SQLError::Internal(format!("invalid routine catalog name `{name}`: {error}"))
52            })?;
53            if catalog
54                .schema_security(&relation.schema)
55                .is_none_or(|security| security.namespace_oid(&relation.schema) != namespace_oid)
56            {
57                continue;
58            }
59            for (position, routine) in overloads.iter().enumerate() {
60                let prokind = if routine.def.is_procedure { 'p' } else { 'f' };
61                if kind_matches(kind, prokind) {
62                    matches.push((catalog_oid(routine)?, name.clone(), position));
63                }
64            }
65        }
66        matches.sort_by_key(|(oid, _, _)| *oid);
67        targets.extend(
68            matches
69                .into_iter()
70                .map(|(_, name, position)| (name, position)),
71        );
72    }
73    Ok(targets)
74}
75
76/// `GRANTED BY` is checked before the command resolves its target objects or recipients.
77pub fn validate_grantor(
78    requested: Option<&str>,
79    current: &(impl RoleSubject + ?Sized),
80    roles: &BTreeMap<String, RoleDefinition>,
81) -> Result<(), SQLError> {
82    let Some(requested) = requested else {
83        return Ok(());
84    };
85    if !roles.contains_key(requested) {
86        return Err(SQLError::Routine {
87            sqlstate: "42704".into(),
88            message: format!("role \"{requested}\" does not exist"),
89        });
90    }
91    if current.role_name(roles) != Some(requested) {
92        return Err(SQLError::Routine {
93            sqlstate: "0A000".into(),
94            message: "grantor must be current user".into(),
95        });
96    }
97    Ok(())
98}
99
100/// Privilege validation follows target and recipient binding, even for an empty schema.
101pub fn validate_privileges(stmt: &GrantRoutineStmt) -> Result<(), SQLError> {
102    for privilege in &stmt.privileges {
103        let message = match privilege {
104            RoutinePrivilege::Execute => continue,
105            RoutinePrivilege::Unsupported(name) => {
106                let privilege = match name.as_str() {
107                    "temporary" | "temp" => "TEMP".into(),
108                    "insert" | "select" | "update" | "delete" | "truncate" | "references"
109                    | "trigger" | "usage" | "create" | "connect" | "set" | "alter system"
110                    | "maintain" => name.to_ascii_uppercase(),
111                    _ => {
112                        return Err(SQLError::Routine {
113                            sqlstate: "42601".into(),
114                            message: format!("unrecognized privilege type \"{name}\""),
115                        })
116                    }
117                };
118                let kind = crate::routines::lifecycle::alter_routine_kind_name(stmt.kind);
119                format!("invalid privilege type {privilege} for {kind}")
120            }
121            RoutinePrivilege::ColumnsUnsupported => {
122                "column privileges are only valid for relations".into()
123            }
124        };
125        return Err(SQLError::Routine {
126            sqlstate: "0LP01".into(),
127            message,
128        });
129    }
130    Ok(())
131}
132
133/// An empty target list never invokes `merge_acl_with_grant`, so a PUBLIC grant option is only rejected when at least one object is selected.
134pub fn validate_target_options(
135    stmt: &GrantRoutineStmt,
136    grantees: &[AclGrantee],
137) -> Result<(), SQLError> {
138    if stmt.is_grant && stmt.grant_option && grantees.iter().any(AclGrantee::is_public) {
139        return Err(SQLError::Routine {
140            sqlstate: "0LP01".into(),
141            message: "grant options can only be granted to roles".into(),
142        });
143    }
144    Ok(())
145}
146
147pub mod targets;
148
149#[cfg(test)]
150mod tests;