uqa_sql/routines/security/
grants.rs1use 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
21pub 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
30pub 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
76pub 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
100pub 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
133pub 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;