Skip to main content

uqa_sql/catalog/security/
acl_command.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! An ACL command binds its recipients once and projects those identities after catalog waits.
8
9use crate::{
10    ast::{AclRoleSpecification, RoleSpecification},
11    catalog::roles::{
12        identity::RoleBinding, resolve_acl_role_specification, resolve_role_specification,
13        RoleDefinition, RoleReference, RoleReferenceNames,
14    },
15    SQLError,
16};
17use std::collections::BTreeMap;
18use uqa_core::catalog_acl::AclGrantee;
19
20#[derive(Clone, Default)]
21pub struct AclCommandRoles {
22    bound: Option<BoundAclRoles>,
23}
24
25#[derive(Clone)]
26struct BoundAclRoles {
27    grantees: Vec<Option<RoleBinding>>,
28    grantor: Option<RoleBinding>,
29    current_user: RoleReference,
30}
31
32pub struct ResolvedAclRoles {
33    pub grantees: Vec<AclGrantee>,
34    pub requested_grantor: Option<String>,
35    pub current_user: RoleReference,
36}
37
38impl AclCommandRoles {
39    /// Preserve each object's validation order before retaining the complete command's role arguments.
40    pub fn resolve_validated(
41        &mut self,
42        names: &dyn RoleReferenceNames,
43        roles: &BTreeMap<String, RoleDefinition>,
44        grantees: &[AclRoleSpecification],
45        grantor: Option<&RoleSpecification>,
46        validate: impl FnOnce(&ResolvedAclRoles) -> Result<(), SQLError>,
47    ) -> Result<ResolvedAclRoles, SQLError> {
48        let resolved = if let Some(bound) = &self.bound {
49            bound.resolve(roles)?
50        } else {
51            ResolvedAclRoles {
52                grantees: grantees
53                    .iter()
54                    .map(|role| resolve_acl_role_specification(names, role, roles))
55                    .collect::<Result<_, _>>()?,
56                requested_grantor: grantor
57                    .map(|role| resolve_role_specification(names, role).catalog_name(roles))
58                    .transpose()?,
59                current_user: names.current_role(),
60            }
61        };
62        validate(&resolved)?;
63        if self.bound.is_none() {
64            self.bound = Some(BoundAclRoles {
65                grantees: resolved
66                    .grantees
67                    .iter()
68                    .map(|grantee| {
69                        grantee
70                            .role_name()
71                            .map(|name| RoleReference::from(name).bind(roles))
72                            .transpose()
73                    })
74                    .collect::<Result<_, _>>()?,
75                grantor: resolved
76                    .requested_grantor
77                    .as_deref()
78                    .map(|name| RoleReference::from(name).bind(roles))
79                    .transpose()?,
80                current_user: match &resolved.current_user {
81                    RoleReference::Bound(_) => resolved.current_user.clone(),
82                    RoleReference::Named(_) => RoleReference::Bound(std::sync::Arc::new(
83                        resolved.current_user.bind(roles)?,
84                    )),
85                },
86            });
87        }
88        Ok(resolved)
89    }
90}
91
92impl BoundAclRoles {
93    fn resolve(
94        &self,
95        roles: &BTreeMap<String, RoleDefinition>,
96    ) -> Result<ResolvedAclRoles, SQLError> {
97        let name = |binding: &RoleBinding| {
98            binding.revalidate(roles)?;
99            binding.require_name(roles).map(str::to_owned)
100        };
101        Ok(ResolvedAclRoles {
102            grantees: self
103                .grantees
104                .iter()
105                .map(|binding| {
106                    binding.as_ref().map_or_else(
107                        || Ok(AclGrantee::Public),
108                        |binding| name(binding).map(AclGrantee::Role),
109                    )
110                })
111                .collect::<Result<_, _>>()?,
112            requested_grantor: self.grantor.as_ref().map(name).transpose()?,
113            current_user: self.current_user.clone(),
114        })
115    }
116}
117
118#[cfg(test)]
119mod tests;