Skip to main content

uqa_sql/catalog/roles/
identity.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Role incarnations and authorization subjects over an explicit role catalog.
8
9use super::RoleDefinition;
10use crate::SQLError;
11use serde::{Deserialize, Serialize};
12use std::{collections::BTreeMap, sync::Arc};
13pub use uqa_core::catalog_role::RoleIdentity;
14
15/// A selected role keeps its incarnation even if another role later reuses its name or OID.
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct RoleBinding {
18    pub name: String,
19    pub oid: u32,
20    pub object_id: [u8; 16],
21}
22
23/// Explicit SQL names are resolved against the current catalog; captured authority retains its incarnation.
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(untagged)]
26pub enum RoleReference {
27    Named(String),
28    Bound(Arc<RoleBinding>),
29}
30
31impl From<String> for RoleReference {
32    fn from(name: String) -> Self {
33        Self::Named(name)
34    }
35}
36
37impl From<&str> for RoleReference {
38    fn from(name: &str) -> Self {
39        Self::Named(name.into())
40    }
41}
42
43impl RoleReference {
44    pub fn from_identity(
45        identity: RoleIdentity,
46        roles: &BTreeMap<String, RoleDefinition>,
47    ) -> Result<Self, SQLError> {
48        let name = identity.role_name(roles).ok_or_else(|| {
49            SQLError::Internal("role identity references a missing or replaced incarnation".into())
50        })?;
51        Ok(Self::Bound(Arc::new(RoleBinding::from_definition(
52            &roles[name],
53        )?)))
54    }
55
56    pub fn require_name<'a>(
57        &'a self,
58        roles: &'a BTreeMap<String, RoleDefinition>,
59    ) -> Result<&'a str, SQLError> {
60        match self {
61            Self::Named(name) => {
62                super::require_role_exists(roles, name)?;
63                Ok(name)
64            }
65            Self::Bound(role) => role.require_name(roles),
66        }
67    }
68
69    pub fn catalog_name(
70        &self,
71        roles: &BTreeMap<String, RoleDefinition>,
72    ) -> Result<String, SQLError> {
73        match self {
74            Self::Named(name) => Ok(name.clone()),
75            Self::Bound(role) => role.require_name(roles).map(str::to_owned),
76        }
77    }
78
79    pub fn bind(&self, roles: &BTreeMap<String, RoleDefinition>) -> Result<RoleBinding, SQLError> {
80        let name = self.require_name(roles)?;
81        RoleBinding::from_definition(&roles[name])
82    }
83}
84
85impl RoleSubject for RoleReference {
86    fn role_name<'a>(&'a self, roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str> {
87        match self {
88            Self::Named(name) => name.role_name(roles),
89            Self::Bound(role) => role.role_name(roles),
90        }
91    }
92
93    fn role_definition<'a>(
94        &self,
95        roles: &'a BTreeMap<String, RoleDefinition>,
96    ) -> Option<&'a RoleDefinition> {
97        match self {
98            Self::Named(name) => name.role_definition(roles),
99            Self::Bound(role) => role.role_definition(roles),
100        }
101    }
102}
103
104impl RoleBinding {
105    pub fn identity(&self) -> RoleIdentity {
106        RoleIdentity {
107            oid: i64::from(self.oid),
108            object_id: self.object_id,
109        }
110    }
111
112    pub fn from_definition(role: &RoleDefinition) -> Result<Self, SQLError> {
113        if role.oid <= 0 {
114            return Err(SQLError::Internal("invalid role OID".into()));
115        }
116        if role.object_id == [0; 16] {
117            return Err(SQLError::Internal("role has no object identity".into()));
118        }
119        Ok(Self {
120            name: role.name.clone(),
121            oid: u32::try_from(role.oid)
122                .map_err(|_| SQLError::Internal("invalid role OID".into()))?,
123            object_id: role.object_id,
124        })
125    }
126
127    /// Retain the original incarnation through name changes before publishing a stored role reference.
128    pub fn revalidate(&self, roles: &BTreeMap<String, RoleDefinition>) -> Result<(), SQLError> {
129        if self.role_definition(roles).is_some() {
130            Ok(())
131        } else {
132            Err(SQLError::Routine {
133                sqlstate: "42704".into(),
134                message: format!("role {} was concurrently dropped", self.oid),
135            })
136        }
137    }
138
139    fn matches_definition(&self, role: &RoleDefinition) -> bool {
140        role.oid == i64::from(self.oid) && role.object_id == self.object_id
141    }
142
143    pub fn require_name<'a>(
144        &self,
145        roles: &'a BTreeMap<String, RoleDefinition>,
146    ) -> Result<&'a str, SQLError> {
147        self.role_definition(roles)
148            .map(|role| role.name.as_str())
149            .ok_or_else(|| SQLError::Routine {
150                sqlstate: "42704".into(),
151                message: format!("invalid role OID: {}", self.oid),
152            })
153    }
154}
155
156/// A named SQL argument and an already selected session role have different lookup semantics. An absent bound role contributes no inherited privileges; PUBLIC ACLs remain applicable.
157pub trait RoleSubject {
158    fn role_name<'a>(&'a self, roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str>;
159
160    fn role_definition<'a>(
161        &self,
162        roles: &'a BTreeMap<String, RoleDefinition>,
163    ) -> Option<&'a RoleDefinition>;
164}
165
166impl<T: RoleSubject + ?Sized> RoleSubject for Arc<T> {
167    fn role_name<'a>(&'a self, roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str> {
168        self.as_ref().role_name(roles)
169    }
170    fn role_definition<'a>(
171        &self,
172        roles: &'a BTreeMap<String, RoleDefinition>,
173    ) -> Option<&'a RoleDefinition> {
174        self.as_ref().role_definition(roles)
175    }
176}
177
178impl<T: RoleSubject + ?Sized> RoleSubject for &T {
179    fn role_name<'a>(&'a self, roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str> {
180        (*self).role_name(roles)
181    }
182
183    fn role_definition<'a>(
184        &self,
185        roles: &'a BTreeMap<String, RoleDefinition>,
186    ) -> Option<&'a RoleDefinition> {
187        (*self).role_definition(roles)
188    }
189}
190
191impl RoleSubject for str {
192    fn role_name<'a>(&'a self, _roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str> {
193        Some(self)
194    }
195
196    fn role_definition<'a>(
197        &self,
198        roles: &'a BTreeMap<String, RoleDefinition>,
199    ) -> Option<&'a RoleDefinition> {
200        roles.get(self)
201    }
202}
203
204impl RoleSubject for String {
205    fn role_name<'a>(&'a self, roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str> {
206        self.as_str().role_name(roles)
207    }
208
209    fn role_definition<'a>(
210        &self,
211        roles: &'a BTreeMap<String, RoleDefinition>,
212    ) -> Option<&'a RoleDefinition> {
213        self.as_str().role_definition(roles)
214    }
215}
216
217impl RoleSubject for uqa_core::catalog_acl::AclGrantee {
218    fn role_name<'a>(&'a self, _roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str> {
219        self.role_name()
220    }
221
222    fn role_definition<'a>(
223        &self,
224        roles: &'a BTreeMap<String, RoleDefinition>,
225    ) -> Option<&'a RoleDefinition> {
226        self.role_name().and_then(|name| roles.get(name))
227    }
228}
229
230impl RoleSubject for RoleBinding {
231    fn role_name<'a>(&'a self, roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str> {
232        self.role_definition(roles).map(|role| role.name.as_str())
233    }
234
235    fn role_definition<'a>(
236        &self,
237        roles: &'a BTreeMap<String, RoleDefinition>,
238    ) -> Option<&'a RoleDefinition> {
239        roles
240            .get(&self.name)
241            .filter(|role| self.matches_definition(role))
242            .or_else(|| roles.values().find(|role| self.matches_definition(role)))
243    }
244}
245
246impl RoleSubject for RoleIdentity {
247    fn role_name<'a>(&'a self, roles: &'a BTreeMap<String, RoleDefinition>) -> Option<&'a str> {
248        self.role_definition(roles).map(|role| role.name.as_str())
249    }
250
251    fn role_definition<'a>(
252        &self,
253        roles: &'a BTreeMap<String, RoleDefinition>,
254    ) -> Option<&'a RoleDefinition> {
255        if !self.is_valid() {
256            return None;
257        }
258        roles.values().find(|role| role.identity() == *self)
259    }
260}
261
262#[cfg(test)]
263mod tests;