1use super::{
10 guards::RoleCatalogGuards,
11 memberships::{
12 insufficient_privilege, require_role_attribute_authority, role_has_transitive_admin,
13 role_is_superuser,
14 },
15 RoleDefinition, RoleIdentity, RoleMembership, RoleMembershipKey, RoleReference,
16 RoleReferenceNames,
17};
18use crate::catalog::roles::identity::{RoleBinding, RoleSubject};
19use crate::{
20 ast::{AlterRoleStmt, RoleAttribute, RoleSpecification},
21 SQLError,
22};
23use std::collections::{BTreeMap, BTreeSet};
24
25pub trait RoleNotices {
26 fn notice(&self, level: &str, message: &str);
27}
28
29#[derive(Clone, Copy)]
30pub struct RoleValidationContext<'a> {
31 pub names: &'a dyn RoleReferenceNames,
32 pub roles: &'a dyn RoleCatalogGuards,
33 pub notices: &'a dyn RoleNotices,
34}
35
36pub fn require_role_creation(context: &RoleValidationContext<'_>) -> Result<(), SQLError> {
37 let current = context.names.current_role();
38 require_createrole(&context.roles.role_definitions(), ¤t, "create role")
39}
40
41fn require_createrole(
42 roles: &BTreeMap<String, RoleDefinition>,
43 current: &(impl RoleSubject + ?Sized),
44 action: &str,
45) -> Result<(), SQLError> {
46 let allowed = current.role_definition(roles).is_some_and(|role| {
47 role.has(RoleAttribute::Superuser) || role.has(RoleAttribute::CreateRole)
48 });
49 if allowed {
50 Ok(())
51 } else {
52 Err(insufficient_privilege(&format!(
53 "permission denied to {action}"
54 )))
55 }
56}
57
58pub fn require_role_administration_for(
59 catalog: &dyn RoleCatalogGuards,
60 roles: &BTreeMap<String, RoleDefinition>,
61 current: &(impl RoleSubject + ?Sized),
62 target: &str,
63 action: &str,
64) -> Result<(), SQLError> {
65 if role_is_superuser(roles, current) {
66 return Ok(());
67 }
68 let can_create_roles = current
69 .role_definition(roles)
70 .is_some_and(|role| role.has(RoleAttribute::CreateRole));
71 let memberships = catalog.role_memberships();
72 if can_create_roles
73 && current
74 .role_definition(roles)
75 .zip(roles.get(target))
76 .is_some_and(|(member, role)| {
77 role_has_transitive_admin(&memberships, member.identity(), role.identity())
78 })
79 {
80 Ok(())
81 } else {
82 Err(insufficient_privilege(&format!(
83 "permission denied to {action}"
84 )))
85 }
86}
87
88pub fn create_role_candidate(
89 roles: &BTreeMap<String, RoleDefinition>,
90 current: &(impl RoleSubject + ?Sized),
91 definition: RoleDefinition,
92) -> Result<(BTreeMap<String, RoleDefinition>, bool), SQLError> {
93 if roles.contains_key(&definition.name) {
94 return Err(SQLError::Routine {
95 sqlstate: "42710".into(),
96 message: format!("role \"{}\" already exists", definition.name),
97 });
98 }
99 let current_is_superuser = role_is_superuser(roles, current);
100 let mut next_roles = roles.clone();
101 next_roles.insert(definition.name.clone(), definition);
102 Ok((next_roles, current_is_superuser))
103}
104
105pub fn alter_role_candidate(
106 context: &RoleValidationContext<'_>,
107 roles: &BTreeMap<String, RoleDefinition>,
108 current: &(impl RoleSubject + ?Sized),
109 name: String,
110 statement: &AlterRoleStmt,
111) -> Result<BTreeMap<String, RoleDefinition>, SQLError> {
112 let existing = roles.get(&name).cloned().ok_or_else(|| SQLError::Routine {
113 sqlstate: "42704".into(),
114 message: format!("role \"{name}\" does not exist"),
115 })?;
116 require_role_administration_for(context.roles, roles, current, &name, "alter role")?;
117 require_role_attribute_authority(
118 roles,
119 current,
120 statement.attributes.keys().copied(),
121 "alter role",
122 )?;
123 let current_is_superuser = current
124 .role_definition(roles)
125 .is_some_and(|role| role.has(RoleAttribute::Superuser));
126 if (statement.attributes.contains_key(&RoleAttribute::Superuser)
127 || existing.has(RoleAttribute::Superuser))
128 && !current_is_superuser
129 {
130 return Err(insufficient_privilege(
131 "must be superuser to alter superuser roles or change superuser attribute",
132 ));
133 }
134 let mut updated = existing;
135 for (&attribute, &enabled) in &statement.attributes {
136 if enabled {
137 updated.attributes.insert(attribute);
138 } else {
139 updated.attributes.remove(&attribute);
140 }
141 }
142 if let Some(value) = statement.connection_limit {
143 updated.connection_limit = value;
144 }
145 updated.advance_revision()?;
146 let mut next = roles.clone();
147 next.insert(name, updated);
148 Ok(next)
149}
150
151pub struct RoleDropAuthority {
153 current: RoleBinding,
154 session: RoleReference,
155}
156
157impl RoleDropAuthority {
158 pub fn new(
159 current: RoleReference,
160 session: RoleReference,
161 roles: &BTreeMap<String, RoleDefinition>,
162 ) -> Result<Self, SQLError> {
163 require_createrole(roles, ¤t, "drop role")?;
164 let session = match session {
165 RoleReference::Bound(_) => session,
166 RoleReference::Named(_) => {
167 RoleReference::Bound(std::sync::Arc::new(session.bind(roles)?))
168 }
169 };
170 Ok(Self {
171 current: current.bind(roles)?,
172 session,
173 })
174 }
175
176 pub fn resolve_target(
178 &self,
179 context: &RoleValidationContext<'_>,
180 requested: &RoleSpecification,
181 if_exists: bool,
182 roles: &BTreeMap<String, RoleDefinition>,
183 ) -> Result<Option<RoleBinding>, SQLError> {
184 let name = match requested {
186 RoleSpecification::Named(name) if name != "public" => name,
187 _ => {
188 return Err(SQLError::Routine {
189 sqlstate: "22023".into(),
190 message: "cannot use special role specifier in DROP ROLE".into(),
191 });
192 }
193 };
194 let Some(role) = roles.get(name) else {
195 if if_exists {
196 context.notices.notice(
197 "NOTICE",
198 &format!("role \"{name}\" does not exist, skipping"),
199 );
200 return Ok(None);
201 }
202 return Err(SQLError::Routine {
203 sqlstate: "42704".into(),
204 message: format!("role \"{name}\" does not exist"),
205 });
206 };
207 require_role_drop_authority(context, roles, &self.current, &self.session, name)?;
208 RoleBinding::from_definition(role).map(Some)
209 }
210}
211
212pub fn role_drop_memberships_candidate(
213 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
214 removed: RoleIdentity,
215) -> BTreeMap<RoleMembershipKey, RoleMembership> {
216 memberships
217 .iter()
218 .filter(|(_, membership)| {
219 membership.role.identity() != removed && membership.member.identity() != removed
220 })
221 .map(|(key, membership)| (*key, membership.clone()))
222 .collect()
223}
224
225fn require_role_drop_authority(
226 context: &RoleValidationContext<'_>,
227 roles: &BTreeMap<String, RoleDefinition>,
228 current: &(impl RoleSubject + ?Sized),
229 session: &(impl RoleSubject + ?Sized),
230 name: &str,
231) -> Result<(), SQLError> {
232 let protected_user = if current.role_name(roles) == Some(name)
233 || context.names.outer_role().role_name(roles) == Some(name)
234 {
235 Some("current")
236 } else if session.role_name(roles) == Some(name) {
237 Some("session")
238 } else {
239 None
240 };
241 if let Some(subject) = protected_user {
242 return Err(SQLError::Routine {
243 sqlstate: "55006".into(),
244 message: format!("{subject} user cannot be dropped"),
245 });
246 }
247 if roles
248 .get(name)
249 .is_some_and(|role| role.has(RoleAttribute::Superuser))
250 && !role_is_superuser(roles, current)
251 {
252 return Err(insufficient_privilege("permission denied to drop role"));
253 }
254 if role_is_superuser(roles, current) {
255 return Ok(());
256 }
257 let memberships = context.roles.role_memberships();
258 if current
259 .role_definition(roles)
260 .zip(roles.get(name))
261 .is_some_and(|(member, role)| {
262 role_has_transitive_admin(&memberships, member.identity(), role.identity())
263 })
264 {
265 Ok(())
266 } else {
267 Err(insufficient_privilege("permission denied to drop role"))
268 }
269}
270
271pub fn ensure_no_grantor_dependencies(
272 memberships: &BTreeMap<RoleMembershipKey, RoleMembership>,
273 identities: &BTreeSet<RoleIdentity>,
274) -> Result<(), SQLError> {
275 for membership in memberships.values() {
276 if identities.contains(&membership.grantor.identity())
277 && !identities.contains(&membership.role.identity())
278 && !identities.contains(&membership.member.identity())
279 {
280 return Err(SQLError::Routine {
281 sqlstate: "2BP01".into(),
282 message: format!(
283 "role \"{}\" cannot be dropped because some objects depend on it: privileges for membership of role {} in role {}",
284 membership.grantor.name, membership.member.name, membership.role.name
285 ),
286 });
287 }
288 }
289 Ok(())
290}
291
292#[cfg(test)]
293mod tests;