Skip to main content

uqa_sql/catalog/roles/memberships/
command.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Captured membership authority and pure per-recipient catalog changes.
8
9use super::{
10    grants, RoleAttribute, RoleBinding, RoleDefinition, RoleMembership, RoleMembershipKey,
11    RoleSubject, SQLError,
12};
13use crate::{
14    ast::{AlterRoleStmt, GrantRoleStmt, RoleMembershipAction, RoleMembershipOptions},
15    catalog::roles::{resolve_role_specification, RoleReference, RoleReferenceNames},
16};
17use std::collections::BTreeMap;
18
19mod revoke;
20pub use revoke::MembershipRevocation;
21
22pub fn creator_membership(
23    roles: &BTreeMap<String, RoleDefinition>,
24    current: &(impl RoleSubject + ?Sized),
25    created: &RoleDefinition,
26) -> Result<MembershipInsertion, SQLError> {
27    let creator = current
28        .role_definition(roles)
29        .ok_or_else(|| super::insufficient_privilege("permission denied to create role"))?;
30    let bootstrap = roles
31        .values()
32        .find(|role| role.oid == 10)
33        .ok_or_else(|| SQLError::Internal("role catalog has no bootstrap superuser".into()))?;
34    Ok(MembershipInsertion {
35        role: RoleBinding::from_definition(created)?,
36        member: RoleBinding::from_definition(creator)?,
37        grantor: RoleBinding::from_definition(bootstrap)?,
38        admin_option: true,
39        inherit_option: false,
40        set_option: false,
41    })
42}
43
44#[derive(Clone)]
45pub struct MembershipRecipients {
46    pub members: Vec<RoleBinding>,
47    pub grantor: Option<RoleBinding>,
48}
49
50impl MembershipRecipients {
51    /// Resolve the explicit grantor before all recipients; target lookup and authority follow separately in statement order.
52    pub fn bind(
53        names: &dyn RoleReferenceNames,
54        roles: &BTreeMap<String, RoleDefinition>,
55        statement: &GrantRoleStmt,
56    ) -> Result<Self, SQLError> {
57        let grantor = statement
58            .grantor
59            .as_ref()
60            .map(|role| resolve_role_specification(names, role).bind(roles))
61            .transpose()?;
62        let members = statement
63            .grantee_roles
64            .iter()
65            .map(|role| resolve_role_specification(names, role).bind(roles))
66            .collect::<Result<_, _>>()?;
67        Ok(Self { members, grantor })
68    }
69}
70
71#[derive(Clone)]
72pub struct MembershipTarget {
73    pub role: RoleBinding,
74    pub grantor: RoleBinding,
75    pub members: Vec<RoleBinding>,
76    pub is_grant: bool,
77    pub options: RoleMembershipOptions,
78    pub cascade: bool,
79}
80
81impl MembershipTarget {
82    /// ALTER GROUP checks the target and its administration before resolving recipients.
83    pub fn authorize_group(
84        names: &dyn RoleReferenceNames,
85        roles: &BTreeMap<String, RoleDefinition>,
86        memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
87        current: &(impl RoleSubject + ?Sized),
88        statement: &AlterRoleStmt,
89    ) -> Result<Self, SQLError> {
90        let target = resolve_role_specification(names, &statement.name);
91        let role = target.bind(roles)?;
92        grants::require_group_authority(roles, memberships, current, &role)?;
93        let command = GrantRoleStmt {
94            granted_roles: Vec::new(),
95            grantee_roles: statement.members.clone(),
96            is_grant: statement.membership_action == Some(RoleMembershipAction::Add),
97            options: RoleMembershipOptions::default(),
98            grantor: None,
99            cascade: false,
100        };
101        let recipients = MembershipRecipients::bind(names, roles, &command)?;
102        Self::authorize(roles, memberships, current, &target, &recipients, &command)
103    }
104
105    /// Capture authority before execution waits on the target; subsequent graph checks do not repeat this authorization.
106    pub fn authorize(
107        roles: &BTreeMap<String, RoleDefinition>,
108        memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
109        current: &(impl RoleSubject + ?Sized),
110        target: &RoleReference,
111        recipients: &MembershipRecipients,
112        statement: &GrantRoleStmt,
113    ) -> Result<Self, SQLError> {
114        let role = target.bind(roles)?;
115        let grantor = grants::select_grantor(
116            roles,
117            memberships,
118            current,
119            &role,
120            recipients.grantor.as_ref(),
121            statement.is_grant,
122        )?;
123        Ok(Self {
124            role,
125            grantor,
126            members: recipients.members.clone(),
127            is_grant: statement.is_grant,
128            options: statement.options,
129            cascade: statement.cascade,
130        })
131    }
132
133    pub fn validate_graph(
134        &self,
135        memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
136    ) -> Result<(), SQLError> {
137        if self.is_grant {
138            grants::validate_grant(
139                memberships,
140                &self.role,
141                &self.grantor,
142                &self.members,
143                self.options,
144            )?;
145        }
146        Ok(())
147    }
148
149    pub fn change_for_member(
150        &self,
151        roles: &BTreeMap<String, RoleDefinition>,
152        memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
153        member: &RoleBinding,
154    ) -> Result<MembershipChange, SQLError> {
155        let key = RoleMembershipKey {
156            role: self.role.identity(),
157            member: member.identity(),
158            grantor: self.grantor.identity(),
159        };
160        if !self.is_grant {
161            return Err(SQLError::Internal(
162                "role revocation requires a complete target plan".into(),
163            ));
164        }
165        if let Some(before) = memberships.get(&key) {
166            let mut after = before.clone();
167            if let Some(option) = self.options.admin {
168                after.admin_option = option;
169            }
170            if let Some(option) = self.options.inherit {
171                after.inherit_option = option;
172            }
173            if let Some(option) = self.options.set {
174                after.set_option = option;
175            }
176            return Ok(if after == *before {
177                MembershipChange::Notice {
178                    level: "NOTICE",
179                    message: self.membership_notice(roles, member, "has already been granted")?,
180                }
181            } else {
182                MembershipChange::Update(vec![MembershipUpdate {
183                    before: before.clone(),
184                    after: Some(after),
185                }])
186            });
187        }
188        let inherit = match self.options.inherit {
189            Some(value) => value,
190            None => member
191                .role_definition(roles)
192                .ok_or_else(|| {
193                    SQLError::Internal(format!("cache lookup failed for role {}", member.oid))
194                })?
195                .has(RoleAttribute::Inherit),
196        };
197        Ok(MembershipChange::Insert(MembershipInsertion {
198            role: self.role.clone(),
199            member: member.clone(),
200            grantor: self.grantor.clone(),
201            admin_option: self.options.admin.unwrap_or(false),
202            inherit_option: inherit,
203            set_option: self.options.set.unwrap_or(true),
204        }))
205    }
206
207    fn membership_notice(
208        &self,
209        roles: &BTreeMap<String, RoleDefinition>,
210        member: &RoleBinding,
211        action: &str,
212    ) -> Result<String, SQLError> {
213        let grantor = self.grantor.require_name(roles)?;
214        Ok(format!(
215            "role \"{}\" {action} membership in role \"{}\" by role \"{grantor}\"",
216            member.name, self.role.name
217        ))
218    }
219}
220
221pub enum MembershipChange {
222    Insert(MembershipInsertion),
223    Update(Vec<MembershipUpdate>),
224    Notice {
225        level: &'static str,
226        message: String,
227    },
228}
229
230pub struct MembershipUpdate {
231    pub before: RoleMembership,
232    pub after: Option<RoleMembership>,
233}
234
235impl MembershipUpdate {
236    pub fn apply(
237        &self,
238        memberships: &mut BTreeMap<RoleMembershipKey, RoleMembership>,
239    ) -> Result<(), SQLError> {
240        let key = self.before.key();
241        match memberships.get(&key) {
242            Some(current) if current == &self.before => {}
243            Some(_) => return Err(SQLError::Internal("tuple concurrently updated".into())),
244            None => return Err(SQLError::Internal("tuple concurrently deleted".into())),
245        }
246        if let Some(after) = &self.after {
247            memberships.insert(key, after.clone());
248        } else {
249            memberships.remove(&key);
250        }
251        Ok(())
252    }
253}
254
255/// SQL specifies the tuple; execution supplies its reserved public OID before publication.
256pub struct MembershipInsertion {
257    pub role: RoleBinding,
258    pub member: RoleBinding,
259    pub grantor: RoleBinding,
260    pub admin_option: bool,
261    pub inherit_option: bool,
262    pub set_option: bool,
263}
264
265impl MembershipInsertion {
266    pub fn with_oid(self, oid: i64) -> Result<RoleMembership, SQLError> {
267        if oid <= 0 || oid > i64::from(u32::MAX) {
268            return Err(SQLError::Internal("invalid role membership OID".into()));
269        }
270        Ok(RoleMembership {
271            oid,
272            role: self.role,
273            member: self.member,
274            grantor: self.grantor,
275            admin_option: self.admin_option,
276            inherit_option: self.inherit_option,
277            set_option: self.set_option,
278        })
279    }
280}
281
282#[cfg(test)]
283mod tests;