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(crate::SQLNotice::notice(self.membership_notice(
178                    roles,
179                    member,
180                    "has already been granted",
181                )?))
182            } else {
183                MembershipChange::Update(vec![MembershipUpdate {
184                    before: before.clone(),
185                    after: Some(after),
186                }])
187            });
188        }
189        let inherit = match self.options.inherit {
190            Some(value) => value,
191            None => member
192                .role_definition(roles)
193                .ok_or_else(|| {
194                    SQLError::Internal(format!("cache lookup failed for role {}", member.oid))
195                })?
196                .has(RoleAttribute::Inherit),
197        };
198        Ok(MembershipChange::Insert(MembershipInsertion {
199            role: self.role.clone(),
200            member: member.clone(),
201            grantor: self.grantor.clone(),
202            admin_option: self.options.admin.unwrap_or(false),
203            inherit_option: inherit,
204            set_option: self.options.set.unwrap_or(true),
205        }))
206    }
207
208    fn membership_notice(
209        &self,
210        roles: &BTreeMap<String, RoleDefinition>,
211        member: &RoleBinding,
212        action: &str,
213    ) -> Result<String, SQLError> {
214        let grantor = self.grantor.require_name(roles)?;
215        Ok(format!(
216            "role \"{}\" {action} membership in role \"{}\" by role \"{grantor}\"",
217            member.name, self.role.name
218        ))
219    }
220}
221
222pub enum MembershipChange {
223    Insert(MembershipInsertion),
224    Update(Vec<MembershipUpdate>),
225    /// The membership exists with the requested options, which `PostgreSQL` reports with a notice.
226    Notice(crate::SQLNotice),
227}
228
229pub struct MembershipUpdate {
230    pub before: RoleMembership,
231    pub after: Option<RoleMembership>,
232}
233
234impl MembershipUpdate {
235    pub fn apply(
236        &self,
237        memberships: &mut BTreeMap<RoleMembershipKey, RoleMembership>,
238    ) -> Result<(), SQLError> {
239        let key = self.before.key();
240        match memberships.get(&key) {
241            Some(current) if current == &self.before => {}
242            Some(_) => return Err(SQLError::Internal("tuple concurrently updated".into())),
243            None => return Err(SQLError::Internal("tuple concurrently deleted".into())),
244        }
245        if let Some(after) = &self.after {
246            memberships.insert(key, after.clone());
247        } else {
248            memberships.remove(&key);
249        }
250        Ok(())
251    }
252}
253
254/// SQL specifies the tuple; execution supplies its reserved public OID before publication.
255pub struct MembershipInsertion {
256    pub role: RoleBinding,
257    pub member: RoleBinding,
258    pub grantor: RoleBinding,
259    pub admin_option: bool,
260    pub inherit_option: bool,
261    pub set_option: bool,
262}
263
264impl MembershipInsertion {
265    pub fn with_oid(self, oid: i64) -> Result<RoleMembership, SQLError> {
266        if oid <= 0 || oid > i64::from(u32::MAX) {
267            return Err(SQLError::Internal("invalid role membership OID".into()));
268        }
269        Ok(RoleMembership {
270            oid,
271            role: self.role,
272            member: self.member,
273            grantor: self.grantor,
274            admin_option: self.admin_option,
275            inherit_option: self.inherit_option,
276            set_option: self.set_option,
277        })
278    }
279}
280
281#[cfg(test)]
282mod tests;