Skip to main content

uqa_sql/catalog/roles/
definition.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Role declaration candidates, authorization and requested-name binding.
8
9use super::{
10    guards::RoleCatalogGuards,
11    memberships::{
12        apply_grant_role_statement, insert_membership, insufficient_privilege,
13        require_role_attribute_authority, role_has_admin, role_is_superuser,
14    },
15    resolve_role_reference, RoleDefinition, RoleMembership, RoleMembershipKey, RoleReferenceNames,
16};
17use crate::{
18    ast::{
19        AlterRoleStmt, CreateRoleStmt, DropRoleStmt, GrantRoleStmt, RoleAttribute,
20        RoleMembershipOptions,
21    },
22    SQLError,
23};
24use std::collections::{BTreeMap, BTreeSet};
25
26pub trait RoleNotices {
27    fn notice(&self, level: &str, message: &str);
28}
29
30#[derive(Clone, Copy)]
31pub struct RoleValidationContext<'a> {
32    pub names: &'a dyn RoleReferenceNames,
33    pub roles: &'a dyn RoleCatalogGuards,
34    pub notices: &'a dyn RoleNotices,
35}
36
37pub fn require_role_creation(context: &RoleValidationContext<'_>) -> Result<(), SQLError> {
38    let current = context.names.current_user_name();
39    let allowed = context
40        .roles
41        .role_definitions()
42        .get(&current)
43        .is_some_and(|role| {
44            role.has(RoleAttribute::Superuser) || role.has(RoleAttribute::CreateRole)
45        });
46    if allowed {
47        Ok(())
48    } else {
49        Err(insufficient_privilege("permission denied to create role"))
50    }
51}
52
53pub fn require_role_administration_for(
54    catalog: &dyn RoleCatalogGuards,
55    roles: &BTreeMap<String, RoleDefinition>,
56    current: &str,
57    target: &str,
58    action: &str,
59) -> Result<(), SQLError> {
60    if role_is_superuser(roles, current) {
61        return Ok(());
62    }
63    let can_create_roles = roles
64        .get(current)
65        .is_some_and(|role| role.has(RoleAttribute::CreateRole));
66    let memberships = catalog.role_memberships();
67    if can_create_roles && role_has_admin(&memberships, current, target) {
68        Ok(())
69    } else {
70        Err(insufficient_privilege(&format!(
71            "permission denied to {action}"
72        )))
73    }
74}
75
76pub fn create_role_candidate(
77    roles: &BTreeMap<String, RoleDefinition>,
78    current: &str,
79    statement: &CreateRoleStmt,
80) -> Result<(BTreeMap<String, RoleDefinition>, bool), SQLError> {
81    if roles.contains_key(&statement.name) {
82        return Err(SQLError::Routine {
83            sqlstate: "42710".into(),
84            message: format!("role \"{}\" already exists", statement.name),
85        });
86    }
87    let current_is_superuser = role_is_superuser(roles, current);
88    let mut next_roles = roles.clone();
89    next_roles.insert(
90        statement.name.clone(),
91        RoleDefinition::from_create(statement),
92    );
93    Ok((next_roles, current_is_superuser))
94}
95
96pub fn apply_create_role_memberships(
97    context: &RoleValidationContext<'_>,
98    statement: &CreateRoleStmt,
99    current: &str,
100    current_is_superuser: bool,
101    roles: &BTreeMap<String, RoleDefinition>,
102    memberships: &mut BTreeMap<RoleMembershipKey, RoleMembership>,
103) -> Result<(), SQLError> {
104    if !current_is_superuser {
105        let bootstrap = roles
106            .values()
107            .find(|role| role.has(RoleAttribute::Superuser))
108            .map(|role| role.name.clone())
109            .ok_or_else(|| SQLError::Internal("role catalog has no bootstrap superuser".into()))?;
110        insert_membership(
111            memberships,
112            &statement.name,
113            current,
114            &bootstrap,
115            RoleMembershipOptions {
116                admin: Some(true),
117                inherit: Some(false),
118                set: Some(false),
119            },
120            roles,
121        );
122    }
123    let in_roles = statement
124        .in_roles
125        .iter()
126        .map(|role| resolve_role_reference(context.names, role))
127        .collect::<Vec<_>>();
128    if !in_roles.is_empty() {
129        apply_grant_role_statement(
130            roles,
131            memberships,
132            current,
133            &GrantRoleStmt {
134                granted_roles: in_roles,
135                grantee_roles: vec![statement.name.clone()],
136                is_grant: true,
137                options: RoleMembershipOptions::default(),
138                grantor: None,
139                cascade: false,
140            },
141        )?;
142    }
143    let role_members = statement
144        .role_members
145        .iter()
146        .map(|role| resolve_role_reference(context.names, role))
147        .collect::<Vec<_>>();
148    if !role_members.is_empty() {
149        apply_grant_role_statement(
150            roles,
151            memberships,
152            current,
153            &GrantRoleStmt {
154                granted_roles: vec![statement.name.clone()],
155                grantee_roles: role_members,
156                is_grant: true,
157                options: RoleMembershipOptions::default(),
158                grantor: None,
159                cascade: false,
160            },
161        )?;
162    }
163    let admin_members = statement
164        .admin_members
165        .iter()
166        .map(|role| resolve_role_reference(context.names, role))
167        .collect::<Vec<_>>();
168    if !admin_members.is_empty() {
169        apply_grant_role_statement(
170            roles,
171            memberships,
172            current,
173            &GrantRoleStmt {
174                granted_roles: vec![statement.name.clone()],
175                grantee_roles: admin_members,
176                is_grant: true,
177                options: RoleMembershipOptions {
178                    admin: Some(true),
179                    ..RoleMembershipOptions::default()
180                },
181                grantor: None,
182                cascade: false,
183            },
184        )?;
185    }
186    Ok(())
187}
188
189pub fn alter_role_candidate(
190    context: &RoleValidationContext<'_>,
191    roles: &BTreeMap<String, RoleDefinition>,
192    current: &str,
193    name: String,
194    statement: &AlterRoleStmt,
195) -> Result<BTreeMap<String, RoleDefinition>, SQLError> {
196    let existing = roles.get(&name).cloned().ok_or_else(|| SQLError::Routine {
197        sqlstate: "42704".into(),
198        message: format!("role \"{name}\" does not exist"),
199    })?;
200    require_role_administration_for(context.roles, roles, current, &name, "alter role")?;
201    require_role_attribute_authority(
202        roles,
203        current,
204        statement.attributes.keys().copied(),
205        "alter role",
206    )?;
207    let current_is_superuser = roles
208        .get(current)
209        .is_some_and(|role| role.has(RoleAttribute::Superuser));
210    if (statement.attributes.contains_key(&RoleAttribute::Superuser)
211        || existing.has(RoleAttribute::Superuser))
212        && !current_is_superuser
213    {
214        return Err(insufficient_privilege(
215            "must be superuser to alter superuser roles or change superuser attribute",
216        ));
217    }
218    let mut updated = existing;
219    for (&attribute, &enabled) in &statement.attributes {
220        if enabled {
221            updated.attributes.insert(attribute);
222        } else {
223            updated.attributes.remove(&attribute);
224        }
225    }
226    if let Some(value) = statement.connection_limit {
227        updated.connection_limit = value;
228    }
229    let mut next = roles.clone();
230    next.insert(name, updated);
231    Ok(next)
232}
233
234pub fn resolve_drop_role_names(
235    context: &RoleValidationContext<'_>,
236    statement: &DropRoleStmt,
237    current: &str,
238    session: &str,
239    snapshot: &BTreeMap<String, RoleDefinition>,
240) -> Result<Vec<String>, SQLError> {
241    let mut names = Vec::new();
242    for requested in &statement.names {
243        let name = resolve_role_reference(context.names, requested);
244        if !snapshot.contains_key(&name) {
245            if statement.if_exists {
246                context.notices.notice(
247                    "NOTICE",
248                    &format!("role \"{name}\" does not exist, skipping"),
249                );
250                continue;
251            }
252            return Err(SQLError::Routine {
253                sqlstate: "42704".into(),
254                message: format!("role \"{name}\" does not exist"),
255            });
256        }
257        if name == current || name == session {
258            return Err(SQLError::Routine {
259                sqlstate: "55006".into(),
260                message: "current user cannot be dropped".into(),
261            });
262        }
263        require_role_administration_for(context.roles, snapshot, current, &name, "drop role")?;
264        names.push(name);
265    }
266    Ok(names)
267}
268
269pub fn bind_grant_role_statement(
270    context: &RoleValidationContext<'_>,
271    statement: &GrantRoleStmt,
272) -> GrantRoleStmt {
273    GrantRoleStmt {
274        granted_roles: statement
275            .granted_roles
276            .iter()
277            .map(|role| resolve_role_reference(context.names, role))
278            .collect(),
279        grantee_roles: statement
280            .grantee_roles
281            .iter()
282            .map(|role| resolve_role_reference(context.names, role))
283            .collect(),
284        is_grant: statement.is_grant,
285        options: statement.options,
286        grantor: statement
287            .grantor
288            .as_ref()
289            .map(|role| resolve_role_reference(context.names, role)),
290        cascade: statement.cascade,
291    }
292}
293
294pub fn ensure_no_grantor_dependencies(
295    memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
296    names_set: &BTreeSet<String>,
297) -> Result<(), SQLError> {
298    for membership in memberships.values() {
299        if names_set.contains(&membership.grantor)
300            && !names_set.contains(&membership.role)
301            && !names_set.contains(&membership.member)
302        {
303            return Err(SQLError::Routine {
304                sqlstate: "2BP01".into(),
305                message: format!(
306                    "role \"{}\" cannot be dropped because some objects depend on it: privileges for membership of role {} in role {}",
307                    membership.grantor, membership.member, membership.role
308                ),
309            });
310        }
311    }
312    Ok(())
313}
314
315#[cfg(test)]
316mod tests;