Skip to main content

uqa_sql/routines/security/
binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bind legacy routine names once and validate retained owner and ACL incarnations.
8
9use super::{bound_routine_owner, routine_grant_option_roles_for};
10use crate::{
11    ast::{CreateFunction, RoutineAclEntry},
12    catalog::roles::{identity::RoleSubject, RoleDefinition, RoleIdentity, RoleReference},
13    SQLError,
14};
15use serde::Deserialize;
16use std::collections::{BTreeMap, BTreeSet};
17use uqa_core::catalog_acl::AclGrantee;
18
19/// Only the initial catalog converter may interpret named routine authority.
20#[derive(Deserialize)]
21pub struct LegacyRoutineAclEntry {
22    pub role: AclGrantee,
23    #[serde(default)]
24    pub grantor: Option<String>,
25    pub grant_option: bool,
26}
27
28pub struct BoundRoutineAuthority {
29    pub owner: RoleIdentity,
30    pub execute_acl: Option<Vec<RoutineAclEntry>>,
31}
32
33pub fn bind_legacy_authority(
34    owner: &str,
35    acl: Option<Vec<LegacyRoutineAclEntry>>,
36    roles: &BTreeMap<String, RoleDefinition>,
37    implicit_owner_execute: bool,
38) -> Result<BoundRoutineAuthority, SQLError> {
39    let bind = |name: &str| {
40        RoleReference::from(name)
41            .bind(roles)
42            .map(|role| role.identity())
43    };
44    let identity = bind(owner)?;
45    let mut execute_acl = acl
46        .map(|acl| {
47            acl.into_iter()
48                .map(|entry| {
49                    Ok(RoutineAclEntry {
50                        role: entry.role.role_name().map(bind).transpose()?,
51                        grantor: bind(entry.grantor.as_deref().unwrap_or(owner))?,
52                        grant_option: entry.grant_option,
53                    })
54                })
55                .collect::<Result<Vec<_>, SQLError>>()
56        })
57        .transpose()?;
58    if implicit_owner_execute {
59        if let Some(acl) = execute_acl.as_mut() {
60            if !acl.iter().any(|entry| entry.role == Some(identity)) {
61                acl.push(RoutineAclEntry {
62                    role: Some(identity),
63                    grantor: identity,
64                    grant_option: false,
65                });
66            }
67        }
68    }
69    validate_acl(identity, execute_acl.as_deref())?;
70    Ok(BoundRoutineAuthority {
71        owner: identity,
72        execute_acl,
73    })
74}
75
76fn invalid(message: &str) -> SQLError {
77    SQLError::Internal(format!("invalid routine authority: {message}"))
78}
79
80fn validate_acl(owner: RoleIdentity, acl: Option<&[RoutineAclEntry]>) -> Result<(), SQLError> {
81    if !owner.is_valid() {
82        return Err(invalid("missing owner incarnation"));
83    }
84    let reachable = routine_grant_option_roles_for(owner, acl);
85    let mut paths = BTreeSet::new();
86    for entry in acl.into_iter().flatten() {
87        if !entry.grantor.is_valid() || entry.role.is_some_and(|role| !role.is_valid()) {
88            return Err(invalid("missing ACL endpoint incarnation"));
89        }
90        if entry.role.is_none() && entry.grant_option {
91            return Err(invalid("PUBLIC cannot retain a grant option"));
92        }
93        if !paths.insert((entry.role, entry.grantor)) {
94            return Err(invalid("duplicate ACL grant path"));
95        }
96        if !reachable.contains(&entry.grantor) {
97            return Err(invalid("ACL grantor has no owner-rooted grant option"));
98        }
99    }
100    Ok(())
101}
102
103pub fn validate_routine_authority_identities(definition: &CreateFunction) -> Result<(), SQLError> {
104    validate_acl(
105        bound_routine_owner(definition)?,
106        definition.execute_acl.as_deref(),
107    )
108}
109
110fn authority_roles(definition: &CreateFunction) -> Result<BTreeSet<RoleIdentity>, SQLError> {
111    let owner = bound_routine_owner(definition)?;
112    let mut identities = BTreeSet::from([owner]);
113    for entry in definition.execute_acl.iter().flatten() {
114        identities.extend(entry.role);
115        identities.insert(entry.grantor);
116    }
117    Ok(identities)
118}
119
120pub fn routine_role_dependencies(
121    definition: &CreateFunction,
122    roles: &BTreeMap<String, RoleDefinition>,
123) -> Result<BTreeSet<String>, SQLError> {
124    validate_routine_authority_identities(definition)?;
125    authority_roles(definition)?
126        .into_iter()
127        .map(|identity| {
128            identity
129                .role_name(roles)
130                .map(str::to_owned)
131                .ok_or_else(|| invalid("missing role incarnation"))
132        })
133        .collect()
134}
135
136pub fn validate_routine_authority(
137    definition: &CreateFunction,
138    roles: &BTreeMap<String, RoleDefinition>,
139) -> Result<(), SQLError> {
140    routine_role_dependencies(definition, roles).map(|_| ())
141}
142
143pub fn bind_routine_grantees(
144    grantees: &[AclGrantee],
145    roles: &BTreeMap<String, RoleDefinition>,
146) -> Result<Vec<Option<RoleIdentity>>, SQLError> {
147    grantees
148        .iter()
149        .map(|grantee| {
150            grantee
151                .role_name()
152                .map(|name| {
153                    RoleReference::from(name)
154                        .bind(roles)
155                        .map(|role| role.identity())
156                })
157                .transpose()
158        })
159        .collect()
160}
161
162pub fn added_routine_acl_roles(
163    before: &CreateFunction,
164    after: &CreateFunction,
165    roles: &BTreeMap<String, RoleDefinition>,
166    added: &mut BTreeSet<String>,
167) -> Result<(), SQLError> {
168    let mut old = authority_roles(before)?;
169    old.remove(&bound_routine_owner(before)?);
170    let mut new = authority_roles(after)?;
171    new.remove(&bound_routine_owner(after)?);
172    validate_routine_authority(after, roles)?;
173    for identity in new.difference(&old) {
174        added.insert(
175            identity
176                .role_name(roles)
177                .ok_or_else(|| invalid("missing ACL incarnation"))?
178                .to_owned(),
179        );
180    }
181    Ok(())
182}
183
184#[cfg(test)]
185mod tests;